mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-05-24 01:12:54 -04: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