OpenID Connect implementation (closes #298)

This commit is contained in:
mdecimus
2024-09-30 16:57:34 +02:00
parent 1fed40a926
commit 6a5f963b43
18 changed files with 899 additions and 149 deletions

View File

@@ -61,6 +61,7 @@ num_cpus = "1.15.0"
async-trait = "0.1.68"
chrono = "0.4"
ring = { version = "0.17" }
biscuit = "0.7.0"
[target.'cfg(not(target_env = "msvc"))'.dependencies]
jemallocator = "0.5.0"

View File

@@ -7,11 +7,13 @@
use std::time::{Duration, Instant};
use base64::{engine::general_purpose, Engine};
use biscuit::{jwk::JWKSet, SingleOrMultiple, JWT};
use bytes::Bytes;
use common::auth::oauth::introspect::OAuthIntrospect;
use imap_proto::ResponseType;
use jmap::auth::oauth::{
DeviceAuthResponse, ErrorType, OAuthCodeRequest, OAuthMetadata, TokenResponse,
auth::OAuthMetadata, openid::OpenIdMetadata, DeviceAuthResponse, ErrorType, OAuthCodeRequest,
TokenResponse,
};
use jmap_client::{
client::{Client, Credentials},
@@ -47,20 +49,18 @@ pub async fn test(params: &mut JMAPTest) {
// Create test account
let server = params.server.clone();
let john_id = Id::from(
server
.core
.storage
.data
.create_test_user(
"jdoe@example.com",
"12345",
"John Doe",
&["jdoe@example.com"],
)
.await,
)
.to_string();
let john_int_id = server
.core
.storage
.data
.create_test_user(
"jdoe@example.com",
"12345",
"John Doe",
&["jdoe@example.com"],
)
.await;
let john_id = Id::from(john_int_id).to_string();
// Build API
let api = ManagementApi::new(8899, "jdoe@example.com", "12345");
@@ -68,7 +68,13 @@ pub async fn test(params: &mut JMAPTest) {
// Obtain OAuth metadata
let metadata: OAuthMetadata =
get("https://127.0.0.1:8899/.well-known/oauth-authorization-server").await;
//println!("OAuth metadata: {:#?}", metadata);
let oidc_metadata: OpenIdMetadata =
get("https://127.0.0.1:8899/.well-known/openid-configuration").await;
let jwk_set: JWKSet<()> = get(&oidc_metadata.jwks_uri).await;
/*println!("OAuth metadata: {:#?}", metadata);
println!("OpenID metadata: {:#?}", oidc_metadata);
println!("JWKSet: {:#?}", jwk_set);*/
// ------------------------
// Authorization code flow
@@ -114,8 +120,8 @@ pub async fn test(params: &mut JMAPTest) {
// Obtain token
token_params.insert("redirect_uri".to_string(), "https://localhost".to_string());
let (token, refresh_token, _) =
unwrap_token_response(post(&metadata.token_endpoint, &token_params).await);
let (token, refresh_token, id_token) =
unwrap_oidc_token_response(post(&metadata.token_endpoint, &token_params).await);
// Connect to account using token and attempt to search
let john_client = Client::new()
@@ -132,6 +138,18 @@ pub async fn test(params: &mut JMAPTest) {
.ids()
.is_empty());
// Verify ID token using the JWK set
let id_token = JWT::<(), 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()));
assert_eq!(
claims.audience,
Some(SingleOrMultiple::Single("OAuthyMcOAuthFace".to_string()))
);
// Introspect token
let access_introspect: OAuthIntrospect = post_with_auth::<OAuthIntrospect>(
&metadata.introspection_endpoint,
@@ -441,3 +459,17 @@ fn unwrap_token_response(response: TokenResponse) -> (String, Option<String>, u6
TokenResponse::Error { error } => panic!("Expected granted, got {:?}", error),
}
}
fn unwrap_oidc_token_response(response: TokenResponse) -> (String, Option<String>, String) {
match response {
TokenResponse::Granted(granted) => {
assert_eq!(granted.token_type, "bearer");
(
granted.access_token,
granted.refresh_token,
granted.id_token.unwrap(),
)
}
TokenResponse::Error { error } => panic!("Expected granted, got {:?}", error),
}
}

View File

@@ -289,6 +289,47 @@ token = "1s"
refresh-token = "3s"
refresh-token-renew = "2s"
[oauth.oidc]
signature-key = '''-----BEGIN PRIVATE KEY-----
MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQDMXJI1bL3z8gaF
Ze/6493VjL+jHkFMP2Pc7fLwRF1fhkuIdYTp69LabzrSEJCRCz0UI2NHqPOgtOta
+zRHKAMr7c7Z6uKO0K+aXiQYHw4Y70uSG8CnmNl7kb4OM/CAcoO6fePmvBsyESfn
TmkJ5bfHEZQFDQEAoDlDjtjxuwYsAQQVQXuAydi8j8pyTWKAJ1RDgnUT+HbOub7j
JrQ7sPe6MPCjXv5N76v9RMHKktfYwRNMlkLkxImQU55+vlvghNztgFlIlJDFfNiy
UQPV5FTEZJli9BzMoj1JQK3sZyV8WV0W1zN41QQ+glAAC6+K7iTDPRMINBSwbHyn
6Lb9Q6U7AgMBAAECggEAB93qZ5xrhYgEFeoyKO4mUdGsu4qZyJB0zNeWGgdaXCfZ
zC4l8zFM+R6osix0EY6lXRtC95+6h9hfFQNa5FWseupDzmIQiEnim1EowjWef87l
Eayi0nDRB8TjqZKjR/aLOUhzrPlXHKrKEUk/RDkacCiDklwz9S0LIfLOSXlByBDM
/n/eczfX2gUATexMHSeIXs8vN2jpuiVv0r+FPXcRvqdzDZnYSzS8BJ9k6RYXVQ4o
NzCbfqgFIpVryB7nHgSTrNX9G7299If8/dXmesXWSFEJvvDSSpcBoINKbfgSlrxd
6ubjiotcEIBUSlbaanRrydwShhLHnXyupNAb7tlvyQKBgQDsIipSK4+H9FGl1rAk
Gg9DLJ7P/94sidhoq1KYnj/CxwGLoRq22khZEUYZkSvYXDu1Qkj9Avi3TRhw8uol
l2SK1VylL5FQvTLKhWB7b2hjrUd5llMRgS3/NIdLhOgDMB7w3UxJnCA/df/Rj+dM
WhkyS1f0x3t7XPLwWGurW0nJcwKBgQDdjhrNfabrK7OQvDpAvNJizuwZK9WUL7CD
rR0V0MpDGYW12BTEOY6tUK6XZgiRitAXf4EkEI6R0Q0bFzwDDLrg7TvGdTuzNeg/
8vm8IlRlOkrdihtHZI4uRB7Ytmz24vzywEBE0p6enA7v4oniscUks/KKmDGr0V90
yT9gIVrjGQKBgQCjnWC5otlHGLDiOgm+WhgtMWOxN9dYAQNkMyF+Alinu4CEoVKD
VGhA3sk1ufMpbW8pvw4X0dFIITFIQeift3DBCemxw23rBc2FqjkaDi3EszINO22/
eUTHyjvcxfCFFPi7aHsNnhJyJm7lY9Kegudmg/Ij93zGE7d5darVBuHvpQKBgBBY
YovUgFMLR1UfPeD2zUKy52I4BKrJFemxBNtOKw3mPSIcTfPoFymcMTVENs+eARoq
svlZK1uAo8ni3e+Pqd3cQrOyhHQFPxwwrdH+amGJemp7vOV4erDZH7l3Q/S27Fhw
bI1nSIKFGukBupB58wRxLiyha9C0QqmYC0/pRg5JAn8Rbj5tP26oVCXjZEfWJL8J
axxSxsGA4Vol6i6LYnVgZG+1ez2rP8vUORo1lRzmdeP4o1BSJf9TPwXkuppE5J+t
UZVKtYGlEn1RqwGNd8I9TiWvU84rcY9nsxlDR86xwKRWFvYqVOiGYtzRyewYRdjU
rTs9aqB3v1+OVxGxR6Na
-----END PRIVATE KEY-----
'''
signature-algorithm = "RS256"
[oauth.oidc-ignore]
signature-key = '''-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQggybcqc86ulFFiOon
WiYrLO4z8/kmkqvA7wGElBok9IqhRANCAAQxZK68FnQtHC0eyh8CA05xRIvxhVHn
0ymka6XBh9aFtW4wfeoKhTkSKjHc/zjh9Rr2dr3kvmYe80fMGhW4ycGA
-----END PRIVATE KEY-----
'''
signature-algorithm = "ES256"
[session.extensions]
expn = true
vrfy = true