Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add groups claim to Id token in openid flow #520

Merged
merged 7 commits into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 50 additions & 15 deletions src/handlers/openid_flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -83,6 +83,16 @@ pub async fn discovery_keys(State(appstate): State<AppState>) -> ApiResult {
status: StatusCode::OK,
})
}
pub type DefguardIdTokenFields = IdTokenFields<
GroupClaims,
EmptyExtraTokenFields,
CoreGenderClaim,
CoreJweContentEncryptionAlgorithm,
CoreJwsSigningAlgorithm,
CoreJsonWebKeyType,
>;

pub type DefguardTokenResponse = StandardTokenResponse<DefguardIdTokenFields, CoreTokenType>;

/// Provide `OAuth2Client` when Basic Authorization header contains `client_id` and `client_secret`.
#[async_trait]
Expand Down Expand Up @@ -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<Vec<String>>,
}

impl AdditionalClaims for GroupClaims {}

pub async fn get_group_claims(pool: &DbPool, user: &User) -> Result<GroupClaims, WebError> {
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,
Expand Down Expand Up @@ -601,7 +626,8 @@ impl TokenRequest {
base_url: &Url,
secret: T,
rsa_key: Option<CoreRsaPrivateSigningKey>,
) -> Result<CoreTokenResponse, CoreErrorResponseType>
group_claims: GroupClaims,
) -> Result<DefguardTokenResponse, CoreErrorResponseType>
where
T: Into<Vec<u8>>,
{
Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -767,13 +794,19 @@ 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,
(&user).into(),
&appstate.config.url,
client.client_secret,
appstate.config.openid_key(),
group_claims,
) {
Ok(response) => {
token.save(&appstate.pool).await?;
Expand Down Expand Up @@ -879,6 +912,7 @@ pub async fn openid_configuration(State(appstate): State<AppState>) -> 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()),
Expand All @@ -891,6 +925,7 @@ pub async fn openid_configuration(State(appstate): State<AppState>) -> 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,
Expand Down
4 changes: 4 additions & 0 deletions web/src/i18n/en/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1086,6 +1086,9 @@ const en: BaseTranslation = {
phone: {
label: 'Phone',
},
groups: {
label: 'Groups',
},
},
controls: {
addUrl: 'Add URL',
Expand Down Expand Up @@ -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',
Expand Down
20 changes: 20 additions & 0 deletions web/src/i18n/i18n-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2523,6 +2523,12 @@ type RootTranslation = {
*/
label: string
}
groups: {
/**
* G​r​o​u​p​s
*/
label: string
}
}
controls: {
/**
Expand Down Expand Up @@ -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: {
/**
Expand Down Expand Up @@ -6057,6 +6067,12 @@ export type TranslationFunctions = {
*/
label: () => LocalizedString
}
groups: {
/**
* Groups
*/
label: () => LocalizedString
}
}
controls: {
/**
Expand Down Expand Up @@ -6294,6 +6310,10 @@ export type TranslationFunctions = {
* Know your phone number.
*/
phone: () => LocalizedString
/**
* Know your groups membership.
*/
groups: () => LocalizedString
}
controls: {
/**
Expand Down
4 changes: 4 additions & 0 deletions web/src/i18n/pl/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1070,6 +1070,9 @@ Uwaga, konfiguracje tutaj podane, nie posiadają twojego klucza prywatnego. Musi
phone: {
label: 'Telefon',
},
groups: {
label: 'Grupy',
},
},
controls: {
addUrl: 'Dodaj URL',
Expand Down Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions web/src/pages/allow/OpenidAllowPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <LoaderPage />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ export const OpenIdClientModalFormScopes = ({ control, disabled = false }: Props
value={value.includes(OpenIdClientScope.PHONE)}
onChange={() => handleChange(OpenIdClientScope.PHONE, value)}
/>
<LabeledCheckbox
data-testid="field-scope-groups"
label={LL.openidOverview.modals.openidClientModal.form.fields.groups.label()}
disabled={disabled}
value={value.includes(OpenIdClientScope.GROUPS)}
onChange={() => handleChange(OpenIdClientScope.GROUPS, value)}
/>
</div>
);
};
1 change: 1 addition & 0 deletions web/src/pages/openid/modals/OpenIdClientModal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ export enum OpenIdClientScope {
PROFILE = 'profile',
EMAIL = 'email',
PHONE = 'phone',
GROUPS = 'groups',
}