mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-11-03 19:18:22 -05:00 
			
		
		
		
	Make OIDC groups claim configurable and optional (#3552)
This commit is contained in:
		
							parent
							
								
									6957e2fa74
								
							
						
					
					
						commit
						fac1df31d3
					
				@ -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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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
											
										
									
								
							@ -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
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
				
			|||||||
@ -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(
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
@ -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,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user