diff --git a/src/handlers/openid_flow.rs b/src/handlers/openid_flow.rs index 741fe31c3..60f86f9eb 100644 --- a/src/handlers/openid_flow.rs +++ b/src/handlers/openid_flow.rs @@ -15,17 +15,17 @@ use chrono::Utc; use openidconnect::{ core::{ CoreAuthErrorResponseType, CoreClaimName, CoreErrorResponseType, CoreGenderClaim, - CoreGrantType, CoreHmacKey, CoreIdToken, CoreIdTokenClaims, CoreIdTokenFields, - CoreJsonWebKeySet, CoreJwsSigningAlgorithm, CoreProviderMetadata, CoreResponseType, - CoreRsaPrivateSigningKey, CoreSubjectIdentifierType, CoreTokenResponse, CoreTokenType, + CoreGrantType, CoreHmacKey, CoreJsonWebKeySet, CoreJsonWebKeyType, + CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm, CoreProviderMetadata, + CoreResponseType, CoreRsaPrivateSigningKey, CoreSubjectIdentifierType, CoreTokenType, }, url::Url, - AccessToken, Audience, AuthUrl, AuthorizationCode, EmptyAdditionalClaims, + AccessToken, AdditionalClaims, Audience, AuthUrl, AuthorizationCode, EmptyAdditionalProviderMetadata, EmptyExtraTokenFields, EndUserEmail, EndUserFamilyName, - EndUserGivenName, EndUserName, EndUserPhoneNumber, EndUserUsername, IssuerUrl, - JsonWebKeySetUrl, LocalizedClaim, Nonce, PkceCodeChallenge, PkceCodeVerifier, - PrivateSigningKey, RefreshToken, ResponseTypes, Scope, StandardClaims, StandardErrorResponse, - StandardTokenResponse, SubjectIdentifier, TokenUrl, UserInfoUrl, + EndUserGivenName, EndUserName, EndUserPhoneNumber, EndUserUsername, IdToken, IdTokenClaims, + IdTokenFields, IssuerUrl, JsonWebKeySetUrl, LocalizedClaim, Nonce, PkceCodeChallenge, + PkceCodeVerifier, PrivateSigningKey, RefreshToken, ResponseTypes, Scope, StandardClaims, + StandardErrorResponse, StandardTokenResponse, SubjectIdentifier, TokenUrl, UserInfoUrl, }; use serde::{ de::{Deserialize, Deserializer, Error as DeError, Unexpected, Visitor}, @@ -83,6 +83,16 @@ pub async fn discovery_keys(State(appstate): State) -> ApiResult { status: StatusCode::OK, }) } +pub type DefguardIdTokenFields = IdTokenFields< + GroupClaims, + EmptyExtraTokenFields, + CoreGenderClaim, + CoreJweContentEncryptionAlgorithm, + CoreJwsSigningAlgorithm, + CoreJsonWebKeyType, +>; + +pub type DefguardTokenResponse = StandardTokenResponse; /// Provide `OAuth2Client` when Basic Authorization header contains `client_id` and `client_secret`. #[async_trait] @@ -474,6 +484,21 @@ pub async fn authorization( Ok(redirect_to(url, private_cookies)) } +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, Default)] +pub struct GroupClaims { + #[serde(skip_serializing_if = "Option::is_none")] + groups: Option>, +} + +impl AdditionalClaims for GroupClaims {} + +pub async fn get_group_claims(pool: &DbPool, user: &User) -> Result { + let groups = user.member_of_names(pool).await?; + Ok(GroupClaims { + groups: Some(groups), + }) +} + /// Login Authorization Endpoint redirect with authorization code pub async fn secure_authorization( session_info: SessionInfo, @@ -601,7 +626,8 @@ impl TokenRequest { base_url: &Url, secret: T, rsa_key: Option, - ) -> Result + group_claims: GroupClaims, + ) -> Result where T: Into>, { @@ -631,24 +657,25 @@ impl TokenRequest { let authorization_code = AuthorizationCode::new(code.into()); let issue_time = Utc::now(); let expiration = issue_time + chrono::Duration::seconds(SESSION_TIMEOUT as i64); - let id_token_claims = CoreIdTokenClaims::new( + let id_token_claims = IdTokenClaims::new( IssuerUrl::from_url(base_url.clone()), vec![Audience::new(auth_code.client_id.clone())], expiration, issue_time, claims, - EmptyAdditionalClaims {}, + group_claims, ) .set_nonce(auth_code.nonce.clone().map(Nonce::new)); + let id_token = match rsa_key { - Some(key) => CoreIdToken::new( + Some(key) => IdToken::new( id_token_claims, &key, CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, Some(&access_token), Some(&authorization_code), ), - None => CoreIdToken::new( + None => IdToken::new( id_token_claims, &CoreHmacKey::new(secret), CoreJwsSigningAlgorithm::HmacSha256, @@ -661,10 +688,10 @@ impl TokenRequest { None }; - let mut token_response = CoreTokenResponse::new( + let mut token_response = DefguardTokenResponse::new( access_token, CoreTokenType::Bearer, - CoreIdTokenFields::new(id_token, EmptyExtraTokenFields {}), + IdTokenFields::new(id_token, EmptyExtraTokenFields {}), ); token_response.set_refresh_token(Some(RefreshToken::new(token.refresh_token.clone()))); Ok(token_response) @@ -767,6 +794,11 @@ pub async fn token( auth_code.redirect_uri.clone(), auth_code.scope.clone(), ); + let group_claims = if auth_code.scope.contains("groups") { + get_group_claims(&appstate.pool, &user).await? + } else { + GroupClaims { groups: None } + }; match form.authorization_code_flow( &auth_code, &token, @@ -774,6 +806,7 @@ pub async fn token( &appstate.config.url, client.client_secret, appstate.config.openid_key(), + group_claims, ) { Ok(response) => { token.save(&appstate.pool).await?; @@ -879,6 +912,7 @@ pub async fn openid_configuration(State(appstate): State) -> ApiResult Scope::new("profile".into()), Scope::new("email".into()), Scope::new("phone".into()), + Scope::new("groups".into()), ])) .set_claims_supported(Some(vec![ CoreClaimName::new("iss".into()), @@ -891,6 +925,7 @@ pub async fn openid_configuration(State(appstate): State) -> ApiResult CoreClaimName::new("family_name".into()), CoreClaimName::new("email".into()), CoreClaimName::new("phone_number".into()), + CoreClaimName::new("groups".into()), ])) .set_grant_types_supported(Some(vec![ CoreGrantType::AuthorizationCode, diff --git a/web/src/i18n/en/index.ts b/web/src/i18n/en/index.ts index 358c71b1d..9368a7fdd 100644 --- a/web/src/i18n/en/index.ts +++ b/web/src/i18n/en/index.ts @@ -1086,6 +1086,9 @@ const en: BaseTranslation = { phone: { label: 'Phone', }, + groups: { + label: 'Groups', + }, }, controls: { addUrl: 'Add URL', @@ -1182,6 +1185,7 @@ const en: BaseTranslation = { profile: 'Know basic information from your profile like name, profile picture etc.', email: 'Know your email address.', phone: 'Know your phone number.', + groups: 'Know your groups membership.', }, controls: { accept: 'Accept', diff --git a/web/src/i18n/i18n-types.ts b/web/src/i18n/i18n-types.ts index fe3acefb8..b1b72c13e 100644 --- a/web/src/i18n/i18n-types.ts +++ b/web/src/i18n/i18n-types.ts @@ -2523,6 +2523,12 @@ type RootTranslation = { */ label: string } + groups: { + /** + * G​r​o​u​p​s + */ + label: string + } } controls: { /** @@ -2761,6 +2767,10 @@ type RootTranslation = { * K​n​o​w​ ​y​o​u​r​ ​p​h​o​n​e​ ​n​u​m​b​e​r​. */ phone: string + /** + * K​n​o​w​ ​y​o​u​r​ ​g​r​o​u​p​s​ ​m​e​m​b​e​r​s​h​i​p​. + */ + groups: string } controls: { /** @@ -6057,6 +6067,12 @@ export type TranslationFunctions = { */ label: () => LocalizedString } + groups: { + /** + * Groups + */ + label: () => LocalizedString + } } controls: { /** @@ -6294,6 +6310,10 @@ export type TranslationFunctions = { * Know your phone number. */ phone: () => LocalizedString + /** + * Know your groups membership. + */ + groups: () => LocalizedString } controls: { /** diff --git a/web/src/i18n/pl/index.ts b/web/src/i18n/pl/index.ts index dc024bc0b..00a6fa375 100644 --- a/web/src/i18n/pl/index.ts +++ b/web/src/i18n/pl/index.ts @@ -1070,6 +1070,9 @@ Uwaga, konfiguracje tutaj podane, nie posiadają twojego klucza prywatnego. Musi phone: { label: 'Telefon', }, + groups: { + label: 'Grupy', + }, }, controls: { addUrl: 'Dodaj URL', @@ -1167,6 +1170,7 @@ Uwaga, konfiguracje tutaj podane, nie posiadają twojego klucza prywatnego. Musi 'Poznać podstawowe informacje z twojego profilu, takie jak login, imię itp', email: 'Poznać twój adres e-mail.', phone: 'Poznać twój numer telefonu.', + groups: 'Poznać twoje grupy.', }, controls: { accept: 'Akceptuj', diff --git a/web/src/pages/allow/OpenidAllowPage.tsx b/web/src/pages/allow/OpenidAllowPage.tsx index 2a84373d6..7aa35c8d5 100644 --- a/web/src/pages/allow/OpenidAllowPage.tsx +++ b/web/src/pages/allow/OpenidAllowPage.tsx @@ -99,6 +99,7 @@ export const OpenidAllowPage = () => { profile: LL.openidAllow.scopes.profile(), email: LL.openidAllow.scopes.email(), phone: LL.openidAllow.scopes.phone(), + groups: LL.openidAllow.scopes.groups(), }; if (loadingInfo) return ; diff --git a/web/src/pages/openid/modals/OpenIdClientModal/components/OpenIdClientModalFormScopes.tsx b/web/src/pages/openid/modals/OpenIdClientModal/components/OpenIdClientModalFormScopes.tsx index c3a6ff939..531cbeac5 100644 --- a/web/src/pages/openid/modals/OpenIdClientModal/components/OpenIdClientModalFormScopes.tsx +++ b/web/src/pages/openid/modals/OpenIdClientModal/components/OpenIdClientModalFormScopes.tsx @@ -60,6 +60,13 @@ export const OpenIdClientModalFormScopes = ({ control, disabled = false }: Props value={value.includes(OpenIdClientScope.PHONE)} onChange={() => handleChange(OpenIdClientScope.PHONE, value)} /> + handleChange(OpenIdClientScope.GROUPS, value)} + /> ); }; diff --git a/web/src/pages/openid/modals/OpenIdClientModal/types.ts b/web/src/pages/openid/modals/OpenIdClientModal/types.ts index e832e7d7f..dff5a59ed 100644 --- a/web/src/pages/openid/modals/OpenIdClientModal/types.ts +++ b/web/src/pages/openid/modals/OpenIdClientModal/types.ts @@ -11,4 +11,5 @@ export enum OpenIdClientScope { PROFILE = 'profile', EMAIL = 'email', PHONE = 'phone', + GROUPS = 'groups', }