OIDC: Fallback to userinfo endpoint when JWT token does not contain an email claim.
This commit is contained in:
@@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. This projec
|
|||||||
If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If you are upgrading from v0.15.x and below, please read the [upgrading documentation](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md) for more information on how to upgrade from previous versions.
|
If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If you are upgrading from v0.15.x and below, please read the [upgrading documentation](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md) for more information on how to upgrade from previous versions.
|
||||||
|
|
||||||
## Added
|
## Added
|
||||||
|
- OIDC: Fallback to `userinfo` endpoint when JWT token does not contain an email claim.
|
||||||
- S3: `verifyAfterWrite` option to verify that objects have persisted after writing.
|
- S3: `verifyAfterWrite` option to verify that objects have persisted after writing.
|
||||||
|
|
||||||
## Changed
|
## Changed
|
||||||
|
|||||||
@@ -76,7 +76,15 @@ impl OpenIdDirectory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.validate_scopes(&token_data.claims)?;
|
self.validate_scopes(&token_data.claims)?;
|
||||||
return self.build_account(&token_data.claims);
|
|
||||||
|
let (email, claims) = if let Ok(email) = self.resolve_email(&token_data.claims)
|
||||||
|
{
|
||||||
|
(email, token_data.claims)
|
||||||
|
} else {
|
||||||
|
let claims = self.fetch_userinfo(token).await?;
|
||||||
|
(self.resolve_email(&claims)?, claims)
|
||||||
|
};
|
||||||
|
return self.build_account(email, &claims);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
last_err = Some(e);
|
last_err = Some(e);
|
||||||
@@ -92,7 +100,7 @@ impl OpenIdDirectory {
|
|||||||
|
|
||||||
async fn authenticate_opaque(&self, token: &str) -> Result<Account, OidcError> {
|
async fn authenticate_opaque(&self, token: &str) -> Result<Account, OidcError> {
|
||||||
let claims = self.fetch_userinfo(token).await?;
|
let claims = self.fetch_userinfo(token).await?;
|
||||||
self.build_account(&claims)
|
self.build_account(self.resolve_email(&claims)?, &claims)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_key(&self, kid: Option<&str>) -> Result<Vec<Arc<CachedKey>>, OidcError> {
|
async fn get_key(&self, kid: Option<&str>) -> Result<Vec<Arc<CachedKey>>, OidcError> {
|
||||||
@@ -190,9 +198,13 @@ impl OpenIdDirectory {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_account(&self, claims: &serde_json::Value) -> Result<Account, OidcError> {
|
fn build_account(
|
||||||
|
&self,
|
||||||
|
email: String,
|
||||||
|
claims: &serde_json::Value,
|
||||||
|
) -> Result<Account, OidcError> {
|
||||||
Ok(Account {
|
Ok(Account {
|
||||||
email: self.resolve_email(claims)?,
|
email,
|
||||||
email_aliases: Vec::new(),
|
email_aliases: Vec::new(),
|
||||||
secret: None,
|
secret: None,
|
||||||
groups: self
|
groups: self
|
||||||
|
|||||||
Binary file not shown.
@@ -1 +1 @@
|
|||||||
QRJ4vibPf1dYpzaA4YziIqMnAMR1UOHVUErrjOyjDeE
|
BzNqAzrQXuc-FYcvnkey9hBXkjMDd9HrgWlyb4S9I7E
|
||||||
@@ -68,6 +68,71 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"clientId": "stalwart-fallback",
|
||||||
|
"enabled": true,
|
||||||
|
"clientAuthenticatorType": "client-secret",
|
||||||
|
"secret": "stalwart-fallback-secret",
|
||||||
|
"redirectUris": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"webOrigins": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"publicClient": false,
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"directAccessGrantsEnabled": true,
|
||||||
|
"standardFlowEnabled": true,
|
||||||
|
"serviceAccountsEnabled": true,
|
||||||
|
"defaultClientScopes": [
|
||||||
|
"openid"
|
||||||
|
],
|
||||||
|
"optionalClientScopes": [
|
||||||
|
"email",
|
||||||
|
"profile",
|
||||||
|
"roles"
|
||||||
|
],
|
||||||
|
"protocolMappers": [
|
||||||
|
{
|
||||||
|
"name": "email-claim-userinfo-only",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-usermodel-attribute-mapper",
|
||||||
|
"consentRequired": false,
|
||||||
|
"config": {
|
||||||
|
"user.attribute": "email",
|
||||||
|
"id.token.claim": "false",
|
||||||
|
"access.token.claim": "false",
|
||||||
|
"claim.name": "email",
|
||||||
|
"userinfo.token.claim": "true",
|
||||||
|
"jsonType.label": "String"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "groups-userinfo-only",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-group-membership-mapper",
|
||||||
|
"consentRequired": false,
|
||||||
|
"config": {
|
||||||
|
"full.path": "false",
|
||||||
|
"id.token.claim": "false",
|
||||||
|
"access.token.claim": "false",
|
||||||
|
"claim.name": "groups",
|
||||||
|
"userinfo.token.claim": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "audience",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-audience-mapper",
|
||||||
|
"consentRequired": false,
|
||||||
|
"config": {
|
||||||
|
"included.client.audience": "stalwart",
|
||||||
|
"id.token.claim": "false",
|
||||||
|
"access.token.claim": "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"users": [
|
"users": [
|
||||||
|
|||||||
@@ -70,6 +70,55 @@ pub async fn test() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Test ODIC userinfo fallback
|
||||||
|
let mut config_userinfo_fallback = config.clone();
|
||||||
|
config_userinfo_fallback.claim_username = "email".to_string();
|
||||||
|
config_userinfo_fallback.require_scopes = Map::new(vec!["openid".to_string()]);
|
||||||
|
|
||||||
|
let token_openid_only = get_token_for_client(
|
||||||
|
"stalwart-fallback",
|
||||||
|
"stalwart-fallback-secret",
|
||||||
|
"john.doe@example.org",
|
||||||
|
"this is an OIDC password",
|
||||||
|
"openid",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut oidc_broken_userinfo = OpenIdDirectory::open(config_userinfo_fallback.clone())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
if let Directory::OpenId(directory) = &mut oidc_broken_userinfo {
|
||||||
|
directory.discovery.document.userinfo_endpoint = "http://invalid".to_string();
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
oidc_broken_userinfo
|
||||||
|
.authenticate(&Credentials::Bearer {
|
||||||
|
username: None,
|
||||||
|
token: token_openid_only.clone(),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
let oidc_userinfo_fallback = OpenIdDirectory::open(config_userinfo_fallback)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
oidc_userinfo_fallback
|
||||||
|
.authenticate(&Credentials::Bearer {
|
||||||
|
username: None,
|
||||||
|
token: token_openid_only,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
Account {
|
||||||
|
email: "john.doe@example.org".to_string(),
|
||||||
|
email_aliases: vec![],
|
||||||
|
secret: None,
|
||||||
|
groups: vec!["sales@example.org".to_string()],
|
||||||
|
description: None,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Not matching the required audience should fail
|
// Not matching the required audience should fail
|
||||||
let mut config_wrong_audience = config.clone();
|
let mut config_wrong_audience = config.clone();
|
||||||
config_wrong_audience.require_audience = Some("wrong_audience".to_string());
|
config_wrong_audience.require_audience = Some("wrong_audience".to_string());
|
||||||
@@ -115,17 +164,34 @@ pub async fn test() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn get_token(username: &str, password: &str) -> String {
|
async fn get_token(username: &str, password: &str) -> String {
|
||||||
|
get_token_for_client(
|
||||||
|
"stalwart",
|
||||||
|
"stalwart-secret",
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
"openid email profile",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_token_for_client(
|
||||||
|
client_id: &str,
|
||||||
|
client_secret: &str,
|
||||||
|
username: &str,
|
||||||
|
password: &str,
|
||||||
|
scope: &str,
|
||||||
|
) -> String {
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
let response = client
|
let response = client
|
||||||
.post("http://localhost:9080/realms/stalwart/protocol/openid-connect/token")
|
.post("http://localhost:9080/realms/stalwart/protocol/openid-connect/token")
|
||||||
.form(&[
|
.form(&[
|
||||||
("grant_type", "password"),
|
("grant_type", "password"),
|
||||||
("client_id", "stalwart"),
|
("client_id", client_id),
|
||||||
("client_secret", "stalwart-secret"),
|
("client_secret", client_secret),
|
||||||
("username", username),
|
("username", username),
|
||||||
("password", password),
|
("password", password),
|
||||||
("scope", "openid email profile"),
|
("scope", scope),
|
||||||
])
|
])
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
|
|||||||
Reference in New Issue
Block a user