// Copyright 2022 Kévin Commaille. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use std::collections::HashMap; use chrono::{DateTime, Duration, Utc}; use mas_iana::{jose::JsonWebSignatureAlg, oauth::OAuthClientAuthenticationMethod}; use mas_jose::{ claims::{self, hash_token}, constraints::Constrainable, jwk::PublicJsonWebKeySet, jwt::{JsonWebSignatureHeader, Jwt}, }; use mas_keystore::{JsonWebKey, JsonWebKeySet, Keystore, PrivateKey}; use mas_oidc_client::{ http_service::{hyper::hyper_service, HttpService}, types::{ client_credentials::{ClientCredentials, JwtSigningFn, JwtSigningMethod}, IdToken, }, }; use rand::{ distributions::{Alphanumeric, DistString}, SeedableRng, }; use url::Url; use wiremock::MockServer; mod requests; mod types; const REDIRECT_URI: &str = "http://localhost/"; const CLIENT_ID: &str = "client!+ID"; const CLIENT_SECRET: &str = "SECRET?%Gclient"; const REQUEST_URI: &str = "REQUESTur1"; const AUTHORIZATION_CODE: &str = "authC0D3"; const CODE_VERIFIER: &str = "cODEv3R1f1ER"; const NONCE: &str = "No0o0o0once"; const ACCESS_TOKEN: &str = "AccessToken1"; const REFRESH_TOKEN: &str = "RefreshToken1"; const SUBJECT_IDENTIFIER: &str = "SubjectID"; const ID_TOKEN_SIGNING_ALG: JsonWebSignatureAlg = JsonWebSignatureAlg::Rs256; fn now() -> DateTime { #[allow(clippy::disallowed_methods)] Utc::now() } async fn init_test() -> (HttpService, MockServer, Url) { let http_service = hyper_service(); let mock_server = MockServer::start().await; let issuer = Url::parse(&mock_server.uri()).expect("Couldn't parse URL"); (http_service, mock_server, issuer) } /// Generate a keystore with a single key for the given algorithm. fn keystore(alg: &JsonWebSignatureAlg) -> Keystore { let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(42); let private_key = match alg { JsonWebSignatureAlg::Rs256 | JsonWebSignatureAlg::Rs384 | JsonWebSignatureAlg::Rs512 | JsonWebSignatureAlg::Ps256 | JsonWebSignatureAlg::Ps384 | JsonWebSignatureAlg::Ps512 => PrivateKey::generate_rsa(&mut rng).unwrap(), JsonWebSignatureAlg::Es256 => PrivateKey::generate_ec_p256(&mut rng), JsonWebSignatureAlg::Es384 => PrivateKey::generate_ec_p384(&mut rng), _ => unimplemented!(), }; let jwk = JsonWebKey::new(private_key).with_kid(Alphanumeric.sample_string(&mut rng, 10)); Keystore::new(JsonWebKeySet::new(vec![jwk])) } /// Generate an ID token. fn id_token(issuer: &str) -> (IdToken, PublicJsonWebKeySet) { let signing_alg = ID_TOKEN_SIGNING_ALG; let keystore = keystore(&signing_alg); let mut claims = HashMap::new(); let now = now(); claims::ISS.insert(&mut claims, issuer.to_string()).unwrap(); claims::SUB .insert(&mut claims, SUBJECT_IDENTIFIER.to_owned()) .unwrap(); claims::AUD .insert(&mut claims, CLIENT_ID.to_owned()) .unwrap(); claims::NONCE.insert(&mut claims, NONCE.to_owned()).unwrap(); claims::IAT.insert(&mut claims, now).unwrap(); claims::EXP .insert(&mut claims, now + Duration::hours(1)) .unwrap(); claims::AT_HASH .insert(&mut claims, hash_token(&signing_alg, ACCESS_TOKEN).unwrap()) .unwrap(); claims::C_HASH .insert( &mut claims, hash_token(&signing_alg, AUTHORIZATION_CODE).unwrap(), ) .unwrap(); let key = keystore.signing_key_for_algorithm(&signing_alg).unwrap(); let signer = key.params().signing_key_for_alg(&signing_alg).unwrap(); let header = JsonWebSignatureHeader::new(signing_alg).with_kid(key.kid().unwrap()); let id_token = Jwt::sign(header, claims, &signer).unwrap(); (id_token, keystore.public_jwks()) } /// Generate client credentials for the given authentication method. fn client_credentials( auth_method: OAuthClientAuthenticationMethod, issuer: &Url, custom_signing: Option>, ) -> ClientCredentials { match auth_method { OAuthClientAuthenticationMethod::None => ClientCredentials::None { client_id: CLIENT_ID.to_owned(), }, OAuthClientAuthenticationMethod::ClientSecretPost => ClientCredentials::ClientSecretPost { client_id: CLIENT_ID.to_owned(), client_secret: CLIENT_SECRET.to_owned(), }, OAuthClientAuthenticationMethod::ClientSecretBasic => { ClientCredentials::ClientSecretBasic { client_id: CLIENT_ID.to_owned(), client_secret: CLIENT_SECRET.to_owned(), } } OAuthClientAuthenticationMethod::ClientSecretJwt => ClientCredentials::ClientSecretJwt { client_id: CLIENT_ID.to_owned(), client_secret: CLIENT_SECRET.to_owned(), signing_algorithm: JsonWebSignatureAlg::Hs256, token_endpoint: issuer.join("token").unwrap(), }, OAuthClientAuthenticationMethod::PrivateKeyJwt => { let signing_algorithm = JsonWebSignatureAlg::Es256; let jwt_signing_method = if let Some(signing_fn) = custom_signing { JwtSigningMethod::with_custom_signing_method(signing_fn) } else { JwtSigningMethod::with_keystore(keystore(&signing_algorithm)) }; ClientCredentials::PrivateKeyJwt { client_id: CLIENT_ID.to_owned(), jwt_signing_method, signing_algorithm, token_endpoint: issuer.join("token").unwrap(), } } _ => unimplemented!(), } }