Include nonce parameter in id_tokens
Some checks failed
trivy / Check (push) Has been cancelled

This commit is contained in:
mdecimus 2024-10-03 15:07:38 +02:00
parent cce2d9c915
commit 292d1cc048
5 changed files with 57 additions and 28 deletions

View file

@ -78,12 +78,20 @@ pub struct Userinfo {
pub updated_at: Option<i64>,
}
#[derive(Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct Nonce {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub nonce: Option<String>,
}
impl Server {
pub fn issue_id_token(
&self,
subject: impl Into<String>,
issuer: impl Into<String>,
audience: impl Into<String>,
nonce: Option<impl Into<String>>,
) -> trc::Result<String> {
let now = now() as i64;
@ -93,7 +101,7 @@ impl Server {
key_id: Some("default".into()),
..Default::default()
}),
ClaimsSet::<()> {
ClaimsSet::<Nonce> {
registered: RegisteredClaims {
issuer: Some(issuer.into()),
subject: Some(subject.into()),
@ -103,7 +111,9 @@ impl Server {
expiry: Some((now + self.core.oauth.oidc_expiry_id_token as i64).into()),
..Default::default()
},
private: (),
private: Nonce {
nonce: nonce.map(Into::into),
},
},
)
.into_encoded(&self.core.oauth.oidc_signing_secret)

View file

@ -262,7 +262,8 @@ impl OAuthApiHandler for Server {
user_code,
expires_in: self.core.oauth.oauth_expiry_user_code,
interval: 5,
}).no_cache()
})
.no_cache()
.into_http_response())
}

View file

