From fac1df31d392b986838de79a36648fb656534c9a Mon Sep 17 00:00:00 2001 From: Carter <35710697+cmintey@users.noreply.github.com> Date: Thu, 2 May 2024 22:55:47 -0500 Subject: [PATCH] Make OIDC groups claim configurable and optional (#3552) --- .../getting-started/authentication/oidc.md | 8 +++++--- .../getting-started/installation/backend-config.md | 3 ++- docs/docs/overrides/api.html | 2 +- frontend/schemes/DynamicOpenIDConnectScheme.js | 8 ++++++-- mealie/core/security/providers/openid_provider.py | 12 +++++++----- mealie/core/settings/settings.py | 9 ++++++--- mealie/routes/app/app_about.py | 6 +++++- mealie/schema/admin/about.py | 1 + 8 files changed, 33 insertions(+), 16 deletions(-) diff --git a/docs/docs/documentation/getting-started/authentication/oidc.md b/docs/docs/documentation/getting-started/authentication/oidc.md index 663a6245c3a5..96cb24b25781 100644 --- a/docs/docs/documentation/getting-started/authentication/oidc.md +++ b/docs/docs/documentation/getting-started/authentication/oidc.md @@ -20,7 +20,7 @@ Before you can start using OIDC Authentication, you must first configure a new c 1. Create a new client application - The Provider type should be OIDC or OAuth2 - The Grant type should be `Authorization Code` - - The Application type should be `Web` + - The Application type should be `Web` or `SPA` - The Client type should be `public` 2. Configure redirect URI @@ -42,7 +42,9 @@ Before you can start using OIDC Authentication, you must first configure a new c 4. Configure allowed scopes - The scopes required are `openid profile email groups` + The scopes required are `openid profile email` + + If you plan to use the [groups](#groups) to configure access within Mealie, you will need to also add the scope defined by the `OIDC_GROUPS_CLAIM` environment variable. The default claim is `groups` ## Mealie Setup @@ -50,7 +52,7 @@ Take the client id and your discovery URL and update your environment variables ### Groups -There are two (optional) [environment variables](../installation/backend-config.md#openid-connect-oidc) that can control which of the users in your IdP can log in to Mealie and what permissions they will have. The groups should be **defined in your IdP** and be returned in the `groups` claim. +There are two (optional) [environment variables](../installation/backend-config.md#openid-connect-oidc) that can control which of the users in your IdP can log in to Mealie and what permissions they will have. Keep in mind that these groups **do not necessarily correspond to groups in Mealie**. The groups claim is configurable via the `OIDC_GROUPS_CLAIM` environment variable. The groups should be **defined in your IdP** and be returned in the configured claim value. `OIDC_USER_GROUP`: Users must be a part of this group (within your IdP) to be able to log in. diff --git a/docs/docs/documentation/getting-started/installation/backend-config.md b/docs/docs/documentation/getting-started/installation/backend-config.md index 42d585eb71cb..51ea84f17807 100644 --- a/docs/docs/documentation/getting-started/installation/backend-config.md +++ b/docs/docs/documentation/getting-started/installation/backend-config.md @@ -98,7 +98,8 @@ For usage, see [Usage - OpenID Connect](../authentication/oidc.md) | OIDC_PROVIDER_NAME | OAuth | The provider name is shown in SSO login button. "Login with " | | OIDC_REMEMBER_ME | False | Because redirects bypass the login screen, you cant extend your session by clicking the "Remember Me" checkbox. By setting this value to true, a session will be extended as if "Remember Me" was checked | | OIDC_SIGNING_ALGORITHM | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) | -| OIDC_USER_CLAIM | email | Optional: 'email', 'preferred_username' | +| OIDC_USER_CLAIM | email | This is the claim which Mealie will use to look up an existing user by (e.g. "email", "preferred_username") | +| OIDC_GROUPS_CLAIM | groups | Optional if not using `OIDC_USER_GROUP` or `OIDC_ADMIN_GROUP`. This is the claim Mealie will request from your IdP and will use to compare to `OIDC_USER_GROUP` or `OIDC_ADMIN_GROUP` to allow the user to log in to Mealie or is set as an admin. **Your IdP must be configured to grant this claim**| | OIDC_TLS_CACERTFILE | None | File path to Certificate Authority used to verify server certificate (e.g. `/path/to/ca.crt`) | ### Themeing diff --git a/docs/docs/overrides/api.html b/docs/docs/overrides/api.html index ec16cb46c57b..ea25a4ac3f5a 100644 --- a/docs/docs/overrides/api.html +++ b/docs/docs/overrides/api.html @@ -14,7 +14,7 @@
diff --git a/frontend/schemes/DynamicOpenIDConnectScheme.js b/frontend/schemes/DynamicOpenIDConnectScheme.js index 183a8a9cd95c..ee5bf48f7f15 100644 --- a/frontend/schemes/DynamicOpenIDConnectScheme.js +++ b/frontend/schemes/DynamicOpenIDConnectScheme.js @@ -9,7 +9,6 @@ export default class DynamicOpenIDConnectScheme extends OpenIDConnectScheme { async mounted() { await this.getConfiguration(); - this.options.scope = ["openid", "profile", "email", "groups"] this.configurationDocument = new ConfigurationDocument( this, @@ -78,7 +77,7 @@ export default class DynamicOpenIDConnectScheme extends OpenIDConnectScheme { // Update tokens with mealie token this.updateTokens(response) } catch (e) { - if (e.response?.status === 401) { + if (e.response?.status === 401 || e.response?.status === 500) { this.$auth.reset() } const currentUrl = new URL(window.location.href) @@ -111,6 +110,11 @@ export default class DynamicOpenIDConnectScheme extends OpenIDConnectScheme { const data = await response.json(); this.options.endpoints.configuration = data.configurationUrl; this.options.clientId = data.clientId; + this.options.scope = ["openid", "profile", "email"] + if (data.groupsClaim !== null) { + this.options.scope.push(data.groupsClaim) + } + console.log(this.options.scope) } catch (error) { // pass } diff --git a/mealie/core/security/providers/openid_provider.py b/mealie/core/security/providers/openid_provider.py index ebc021838cc1..15fdb5900c9c 100644 --- a/mealie/core/security/providers/openid_provider.py +++ b/mealie/core/security/providers/openid_provider.py @@ -37,7 +37,7 @@ class OpenIDProvider(AuthProvider[OIDCRequest]): user = self.try_get_user(claims.get(settings.OIDC_USER_CLAIM)) is_admin = False if settings.OIDC_USER_GROUP or settings.OIDC_ADMIN_GROUP: - group_claim = claims.get("groups", []) + group_claim = claims.get(settings.OIDC_GROUPS_CLAIM, []) is_admin = settings.OIDC_ADMIN_GROUP in group_claim if settings.OIDC_ADMIN_GROUP else False is_valid_user = settings.OIDC_USER_GROUP in group_claim if settings.OIDC_USER_GROUP else True @@ -76,12 +76,12 @@ class OpenIDProvider(AuthProvider[OIDCRequest]): repos.users.update(user.id, user) return self.get_access_token(user, settings.OIDC_REMEMBER_ME) - self._logger.info("[OIDC] Found user but their AuthMethod does not match OIDC") + self._logger.warning("[OIDC] Found user but their AuthMethod does not match OIDC") return None def get_claims(self, settings: AppSettings) -> JWTClaims | None: """Get the claims from the ID token and check if the required claims are present""" - required_claims = {"preferred_username", "name", "email"} + required_claims = {"preferred_username", "name", "email", settings.OIDC_USER_CLAIM} jwks = OpenIDProvider.get_jwks() if not jwks: return None @@ -98,11 +98,13 @@ class OpenIDProvider(AuthProvider[OIDCRequest]): try: claims.validate() except ExpiredTokenError as e: - self._logger.debug(f"[OIDC] {e.error}: {e.description}") + self._logger.error(f"[OIDC] {e.error}: {e.description}") return None + except Exception as e: + self._logger.error("[OIDC] Exception while validating id_token claims", e) if not claims: - self._logger.warning("[OIDC] Claims not found") + self._logger.error("[OIDC] Claims not found") return None if not required_claims.issubset(claims.keys()): self._logger.error( diff --git a/mealie/core/settings/settings.py b/mealie/core/settings/settings.py index 8c8767902bce..6b042936a89f 100644 --- a/mealie/core/settings/settings.py +++ b/mealie/core/settings/settings.py @@ -192,17 +192,20 @@ class AppSettings(BaseSettings): OIDC_REMEMBER_ME: bool = False OIDC_SIGNING_ALGORITHM: str = "RS256" OIDC_USER_CLAIM: str = "email" + OIDC_GROUPS_CLAIM: str | None = "groups" OIDC_TLS_CACERTFILE: str | None = None @property def OIDC_READY(self) -> bool: """Validates OIDC settings are all set""" - required = {self.OIDC_CLIENT_ID, self.OIDC_CONFIGURATION_URL} + required = {self.OIDC_CLIENT_ID, self.OIDC_CONFIGURATION_URL, self.OIDC_USER_CLAIM} not_none = None not in required - valid_user_claim = self.OIDC_USER_CLAIM in ["email", "preferred_username"] + valid_group_claim = True + if (not self.OIDC_USER_GROUP or not self.OIDC_ADMIN_GROUP) and not self.OIDC_GROUPS_CLAIM: + valid_group_claim = False - return self.OIDC_AUTH_ENABLED and not_none and valid_user_claim + return self.OIDC_AUTH_ENABLED and not_none and valid_group_claim # =============================================== # Testing Config diff --git a/mealie/routes/app/app_about.py b/mealie/routes/app/app_about.py index 1c080efdd9c1..b54a739f223b 100644 --- a/mealie/routes/app/app_about.py +++ b/mealie/routes/app/app_about.py @@ -66,4 +66,8 @@ def get_oidc_info(resp: Response): settings = get_app_settings() resp.headers["Cache-Control"] = "public, max-age=604800" - return OIDCInfo(configuration_url=settings.OIDC_CONFIGURATION_URL, client_id=settings.OIDC_CLIENT_ID) + return OIDCInfo( + configuration_url=settings.OIDC_CONFIGURATION_URL, + client_id=settings.OIDC_CLIENT_ID, + groups_claim=settings.OIDC_GROUPS_CLAIM if settings.OIDC_USER_GROUP or settings.OIDC_ADMIN_GROUP else None, + ) diff --git a/mealie/schema/admin/about.py b/mealie/schema/admin/about.py index e9ff92305a4a..3e4a47febd63 100644 --- a/mealie/schema/admin/about.py +++ b/mealie/schema/admin/about.py @@ -71,3 +71,4 @@ class CheckAppConfig(MealieModel): class OIDCInfo(MealieModel): configuration_url: str | None client_id: str | None + groups_claim: str | None