Skip to content

Commit f8d2823

Browse files
committed
Make the id_token optional on upstream OAuth 2.0 providers
This makes it possible to use non-OIDC providers as upstream OAuth 2.0 providers, like GitHub.
1 parent 56edcb4 commit f8d2823

File tree

5 files changed

+82
-118
lines changed

5 files changed

+82
-118
lines changed

crates/cli/src/sync.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ pub async fn config_sync(
251251
}
252252

253253
if provider.jwks_uri.is_none() {
254-
error!("Provider has discovery disabled but no JWKS URI set");
254+
warn!("Provider has discovery disabled but no JWKS URI set");
255255
}
256256
}
257257

crates/handlers/src/upstream_oauth2/callback.rs

Lines changed: 70 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,9 @@ use axum_extra::response::Html;
1414
use hyper::StatusCode;
1515
use mas_axum_utils::{cookies::CookieJar, sentry::SentryEventID};
1616
use mas_data_model::{UpstreamOAuthProvider, UpstreamOAuthProviderResponseMode};
17+
use mas_jose::claims::TokenHash;
1718
use mas_keystore::{Encrypter, Keystore};
18-
use mas_oidc_client::requests::{
19-
authorization_code::AuthorizationValidationData, jose::JwtVerificationData,
20-
};
19+
use mas_oidc_client::requests::jose::JwtVerificationData;
2120
use mas_router::UrlBuilder;
2221
use mas_storage::{
2322
upstream_oauth2::{
@@ -27,7 +26,7 @@ use mas_storage::{
2726
BoxClock, BoxRepository, BoxRng, Clock,
2827
};
2928
use mas_templates::{FormPostContext, Templates};
30-
use oauth2_types::errors::ClientErrorCode;
29+
use oauth2_types::{errors::ClientErrorCode, requests::AccessTokenRequest};
3130
use serde::{Deserialize, Serialize};
3231
use serde_json::json;
3332
use thiserror::Error;
@@ -88,9 +87,6 @@ pub(crate) enum RouteError {
8887
#[error("State parameter mismatch")]
8988
StateMismatch,
9089

91-
#[error("Missing ID token")]
92-
MissingIDToken,
93-
9490
#[error("Could not extract subject from ID token")]
9591
ExtractSubject(#[source] minijinja::Error),
9692

@@ -125,7 +121,8 @@ impl_from_error_for_route!(mas_templates::TemplateError);
125121
impl_from_error_for_route!(mas_storage::RepositoryError);
126122
impl_from_error_for_route!(mas_oidc_client::error::DiscoveryError);
127123
impl_from_error_for_route!(mas_oidc_client::error::JwksError);
128-
impl_from_error_for_route!(mas_oidc_client::error::TokenAuthorizationCodeError);
124+
impl_from_error_for_route!(mas_oidc_client::error::TokenRequestError);
125+
impl_from_error_for_route!(mas_oidc_client::error::IdTokenError);
129126
impl_from_error_for_route!(mas_oidc_client::error::UserInfoError);
130127
impl_from_error_for_route!(super::ProviderCredentialsError);
131128
impl_from_error_for_route!(super::cookie::UpstreamSessionNotFound);
@@ -253,11 +250,6 @@ pub(crate) async fn handler(
253250

254251
let mut lazy_metadata = LazyProviderInfos::new(&metadata_cache, &provider, &client);
255252

256-
// Fetch the JWKS
257-
let jwks =
258-
mas_oidc_client::requests::jose::fetch_jwks(&client, lazy_metadata.jwks_uri().await?)
259-
.await?;
260-
261253
// Figure out the client credentials
262254
let client_credentials = client_credentials_for_provider(
263255
&provider,
@@ -268,41 +260,72 @@ pub(crate) async fn handler(
268260

269261
let redirect_uri = url_builder.upstream_oauth_callback(provider.id);
270262

271-
// TODO: all that should be borrowed
272-
let validation_data = AuthorizationValidationData {
273-
state: session.state_str.clone(),
274-
nonce: session.nonce.clone(),
275-
code_challenge_verifier: session.code_challenge_verifier.clone(),
276-
redirect_uri,
277-
};
278-
279-
let verification_data = JwtVerificationData {
280-
issuer: &provider.issuer,
281-
jwks: &jwks,
282-
// TODO: make that configurable
283-
signing_algorithm: &mas_iana::jose::JsonWebSignatureAlg::Rs256,
284-
client_id: &provider.client_id,
285-
};
263+
let token_response = mas_oidc_client::requests::token::request_access_token(
264+
&client,
265+
client_credentials,
266+
lazy_metadata.token_endpoint().await?,
267+
AccessTokenRequest::AuthorizationCode(oauth2_types::requests::AuthorizationCodeGrant {
268+
code: code.clone(),
269+
redirect_uri: Some(redirect_uri),
270+
code_verifier: session.code_challenge_verifier.clone(),
271+
}),
272+
clock.now(),
273+
&mut rng,
274+
)
275+
.await?;
276+
277+
let mut context = AttributeMappingContext::new();
278+
if let Some(id_token) = token_response.id_token.as_ref() {
279+
// Fetch the JWKS
280+
let jwks =
281+
mas_oidc_client::requests::jose::fetch_jwks(&client, lazy_metadata.jwks_uri().await?)
282+
.await?;
283+
284+
let verification_data = JwtVerificationData {
285+
issuer: &provider.issuer,
286+
jwks: &jwks,
287+
// TODO: make that configurable
288+
signing_algorithm: &mas_iana::jose::JsonWebSignatureAlg::Rs256,
289+
client_id: &provider.client_id,
290+
};
286291

287-
let (response, id_token_map) =
288-
mas_oidc_client::requests::authorization_code::access_token_with_authorization_code(
289-
&client,
290-
client_credentials,
291-
lazy_metadata.token_endpoint().await?,
292-
code,
293-
validation_data,
294-
Some(verification_data),
292+
// Decode and verify the ID token
293+
let id_token = mas_oidc_client::requests::jose::verify_id_token(
294+
id_token,
295+
verification_data,
296+
None,
295297
clock.now(),
296-
&mut rng,
297-
)
298-
.await?;
298+
)?;
299+
300+
let (_headers, mut claims) = id_token.into_parts();
301+
302+
// Access token hash must match.
303+
mas_jose::claims::AT_HASH
304+
.extract_optional_with_options(
305+
&mut claims,
306+
TokenHash::new(
307+
verification_data.signing_algorithm,
308+
&token_response.access_token,
309+
),
310+
)
311+
.map_err(mas_oidc_client::error::IdTokenError::from)?;
299312

300-
let (_header, id_token) = id_token_map
301-
.clone()
302-
.ok_or(RouteError::MissingIDToken)?
303-
.into_parts();
313+
// Code hash must match.
314+
mas_jose::claims::C_HASH
315+
.extract_optional_with_options(
316+
&mut claims,
317+
TokenHash::new(verification_data.signing_algorithm, &code),
318+
)
319+
.map_err(mas_oidc_client::error::IdTokenError::from)?;
320+
321+
// Nonce must match.
322+
mas_jose::claims::NONCE
323+
.extract_required_with_options(&mut claims, session.nonce.as_str())
324+
.map_err(mas_oidc_client::error::IdTokenError::from)?;
325+
326+
context = context.with_id_token_claims(claims);
327+
}
304328

305-
let mut context = AttributeMappingContext::new().with_id_token_claims(id_token);
306329
if let Some(extra_callback_parameters) = extra_callback_parameters.clone() {
307330
context = context.with_extra_callback_parameters(extra_callback_parameters);
308331
}
@@ -312,9 +335,8 @@ pub(crate) async fn handler(
312335
mas_oidc_client::requests::userinfo::fetch_userinfo(
313336
&client,
314337
lazy_metadata.userinfo_endpoint().await?,
315-
response.access_token.as_str(),
316-
Some(verification_data),
317-
&id_token_map.ok_or(RouteError::MissingIDToken)?,
338+
token_response.access_token.as_str(),
339+
None,
318340
)
319341
.await?
320342
))
@@ -364,7 +386,7 @@ pub(crate) async fn handler(
364386
&clock,
365387
session,
366388
&link,
367-
response.id_token,
389+
token_response.id_token,
368390
extra_callback_parameters,
369391
userinfo,
370392
)

crates/oidc-client/src/requests/token.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
//! Requests for the Token endpoint.
88
99
use chrono::{DateTime, Utc};
10+
use http::header::ACCEPT;
1011
use mas_http::RequestBuilderExt;
12+
use mime::APPLICATION_JSON;
1113
use oauth2_types::requests::{AccessTokenRequest, AccessTokenResponse};
1214
use rand::Rng;
1315
use url::Url;
@@ -48,7 +50,9 @@ pub async fn request_access_token(
4850
) -> Result<AccessTokenResponse, TokenRequestError> {
4951
tracing::debug!(?request, "Requesting access token...");
5052

51-
let token_request = http_client.post(token_endpoint.as_str());
53+
let token_request = http_client
54+
.post(token_endpoint.as_str())
55+
.header(ACCEPT, APPLICATION_JSON.as_ref());
5256

5357
let token_response = client_credentials
5458
.authenticated_form(token_request, &request, now, rng)?

crates/oidc-client/src/requests/userinfo.rs

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ use std::collections::HashMap;
1313
use headers::{ContentType, HeaderMapExt, HeaderValue};
1414
use http::header::ACCEPT;
1515
use mas_http::RequestBuilderExt;
16-
use mas_jose::claims;
1716
use mime::Mime;
1817
use serde_json::Value;
1918
use url::Url;
@@ -22,7 +21,6 @@ use super::jose::JwtVerificationData;
2221
use crate::{
2322
error::{IdTokenError, ResponseExt, UserInfoError},
2423
requests::jose::verify_signed_jwt,
25-
types::IdToken,
2624
};
2725

2826
/// Obtain information about an authenticated end-user.
@@ -59,7 +57,6 @@ pub async fn fetch_userinfo(
5957
userinfo_endpoint: &Url,
6058
access_token: &str,
6159
jwt_verification_data: Option<JwtVerificationData<'_>>,
62-
auth_id_token: &IdToken<'_>,
6360
) -> Result<HashMap<String, Value>, UserInfoError> {
6461
tracing::debug!("Obtaining user info…");
6562

@@ -94,7 +91,7 @@ pub async fn fetch_userinfo(
9491
});
9592
}
9693

97-
let mut claims = if let Some(verification_data) = jwt_verification_data {
94+
let claims = if let Some(verification_data) = jwt_verification_data {
9895
let response_body = userinfo_response.text().await?;
9996
verify_signed_jwt(&response_body, verification_data)
10097
.map_err(IdTokenError::from)?
@@ -104,18 +101,5 @@ pub async fn fetch_userinfo(
104101
userinfo_response.json().await?
105102
};
106103

107-
let mut auth_claims = auth_id_token.payload().clone();
108-
109-
// Subject identifier must always be the same.
110-
let sub = claims::SUB
111-
.extract_required(&mut claims)
112-
.map_err(IdTokenError::from)?;
113-
let auth_sub = claims::SUB
114-
.extract_required(&mut auth_claims)
115-
.map_err(IdTokenError::from)?;
116-
if sub != auth_sub {
117-
return Err(IdTokenError::WrongSubjectIdentifier.into());
118-
}
119-
120104
Ok(claims)
121105
}

crates/oidc-client/tests/it/requests/userinfo.rs

Lines changed: 5 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,19 @@
44
// SPDX-License-Identifier: AGPL-3.0-only
55
// Please see LICENSE in the repository root for full details.
66

7-
use assert_matches::assert_matches;
8-
use mas_oidc_client::{
9-
error::{IdTokenError, UserInfoError},
10-
requests::userinfo::fetch_userinfo,
11-
};
7+
use mas_oidc_client::requests::userinfo::fetch_userinfo;
128
use serde_json::json;
139
use wiremock::{
1410
matchers::{header, method, path},
1511
Mock, ResponseTemplate,
1612
};
1713

18-
use crate::{id_token, init_test, ACCESS_TOKEN, SUBJECT_IDENTIFIER};
14+
use crate::{init_test, ACCESS_TOKEN, SUBJECT_IDENTIFIER};
1915

2016
#[tokio::test]
2117
async fn pass_fetch_userinfo() {
2218
let (http_client, mock_server, issuer) = init_test().await;
2319
let userinfo_endpoint = issuer.join("userinfo").unwrap();
24-
let (auth_id_token, _) = id_token(issuer.as_str());
2520

2621
Mock::given(method("GET"))
2722
.and(path("/userinfo"))
@@ -36,50 +31,9 @@ async fn pass_fetch_userinfo() {
3631
.mount(&mock_server)
3732
.await;
3833

39-
let claims = fetch_userinfo(
40-
&http_client,
41-
&userinfo_endpoint,
42-
ACCESS_TOKEN,
43-
None,
44-
&auth_id_token,
45-
)
46-
.await
47-
.unwrap();
34+
let claims = fetch_userinfo(&http_client, &userinfo_endpoint, ACCESS_TOKEN, None)
35+
.await
36+
.unwrap();
4837

4938
assert_eq!(claims.get("email").unwrap(), "[email protected]");
5039
}
51-
52-
#[tokio::test]
53-
async fn fail_wrong_subject_identifier() {
54-
let (http_client, mock_server, issuer) = init_test().await;
55-
let userinfo_endpoint = issuer.join("userinfo").unwrap();
56-
let (auth_id_token, _) = id_token(issuer.as_str());
57-
58-
Mock::given(method("GET"))
59-
.and(path("/userinfo"))
60-
.and(header(
61-
"authorization",
62-
format!("Bearer {ACCESS_TOKEN}").as_str(),
63-
))
64-
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
65-
"sub": "wrong_subject_identifier",
66-
"email": "[email protected]",
67-
})))
68-
.mount(&mock_server)
69-
.await;
70-
71-
let error = fetch_userinfo(
72-
&http_client,
73-
&userinfo_endpoint,
74-
ACCESS_TOKEN,
75-
None,
76-
&auth_id_token,
77-
)
78-
.await
79-
.unwrap_err();
80-
81-
assert_matches!(
82-
error,
83-
UserInfoError::IdToken(IdTokenError::WrongSubjectIdentifier)
84-
);
85-
}

0 commit comments

Comments
 (0)