@ -41,6 +41,7 @@ pub trait TokenHandler: Sync + Send {
account_id: u32,
client_id: &str,
issuer: String,
nonce: Option<String>,
with_refresh_token: bool,
) -> impl Future<Output = trc::Result<OAuthResponse>> + Send;
}
@ -63,10 +64,11 @@ impl TokenHandler for Server {
.await;
if grant_type.eq_ignore_ascii_case("authorization_code") {
response = if let (Some(code), Some(client_id), Some(redirect_uri)) = (
response = if let (Some(code), Some(client_id), Some(redirect_uri), nonce) = (
params.get("code"),
params.get("client_id"),
params.get("redirect_uri"),
params.get("nonce"),
) {
// Obtain code
match self
@ -100,15 +102,21 @@ impl TokenHandler for Server {
.await?;
// Issue token
self.issue_token(oauth.account_id, &oauth.client_id, issuer, true)
.await
.map(TokenResponse::Granted)
.map_err(|err| {
trc::AuthEvent::Error
.into_err()
.details(err)
.caused_by(trc::location!())
})?
self.issue_token(
oauth.account_id,
&oauth.client_id,
issuer,
nonce.map(Into::into),
true,
)
.await
.map(TokenResponse::Granted)
.map_err(|err| {
trc::AuthEvent::Error
.into_err()
.details(err)
.caused_by(trc::location!())
})?
}
} else {
TokenResponse::error(ErrorType::InvalidGrant)
@ -122,9 +130,11 @@ impl TokenHandler for Server {
} else if grant_type.eq_ignore_ascii_case("urn:ietf:params:oauth:grant-type:device_code") {
response = TokenResponse::error(ErrorType::ExpiredToken);
if let (Some(device_code), Some(client_id)) =
(params.get("device_code"), params.get("client_id"))
{
if let (Some(device_code), Some(client_id), nonce) = (
params.get("device_code"),
params.get("client_id"),
params.get("nonce"),
) {
// Obtain code
if let Some(auth_code) = self
.core
@ -157,6 +167,7 @@ impl TokenHandler for Server {
oauth.account_id,
&oauth.client_id,
issuer,
nonce.map(Into::into),
true,
)
.await
@ -190,6 +201,7 @@ impl TokenHandler for Server {
token_info.account_id,
&token_info.client_id,
issuer,
None,
token_info.expires_in
<= self.core.oauth.oauth_expiry_refresh_token_renew,
)
@ -251,6 +263,7 @@ impl TokenHandler for Server {
account_id: u32,
client_id: &str,
issuer: String,
nonce: Option<String>,
with_refresh_token: bool,
) -> trc::Result<OAuthResponse> {
Ok(OAuthResponse {
@ -276,7 +289,7 @@ impl TokenHandler for Server {
} else {
None
},
id_token: match self.issue_id_token(account_id.to_string(), issuer, client_id) {
id_token: match self.issue_id_token(account_id.to_string(), issuer, client_id, nonce) {
Ok(id_token) => Some(id_token),
Err(err) => {
trc::error!(err);

View file

@ -11,6 +11,7 @@ use biscuit::{jwk::JWKSet, SingleOrMultiple, JWT};
use bytes::Bytes;
use common::auth::oauth::{
introspect::OAuthIntrospect,
oidc::Nonce,
registration::{ClientRegistrationRequest, ClientRegistrationResponse},
};
use imap_proto::ResponseType;
@ -135,6 +136,7 @@ pub async fn test(params: &mut JMAPTest) {
// Obtain token
token_params.insert("redirect_uri".to_string(), "https://localhost".to_string());
token_params.insert("nonce".to_string(), "abc1234".to_string());
let (token, refresh_token, id_token) =
unwrap_oidc_token_response(post(&metadata.token_endpoint, &token_params).await);
@ -154,16 +156,19 @@ pub async fn test(params: &mut JMAPTest) {
.is_empty());
// Verify ID token using the JWK set
let id_token = JWT::<(), biscuit::Empty>::new_encoded(&id_token)
let id_token = JWT::<Nonce, biscuit::Empty>::new_encoded(&id_token)
.decode_with_jwks(&jwk_set, None)
.unwrap();
let claims = &id_token.payload().unwrap().registered;
assert_eq!(claims.issuer, Some(oidc_metadata.issuer));
assert_eq!(claims.subject, Some(john_int_id.to_string()));
let claims = id_token.payload().unwrap();
let registered_claims = &claims.registered;
let private_claims = &claims.private;
assert_eq!(registered_claims.issuer, Some(oidc_metadata.issuer));
assert_eq!(registered_claims.subject, Some(john_int_id.to_string()));
assert_eq!(
claims.audience,
registered_claims.audience,
Some(SingleOrMultiple::Single(client_id.to_string()))
);
assert_eq!(private_claims.nonce, Some("abc1234".to_string()));
// Introspect token
let access_introspect: OAuthIntrospect = post_with_auth::<OAuthIntrospect>(

View file

@ -370,8 +370,8 @@ pub async fn jmap_tests() {
)
.await;
webhooks::test(&mut params).await;
/*email_query::test(&mut params, delete).await;
/*webhooks::test(&mut params).await;
email_query::test(&mut params, delete).await;
email_get::test(&mut params).await;
email_set::test(&mut params).await;
email_parse::test(&mut params).await;
@ -384,9 +384,9 @@ pub async fn jmap_tests() {
mailbox::test(&mut params).await;
delivery::test(&mut params).await;
auth_acl::test(&mut params).await;
auth_limits::test(&mut params).await;
auth_limits::test(&mut params).await;*/
auth_oauth::test(&mut params).await;
event_source::test(&mut params).await;
/*event_source::test(&mut params).await;
push_subscription::test(&mut params).await;
sieve_script::test(&mut params).await;
vacation_response::test(&mut params).await;
@ -394,10 +394,10 @@ pub async fn jmap_tests() {
websocket::test(&mut params).await;
quota::test(&mut params).await;
crypto::test(&mut params).await;
blob::test(&mut params).await;*/
blob::test(&mut params).await;
permissions::test(&params).await;
purge::test(&mut params).await;
enterprise::test(&mut params).await;
enterprise::test(&mut params).await;*/
if delete {
params.temp_dir.delete();