Add Keycloak SSO (#2272)

* feat: add `keycloak` SSO

---------

Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
This commit is contained in:
Vlad Stan 2024-02-14 10:23:37 +02:00 committed by GitHub
parent b8d295a5b7
commit 526467747e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 113 additions and 6 deletions

View file

@ -7,7 +7,7 @@
# Enable Admin GUI, available for the first user in LNBITS_ADMIN_USERS if available.
# Warning: Enabling this will make LNbits ignore most configurations in file. Only the
# configurations defined in `ReadOnlySettings` will still be read from the environment variables.
# The rest of the settings will be stored in your database and you will be able to change them
# The rest of the settings will be stored in your database and you will be able to change them
# only through the Admin UI.
# Disable this to make LNbits use this config file again.
LNBITS_ADMIN_UI=false
@ -107,21 +107,28 @@ LNTIPS_API_ENDPOINT=https://ln.tips
# Secret Key: will default to the hash of the super user. It is strongly recommended that you set your own value.
AUTH_SECRET_KEY=""
AUTH_TOKEN_EXPIRE_MINUTES=525600
# Possible authorization methods: user-id-only, username-password, google-auth, github-auth
# Possible authorization methods: user-id-only, username-password, google-auth, github-auth, keycloak-auth
AUTH_ALLOWED_METHODS="user-id-only, username-password"
# Set this flag if HTTP is used for OAuth
# OAUTHLIB_INSECURE_TRANSPORT="1"
# Google OAuth Config
# Make sure thant the authorized redirect URIs contain https://{domain}/api/v1/auth/google/token
# Make sure that the authorized redirect URIs contain https://{domain}/api/v1/auth/google/token
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""
# GitHub OAuth Config
# Make sure thant the authorization callback URL is set to https://{domain}/api/v1/auth/github/token
# Make sure that the authorization callback URL is set to https://{domain}/api/v1/auth/github/token
GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET=""
# Keycloak OAuth Config
# Make sure that the valid redirect URIs contain https://{domain}/api/v1/auth/keycloak/token
KEYCLOAK_CLIENT_ID=""
KEYCLOAK_CLIENT_SECRET=""
KEYCLOAK_DISCOVERY_URL=""
######################################
# uvicorn variable, uncomment to allow https behind a proxy

View file

@ -0,0 +1,37 @@
"""Keycloak SSO Login Helper
"""
from typing import Optional
import httpx
from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase
class KeycloakSSO(SSOBase):
"""Class providing login via Keycloak OAuth"""
provider = "keycloak"
scope = ["openid", "email", "profile"]
discovery_url = ""
async def openid_from_response(
self, response: dict, session: Optional["httpx.AsyncClient"] = None
) -> OpenID:
"""Return OpenID from user information provided by Keycloak"""
return OpenID(
email=response.get("email", ""),
provider=self.provider,
id=response.get("sub"),
first_name=response.get("given_name"),
last_name=response.get("family_name"),
display_name=response.get("name"),
picture=response.get("picture"),
)
async def get_discovery_document(self) -> DiscoveryDocument:
"""Get document containing handy urls"""
async with httpx.AsyncClient() as session:
response = await session.get(self.discovery_url)
content = response.json()
return content

View file

@ -78,6 +78,41 @@
</div>
</div>
</q-card-section>
<q-card-section
v-if="formData.auth_allowed_methods?.includes('keycloak-auth')"
class="q-pl-xl"
>
<strong class="q-my-none q-mb-sm">Keycloak Auth</strong>
<div class="row">
<div class="col-md-4 col-sm-12 q-pr-sm">
<q-input
filled
v-model="formData.keycloak_discovery_url"
label="Keycloak Discovey URL"
>
</q-input>
</div>
<div class="col-md-4 col-sm-12 q-pr-sm">
<q-input
filled
v-model="formData.keycloak_client_id"
label="Keycloak Client ID"
hint="Make sure thant the authorization callback URL is set to https://{domain}/api/v1/auth/keycloak/token"
>
</q-input>
</div>
<div class="col-md-4 col-sm-12">
<q-input
filled
v-model="formData.keycloak_client_secret"
type="password"
label="Keycloak Client Secret"
>
</q-input>
</div>
</div>
</q-card-section>
<q-separator></q-separator>
<q-card-section class="q-pa-none">
<br />

View file

@ -263,6 +263,25 @@
<div><span v-text="$t('signin_with_github')"></span></div>
</q-btn>
</div>
{%endif%} {% if "keycloak-auth" in LNBITS_AUTH_METHODS %}
<div class="col-12 full-width q-pa-sm">
<q-btn
href="/api/v1/auth/keycloak"
type="a"
outline
no-caps
color="grey"
rounded
class="full-width"
>
<q-avatar size="32px" class="q-mr-md">
<q-img
:src="'{{ static_url_for('static', 'images/keycloak-logo.png') }}'"
></q-img>
</q-avatar>
<div><span v-text="$t('signin_with_keycloak')"></span></div>
</q-btn>
</div>
{%endif%}
</div>
</q-card-section>

View file

@ -262,6 +262,7 @@ class AuthMethods(Enum):
username_and_password = "username-password"
google_auth = "google-auth"
github_auth = "github-auth"
keycloak_auth = "keycloak-auth"
class AuthSettings(LNbitsSettings):
@ -288,6 +289,12 @@ class GitHubAuthSettings(LNbitsSettings):
github_client_secret: str = Field(default="")
class KeycloakAuthSettings(LNbitsSettings):
keycloak_discovery_url: str = Field(default="")
keycloak_client_id: str = Field(default="")
keycloak_client_secret: str = Field(default="")
class EditableSettings(
UsersSettings,
ExtensionsSettings,
@ -301,6 +308,7 @@ class EditableSettings(
AuthSettings,
GoogleAuthSettings,
GitHubAuthSettings,
KeycloakAuthSettings,
):
@validator(
"lnbits_admin_users",

File diff suppressed because one or more lines are too long

View file

@ -209,6 +209,7 @@ window.localisation.en = {
account_settings: 'Account Settings',
signin_with_google: 'Sign in with Google',
signin_with_github: 'Sign in with GitHub',
signin_with_keycloak: 'Sign in with Keycloak',
username_or_email: 'Username or Email',
password: 'Password',
password_config: 'Password Config',

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -1,6 +1,6 @@
// update cache version every time there is a new deployment
// so the service worker reinitializes the cache
const CACHE_VERSION = 114
const CACHE_VERSION = 115
const CURRENT_CACHE = `lnbits-${CACHE_VERSION}-`
const getApiKey = request => {