/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ use crate::{ imap::{ ImapConnection, Type, pop::{self, Pop3Connection}, }, jmap::{JMAPTest, ManagementApi, mail::delivery::SmtpConnection}, }; use base64::{Engine, engine::general_purpose}; use biscuit::{JWT, SingleOrMultiple, jwk::JWKSet}; use bytes::Bytes; use common::auth::oauth::{ introspect::OAuthIntrospect, oidc::StandardClaims, registration::{ClientRegistrationRequest, ClientRegistrationResponse}, }; use http::auth::oauth::{ DeviceAuthResponse, ErrorType, OAuthCodeRequest, TokenResponse, auth::OAuthMetadata, openid::OpenIdMetadata, }; use imap_proto::ResponseType; use jmap_client::{ client::{Client, Credentials}, mailbox::query::Filter, }; use serde::{Serialize, de::DeserializeOwned}; use std::time::{Duration, Instant}; use store::ahash::AHashMap; #[derive(serde::Deserialize, Debug)] #[allow(dead_code)] struct OAuthCodeResponse { pub code: String, #[serde(rename = "isEnterprise")] pub is_enterprise: bool, } pub async fn test(params: &mut JMAPTest) { println!("Running OAuth tests..."); // Create test account let server = params.server.clone(); let account = params.account("jdoe@example.com"); // Build API let api = ManagementApi::new(8899, "jdoe@example.com", "12345"); // Obtain OAuth metadata let metadata: OAuthMetadata = get("https://127.0.0.1:8899/.well-known/oauth-authorization-server").await; 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; // Register client let registration: ClientRegistrationResponse = post_json( &metadata.registration_endpoint, None, &ClientRegistrationRequest { redirect_uris: vec!["https://localhost".to_string()], ..Default::default() }, ) .await; let client_id = registration.client_id; /*println!("OAuth metadata: {:#?}", metadata); println!("OpenID metadata: {:#?}", oidc_metadata); println!("JWKSet: {:#?}", jwk_set);*/ // ------------------------ // Authorization code flow // ------------------------ // Authenticate with the correct password let response = api .post::( "/api/oauth", &OAuthCodeRequest::Code { client_id: client_id.to_string(), redirect_uri: "https://localhost".to_string().into(), nonce: "abc1234".to_string().into(), }, ) .await .unwrap() .unwrap_data(); // Both client_id and redirect_uri have to match let mut token_params = AHashMap::from_iter([ ("client_id".to_string(), "invalid_client".to_string()), ("redirect_uri".to_string(), "https://localhost".to_string()), ("grant_type".to_string(), "authorization_code".to_string()), ("code".to_string(), response.code), ]); assert_eq!( post::(&metadata.token_endpoint, &token_params).await, TokenResponse::Error { error: ErrorType::InvalidClient } ); token_params.insert("client_id".to_string(), client_id.to_string()); token_params.insert( "redirect_uri".to_string(), "https://some-other.url".to_string(), ); assert_eq!( post::(&metadata.token_endpoint, &token_params).await, TokenResponse::Error { error: ErrorType::InvalidClient } ); // Obtain token token_params.insert("redirect_uri".to_string(), "https://localhost".to_string()); 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() .credentials(Credentials::bearer(&token)) .accept_invalid_certs(true) .follow_redirects(["127.0.0.1"]) .connect("https://127.0.0.1:8899") .await .unwrap(); assert_eq!(john_client.default_account_id(), account.id_string()); assert!( !john_client .mailbox_query(None::, None::>) .await .unwrap() .ids() .is_empty() ); // Verify ID token using the JWK set let id_token = JWT::::new_encoded(&id_token) .decode_with_jwks(&jwk_set, None) .unwrap(); 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(account.id().document_id().to_string()) ); assert_eq!( registered_claims.audience, Some(SingleOrMultiple::Single(client_id.to_string())) ); assert_eq!(private_claims.nonce, Some("abc1234".into())); assert_eq!( private_claims.preferred_username, Some("jdoe@example.com".into()) ); assert_eq!(private_claims.email, Some("jdoe@example.com".into())); // Introspect token let access_introspect: OAuthIntrospect = post_with_auth::( &metadata.introspection_endpoint, token.as_str().into(), &AHashMap::from_iter([("token".to_string(), token.to_string())]), ) .await; assert_eq!(access_introspect.username.unwrap(), "jdoe@example.com"); assert_eq!(access_introspect.token_type.unwrap(), "bearer"); assert_eq!(access_introspect.client_id.unwrap(), client_id); assert!(access_introspect.active); let refresh_introspect = post_with_auth::( &metadata.introspection_endpoint, token.as_str().into(), &AHashMap::from_iter([("token".to_string(), refresh_token.unwrap())]), ) .await; assert_eq!(refresh_introspect.username.unwrap(), "jdoe@example.com"); assert_eq!(refresh_introspect.client_id.unwrap(), client_id); assert!(refresh_introspect.active); assert_eq!( refresh_introspect.iat.unwrap(), access_introspect.iat.unwrap() ); // Try SMTP OAUTHBEARER auth let oauth_bearer_invalid_sasl = general_purpose::STANDARD.encode(format!( "n,a={},\u{1}auth=Bearer {}\u{1}\u{1}", "user@domain", "invalid_token" )); let oauth_bearer_sasl = general_purpose::STANDARD.encode(format!( "n,a={},\u{1}auth=Bearer {}\u{1}\u{1}", "user@domain", token )); let mut smtp = SmtpConnection::connect().await; smtp.send(&format!("AUTH OAUTHBEARER {oauth_bearer_invalid_sasl}",)) .await; smtp.read(1, 4).await; smtp.send(&format!("AUTH OAUTHBEARER {oauth_bearer_sasl}",)) .await; smtp.read(1, 2).await; // Try IMAP OAUTHBEARER auth let mut imap = ImapConnection::connect(b"_x ").await; imap.assert_read(Type::Untagged, ResponseType::Ok).await; imap.send(&format!("AUTHENTICATE OAUTHBEARER {oauth_bearer_sasl}")) .await; imap.assert_read(Type::Tagged, ResponseType::Ok).await; // Try POP3 OAUTHBEARER auth let mut pop3 = Pop3Connection::connect().await; pop3.assert_read(pop::ResponseType::Ok).await; pop3.send(&format!("AUTH OAUTHBEARER {oauth_bearer_sasl}")) .await; pop3.assert_read(pop::ResponseType::Ok).await; // ------------------------ // Device code flow // ------------------------ // Request a device code let device_code_params = AHashMap::from_iter([("client_id".to_string(), client_id.to_string())]); let device_response: DeviceAuthResponse = post(&metadata.device_authorization_endpoint, &device_code_params).await; //println!("Device response: {:#?}", device_response); // Status should be pending let mut token_params = AHashMap::from_iter([ ("client_id".to_string(), client_id.to_string()), ( "grant_type".to_string(), "urn:ietf:params:oauth:grant-type:device_code".to_string(), ), ( "device_code".to_string(), device_response.device_code.to_string(), ), ]); assert_eq!( post::(&metadata.token_endpoint, &token_params).await, TokenResponse::Error { error: ErrorType::AuthorizationPending } ); // Let the code expire and make sure it's invalidated tokio::time::sleep(Duration::from_secs(1)).await; assert!( !api.post::( "/api/oauth", &OAuthCodeRequest::Device { code: device_response.user_code.clone(), }, ) .await .unwrap() .unwrap_data(), "Code should be expired" ); assert_eq!( post::(&metadata.token_endpoint, &token_params).await, TokenResponse::Error { error: ErrorType::ExpiredToken } ); // Authenticate account using a valid code let device_response: DeviceAuthResponse = post(&metadata.device_authorization_endpoint, &device_code_params).await; token_params.insert( "device_code".to_string(), device_response.device_code.to_string(), ); assert!( api.post::( "/api/oauth", &OAuthCodeRequest::Device { code: device_response.user_code.clone(), }, ) .await .unwrap() .unwrap_data(), "Code is invalid" ); // Obtain token let time_first_token = Instant::now(); let (token, refresh_token, _) = unwrap_token_response(post(&metadata.token_endpoint, &token_params).await); let refresh_token = refresh_token.unwrap(); // Authorization codes can only be used once assert_eq!( post::(&metadata.token_endpoint, &token_params).await, TokenResponse::Error { error: ErrorType::ExpiredToken } ); // Connect to account using token and attempt to search let john_client = Client::new() .credentials(Credentials::bearer(&token)) .accept_invalid_certs(true) .follow_redirects(["127.0.0.1"]) .connect("https://127.0.0.1:8899") .await .unwrap(); assert_eq!(john_client.default_account_id(), account.id_string()); assert!( !john_client .mailbox_query(None::, None::>) .await .unwrap() .ids() .is_empty() ); // Connecting using the refresh token should not work assert_unauthorized("https://127.0.0.1:8899", &refresh_token).await; // Refreshing a token using the access token should not work assert_eq!( post::( &metadata.token_endpoint, &AHashMap::from_iter([ ("client_id".to_string(), client_id.to_string()), ("grant_type".to_string(), "refresh_token".to_string()), ("refresh_token".to_string(), token), ]), ) .await, TokenResponse::Error { error: ErrorType::InvalidGrant } ); // Refreshing the access token before expiration should not include a new refresh token let refresh_params = AHashMap::from_iter([ ("client_id".to_string(), client_id.to_string()), ("grant_type".to_string(), "refresh_token".to_string()), ("refresh_token".to_string(), refresh_token), ]); let time_before_post: Instant = Instant::now(); let (token, new_refresh_token, _) = unwrap_token_response(post(&metadata.token_endpoint, &refresh_params).await); assert_eq!( new_refresh_token, None, "Refreshed token in {:?}, since start {:?}", time_before_post.elapsed(), time_first_token.elapsed() ); // Wait 1 second and make sure the access token expired tokio::time::sleep(Duration::from_secs(1)).await; assert_unauthorized("https://127.0.0.1:8899", &token).await; // Wait another second for the refresh token to be about to expire // and expect a new refresh token tokio::time::sleep(Duration::from_secs(1)).await; let (_, new_refresh_token, _) = unwrap_token_response(post(&metadata.token_endpoint, &refresh_params).await); //println!("New refresh token: {:?}", new_refresh_token); assert_ne!(new_refresh_token, None); // Wait another second and make sure the refresh token expired tokio::time::sleep(Duration::from_secs(1)).await; assert_eq!( post::(&metadata.token_endpoint, &refresh_params).await, TokenResponse::Error { error: ErrorType::InvalidGrant } ); // Destroy test accounts server .core .storage .lookup .purge_in_memory_store() .await .unwrap(); params.destroy_all_mailboxes(account).await; params.assert_is_empty().await; } async fn post_bytes( url: &str, auth_token: Option<&str>, params: &AHashMap, ) -> Bytes { let mut client = reqwest::Client::builder() .timeout(Duration::from_millis(500)) .danger_accept_invalid_certs(true) .build() .unwrap_or_default() .post(url); if let Some(auth_token) = auth_token { client = client.bearer_auth(auth_token); } client .form(params) .send() .await .unwrap() .bytes() .await .unwrap() } async fn post_json( url: &str, auth_token: Option<&str>, body: &impl Serialize, ) -> D { let mut client = reqwest::Client::builder() .timeout(Duration::from_millis(500)) .danger_accept_invalid_certs(true) .build() .unwrap_or_default() .post(url); if let Some(auth_token) = auth_token { client = client.bearer_auth(auth_token); } serde_json::from_slice( &client .body(serde_json::to_string(body).unwrap().into_bytes()) .send() .await .unwrap() .bytes() .await .unwrap(), ) .unwrap() } async fn post(url: &str, params: &AHashMap) -> T { post_with_auth(url, None, params).await } async fn post_with_auth( url: &str, auth_token: Option<&str>, params: &AHashMap, ) -> T { serde_json::from_slice(&post_bytes(url, auth_token, params).await).unwrap() } async fn get_bytes(url: &str) -> Bytes { reqwest::Client::builder() .timeout(Duration::from_millis(500)) .danger_accept_invalid_certs(true) .build() .unwrap_or_default() .get(url) .send() .await .unwrap() .bytes() .await .unwrap() } async fn get(url: &str) -> T { serde_json::from_slice(&get_bytes(url).await).unwrap() } async fn assert_unauthorized(base_url: &str, token: &str) { match Client::new() .credentials(Credentials::bearer(token)) .accept_invalid_certs(true) .follow_redirects(["127.0.0.1"]) .connect(base_url) .await { Ok(_) => panic!("Expected unauthorized access."), Err(err) => { let err = err.to_string(); assert!(err.contains("Unauthorized"), "{}", err); } } } fn unwrap_token_response(response: TokenResponse) -> (String, Option, u64) { match response { TokenResponse::Granted(granted) => { assert_eq!(granted.token_type, "bearer"); ( granted.access_token, granted.refresh_token, granted.expires_in, ) } TokenResponse::Error { error } => panic!("Expected granted, got {:?}", error), } } fn unwrap_oidc_token_response(response: TokenResponse) -> (String, Option, 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), } }