Make OIDC groups claim configurable and optional (#3552)

This commit is contained in:
Carter 2024-05-02 22:55:47 -05:00 committed by GitHub
parent 6957e2fa74
commit fac1df31d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 33 additions and 16 deletions

View File

@ -20,7 +20,7 @@ Before you can start using OIDC Authentication, you must first configure a new c
1. Create a new client application 1. Create a new client application
- The Provider type should be OIDC or OAuth2 - The Provider type should be OIDC or OAuth2
- The Grant type should be `Authorization Code` - 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` - The Client type should be `public`
2. Configure redirect URI 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 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 ## Mealie Setup
@ -50,7 +52,7 @@ Take the client id and your discovery URL and update your environment variables
### Groups ### 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. `OIDC_USER_GROUP`: Users must be a part of this group (within your IdP) to be able to log in.

View File

@ -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_PROVIDER_NAME\>" | | OIDC_PROVIDER_NAME | OAuth | The provider name is shown in SSO login button. "Login with <OIDC_PROVIDER_NAME\>" |
| 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_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_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`) | | OIDC_TLS_CACERTFILE | None | File path to Certificate Authority used to verify server certificate (e.g. `/path/to/ca.crt`) |
### Themeing ### Themeing

File diff suppressed because one or more lines are too long

View File

@ -9,7 +9,6 @@ export default class DynamicOpenIDConnectScheme extends OpenIDConnectScheme {
async mounted() { async mounted() {
await this.getConfiguration(); await this.getConfiguration();
this.options.scope = ["openid", "profile", "email", "groups"]
this.configurationDocument = new ConfigurationDocument( this.configurationDocument = new ConfigurationDocument(
this, this,
@ -78,7 +77,7 @@ export default class DynamicOpenIDConnectScheme extends OpenIDConnectScheme {
// Update tokens with mealie token // Update tokens with mealie token
this.updateTokens(response) this.updateTokens(response)
} catch (e) { } catch (e) {
if (e.response?.status === 401) { if (e.response?.status === 401 || e.response?.status === 500) {
this.$auth.reset() this.$auth.reset()
} }
const currentUrl = new URL(window.location.href) const currentUrl = new URL(window.location.href)
@ -111,6 +110,11 @@ export default class DynamicOpenIDConnectScheme extends OpenIDConnectScheme {
const data = await response.json(); const data = await response.json();
this.options.endpoints.configuration = data.configurationUrl; this.options.endpoints.configuration = data.configurationUrl;
this.options.clientId = data.clientId; 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) { } catch (error) {
// pass // pass
} }

View File

@ -37,7 +37,7 @@ class OpenIDProvider(AuthProvider[OIDCRequest]):
user = self.try_get_user(claims.get(settings.OIDC_USER_CLAIM)) user = self.try_get_user(claims.get(settings.OIDC_USER_CLAIM))
is_admin = False is_admin = False
if settings.OIDC_USER_GROUP or settings.OIDC_ADMIN_GROUP: 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_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 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) repos.users.update(user.id, user)
return self.get_access_token(user, settings.OIDC_REMEMBER_ME) 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 return None
def get_claims(self, settings: AppSettings) -> JWTClaims | None: def get_claims(self, settings: AppSettings) -> JWTClaims | None:
"""Get the claims from the ID token and check if the required claims are present""" """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() jwks = OpenIDProvider.get_jwks()
if not jwks: if not jwks:
return None return None
@ -98,11 +98,13 @@ class OpenIDProvider(AuthProvider[OIDCRequest]):
try: try:
claims.validate() claims.validate()
except ExpiredTokenError as e: except ExpiredTokenError as e:
self._logger.debug(f"[OIDC] {e.error}: {e.description}") self._logger.error(f"[OIDC] {e.error}: {e.description}")
return None return None
except Exception as e:
self._logger.error("[OIDC] Exception while validating id_token claims", e)
if not claims: if not claims:
self._logger.warning("[OIDC] Claims not found") self._logger.error("[OIDC] Claims not found")
return None return None
if not required_claims.issubset(claims.keys()): if not required_claims.issubset(claims.keys()):
self._logger.error( self._logger.error(

View File

@ -192,17 +192,20 @@ class AppSettings(BaseSettings):
OIDC_REMEMBER_ME: bool = False OIDC_REMEMBER_ME: bool = False
OIDC_SIGNING_ALGORITHM: str = "RS256" OIDC_SIGNING_ALGORITHM: str = "RS256"
OIDC_USER_CLAIM: str = "email" OIDC_USER_CLAIM: str = "email"
OIDC_GROUPS_CLAIM: str | None = "groups"
OIDC_TLS_CACERTFILE: str | None = None OIDC_TLS_CACERTFILE: str | None = None
@property @property
def OIDC_READY(self) -> bool: def OIDC_READY(self) -> bool:
"""Validates OIDC settings are all set""" """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 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 # Testing Config

View File

@ -66,4 +66,8 @@ def get_oidc_info(resp: Response):
settings = get_app_settings() settings = get_app_settings()
resp.headers["Cache-Control"] = "public, max-age=604800" 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,
)

View File

@ -71,3 +71,4 @@ class CheckAppConfig(MealieModel):
class OIDCInfo(MealieModel): class OIDCInfo(MealieModel):
configuration_url: str | None configuration_url: str | None
client_id: str | None client_id: str | None
groups_claim: str | None