Skip to content

Commit 30a933a

Browse files
goffrieConvex, Inc.
authored and
Convex, Inc.
committedApr 3, 2025·
Implement OpenID Connect custom claims using AdditionalClaims (#36167)
GitOrigin-RevId: 2ae1db008c82cc96c99959252b68c086cb08c222
1 parent 699f452 commit 30a933a

File tree

7 files changed

+89
-18
lines changed

7 files changed

+89
-18
lines changed
 

‎Cargo.lock

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎crates/authentication/src/lib.rs

+7-6
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,16 @@ use chrono::TimeZone;
1818
use common::auth::AuthInfo;
1919
use errors::ErrorMetadata;
2020
use futures::Future;
21-
use keybroker::UserIdentity;
21+
use keybroker::{
22+
CoreIdTokenWithCustomClaims,
23+
UserIdentity,
24+
};
2225
use oauth2::{
2326
HttpRequest,
2427
HttpResponse,
2528
};
2629
use openidconnect::{
2730
core::{
28-
CoreIdToken,
2931
CoreIdTokenVerifier,
3032
CoreProviderMetadata,
3133
},
@@ -108,10 +110,9 @@ where
108110
F: Future<Output = Result<HttpResponse, E>>,
109111
E: std::error::Error + 'static + Send + Sync,
110112
{
111-
let token = CoreIdToken::from_str(&token_str.0).context(ErrorMetadata::unauthenticated(
112-
"InvalidAuthHeader",
113-
"Could not parse as id token",
114-
))?;
113+
let token = CoreIdTokenWithCustomClaims::from_str(&token_str.0).context(
114+
ErrorMetadata::unauthenticated("InvalidAuthHeader", "Could not parse as id token"),
115+
)?;
115116
let (audiences, issuer) = {
116117
let verifier = CoreIdTokenVerifier::new_insecure_without_verification();
117118
let claims = match token.claims(&verifier, |_: Option<&openidconnect::Nonce>| Ok(())) {

‎crates/keybroker/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@ prost = { workspace = true }
2727
rand = { workspace = true }
2828
rsa = { workspace = true, optional = true }
2929
serde = { workspace = true }
30+
serde_json = { workspace = true }
3031
sodium_secretbox = { path = "../sodium_secretbox" }
3132
sync_types = { package = "convex_sync_types", path = "../convex/sync_types" }
3233
tracing = { workspace = true }
3334

3435
[dev-dependencies]
36+
base64 = { workspace = true }
3537
common = { path = "../common", features = ["testing"] }
3638
errors = { path = "../errors", features = ["testing"] }
3739
metrics = { path = "../metrics", features = ["testing"] }

‎crates/keybroker/src/broker.rs

+25-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
use core::panic;
22
use std::{
3-
collections::BTreeMap,
3+
collections::{
4+
BTreeMap,
5+
HashMap,
6+
},
47
fmt,
58
time::{
69
Duration,
@@ -43,9 +46,14 @@ use errors::ErrorMetadata;
4346
use metrics::StaticMetricLabel;
4447
use openidconnect::{
4548
core::{
46-
CoreIdToken,
49+
CoreGenderClaim,
4750
CoreIdTokenVerifier,
51+
CoreJsonWebKeyType,
52+
CoreJweContentEncryptionAlgorithm,
53+
CoreJwsSigningAlgorithm,
4854
},
55+
AdditionalClaims,
56+
IdToken,
4957
Nonce,
5058
};
5159
use pb::{
@@ -370,7 +378,7 @@ pub struct UserIdentity {
370378
pub expiration: SystemTime,
371379
pub attributes: UserIdentityAttributes,
372380
// The original token this user identity was created from.
373-
pub original_token: CoreIdToken,
381+
pub original_token: CoreIdTokenWithCustomClaims,
374382
}
375383

376384
#[cfg(any(test, feature = "testing"))]
@@ -422,9 +430,21 @@ macro_rules! get_localized_string {
422430
};
423431
}
424432

433+
pub type CoreIdTokenWithCustomClaims = IdToken<
434+
CustomClaims,
435+
CoreGenderClaim,
436+
CoreJweContentEncryptionAlgorithm,
437+
CoreJwsSigningAlgorithm,
438+
CoreJsonWebKeyType,
439+
>;
440+
441+
#[derive(Deserialize, Serialize, Debug, Clone, Default, PartialEq, Eq)]
442+
pub struct CustomClaims(HashMap<String, serde_json::Value>);
443+
impl AdditionalClaims for CustomClaims {}
444+
425445
impl UserIdentity {
426446
pub fn from_token(
427-
token: CoreIdToken,
447+
token: CoreIdTokenWithCustomClaims,
428448
verifier: CoreIdTokenVerifier,
429449
) -> Result<Self, anyhow::Error> {
430450
// NB: Nonce verification is optional, and we'd need the developer to create and
@@ -436,7 +456,7 @@ impl UserIdentity {
436456
let subject = claims.subject().to_string();
437457
let issuer = claims.issuer().to_string();
438458
let mut custom_claims = BTreeMap::new();
439-
for claim in claims.custom_claims() {
459+
for claim in &claims.additional_claims().0 {
440460
// Filter out standard claims and claims set by auth providers
441461
match claim.0.as_str() {
442462
// Standard claims that we support: see https://docs.convex.dev/api/interfaces/server.UserIdentity

‎crates/keybroker/src/lib.rs

+5
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,17 @@ mod secret;
99
#[cfg(any(test, feature = "testing"))]
1010
pub mod testing;
1111

12+
#[cfg(test)]
13+
mod tests;
14+
1215
pub use sync_types::UserIdentityAttributes;
1316

1417
pub use self::{
1518
broker::{
1619
AdminIdentity,
1720
AdminIdentityPrincipal,
21+
CoreIdTokenWithCustomClaims,
22+
CustomClaims,
1823
GetFileAuthorization,
1924
Identity,
2025
KeyBroker,

‎crates/keybroker/src/testing.rs

+11-7
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,14 @@ use chrono::{
66
};
77
use openidconnect::{
88
core::{
9-
CoreIdToken,
10-
CoreIdTokenClaims,
119
CoreIdTokenVerifier,
1210
CoreJwsSigningAlgorithm,
1311
CoreRsaPrivateSigningKey,
1412
},
1513
Audience,
16-
EmptyAdditionalClaims,
1714
EndUserEmail,
1815
EndUserName,
16+
IdTokenClaims,
1917
IssuerUrl,
2018
JsonWebKeyId,
2119
StandardClaims,
@@ -24,7 +22,13 @@ use openidconnect::{
2422
use rsa::pkcs1::EncodeRsaPrivateKey;
2523
use sync_types::UserIdentityAttributes;
2624

27-
use crate::UserIdentity;
25+
use crate::{
26+
broker::{
27+
CoreIdTokenWithCustomClaims,
28+
CustomClaims,
29+
},
30+
UserIdentity,
31+
};
2832

2933
pub static TEST_SIGNING_KEY: LazyLock<CoreRsaPrivateSigningKey> = LazyLock::new(|| {
3034
let key = rsa::RsaPrivateKey::new(&mut rsa::rand_core::OsRng, 2048).unwrap();
@@ -42,16 +46,16 @@ impl TestUserIdentity for UserIdentity {
4246
let issuer = "https://testauth.fake.domain".to_owned();
4347
let audience = Audience::new("client-id-123".to_string());
4448

45-
let token = CoreIdToken::new(
46-
CoreIdTokenClaims::new(
49+
let token = CoreIdTokenWithCustomClaims::new(
50+
IdTokenClaims::new(
4751
IssuerUrl::new(issuer).unwrap(),
4852
vec![audience],
4953
Utc::now() + Duration::seconds(600),
5054
Utc::now(),
5155
StandardClaims::new(SubjectIdentifier::new(subject))
5256
.set_email(Some(EndUserEmail::new("foo@bar.com".to_string())))
5357
.set_name(Some(EndUserName::new("Al Pastor".to_string()).into())),
54-
EmptyAdditionalClaims {},
58+
CustomClaims::default(),
5559
),
5660
&*TEST_SIGNING_KEY,
5761
CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256,

‎crates/keybroker/src/tests.rs

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
use std::str::FromStr;
2+
3+
use chrono::{
4+
TimeZone,
5+
Utc,
6+
};
7+
use openidconnect::core::CoreIdTokenVerifier;
8+
9+
use crate::{
10+
CoreIdTokenWithCustomClaims,
11+
UserIdentity,
12+
};
13+
14+
#[test]
15+
fn test_custom_claims() {
16+
let header_json = "{\"alg\":\"none\"}";
17+
let payload_json = "{
18+
\"iss\": \"https://server.example.com\",
19+
\"sub\": \"24400320\",
20+
\"aud\": [\"s6BhdRkqt3\"],
21+
\"exp\": 1311281970,
22+
\"iat\": 1311280970,
23+
\"tfa_method\": \"u2f\",
24+
\"bool_me\": true
25+
}";
26+
let signature = "foo";
27+
let token_str = [header_json, payload_json, signature]
28+
.map(|s| base64::encode_config(s, base64::URL_SAFE_NO_PAD))
29+
.join(".");
30+
let token = CoreIdTokenWithCustomClaims::from_str(&token_str).expect("failed to parse");
31+
let verifier = CoreIdTokenVerifier::new_insecure_without_verification()
32+
.set_time_fn(|| Utc.timestamp_opt(1311281969, 0).unwrap());
33+
let identity = UserIdentity::from_token(token, verifier).unwrap();
34+
let custom_claims = identity.attributes.custom_claims;
35+
assert_eq!(custom_claims.get("tfa_method").unwrap(), "\"u2f\"");
36+
assert_eq!(custom_claims.get("bool_me").unwrap(), "true");
37+
}

0 commit comments

Comments
 (0)
Please sign in to comment.