#![allow(clippy::expect_fun_call)]
extern crate env_logger;
extern crate http;
#[macro_use]
extern crate log;
extern crate openidconnect;
#[macro_use]
extern crate pretty_assertions;
extern crate reqwest_ as reqwest;
extern crate url;
use std::collections::HashMap;
use http::header::LOCATION;
use http::method::Method;
use reqwest::{blocking::Client, redirect::Policy};
use url::Url;
use openidconnect::core::{
CoreClient, CoreClientAuthMethod, CoreClientRegistrationRequest,
CoreClientRegistrationResponse, CoreIdToken, CoreIdTokenClaims, CoreIdTokenVerifier,
CoreJsonWebKeySet, CoreJwsSigningAlgorithm, CoreProviderMetadata, CoreResponseType,
CoreUserInfoClaims,
};
use openidconnect::Nonce;
use openidconnect::{
AccessToken, AuthType, AuthenticationFlow, AuthorizationCode, ClaimsVerificationError,
CsrfToken, OAuth2TokenResponse, RequestTokenError, Scope, SignatureVerificationError,
UserInfoError,
};
#[macro_use]
mod rp_common;
use rp_common::{
get_provider_metadata, http_client, init_log, issuer_url, register_client, PanicIfFail,
};
struct TestState {
access_token: Option<AccessToken>,
authorization_code: Option<AuthorizationCode>,
client: CoreClient,
id_token: Option<CoreIdToken>,
nonce: Option<Nonce>,
provider_metadata: CoreProviderMetadata,
registration_response: CoreClientRegistrationResponse,
}
impl TestState {
pub fn init<F>(test_id: &'static str, reg_request_fn: F) -> Self
where
F: FnOnce(CoreClientRegistrationRequest) -> CoreClientRegistrationRequest,
{
init_log(test_id);
let _issuer_url = issuer_url(test_id);
let provider_metadata = get_provider_metadata(test_id);
let registration_response = register_client(&provider_metadata, reg_request_fn);
let redirect_uri = registration_response.redirect_uris()[0].clone();
let client: CoreClient = CoreClient::from_provider_metadata(
provider_metadata.clone(),
registration_response.client_id().to_owned(),
registration_response.client_secret().cloned(),
)
.set_redirect_uri(redirect_uri);
TestState {
access_token: None,
authorization_code: None,
client,
id_token: None,
nonce: None,
provider_metadata,
registration_response,
}
}
pub fn access_token(&self) -> &AccessToken {
self.access_token.as_ref().expect("no access_token")
}
pub fn authorize(mut self, scopes: &[Scope]) -> Self {
let (authorization_code, nonce) = {
let mut authorization_request = self.client.authorize_url(
AuthenticationFlow::AuthorizationCode::<CoreResponseType>,
CsrfToken::new_random,
Nonce::new_random,
);
authorization_request =
scopes
.iter()
.fold(authorization_request, |mut authorization_request, scope| {
authorization_request = authorization_request.add_scope(scope.clone());
authorization_request
});
let (url, state, nonce) = authorization_request.url();
log_debug!("Authorize URL: {:?}", url);
let http_client = Client::builder().redirect(Policy::none()).build().unwrap();
let redirect_response = http_client
.execute(
http_client
.request(Method::GET, url.as_str())
.build()
.unwrap(),
)
.unwrap();
assert!(redirect_response.status().is_redirection());
let redirected_url = Url::parse(
redirect_response
.headers()
.get(LOCATION)
.unwrap()
.to_str()
.unwrap(),
)
.unwrap();
log_debug!("Authorization Server redirected to: {:?}", redirected_url);
let mut query_params = HashMap::new();
redirected_url.query_pairs().for_each(|(key, value)| {
query_params.insert(key, value);
});
log_debug!(
"Authorization Server returned query params: {:?}",
query_params
);
assert_eq!(
self.provider_metadata.issuer().as_str(),
query_params.get("iss").unwrap()
);
assert_eq!(state.secret(), query_params.get("state").unwrap());
log_info!("Successfully received authentication response from Authorization Server");
let authorization_code =
AuthorizationCode::new(query_params.get("code").unwrap().to_string());
log_debug!(
"Authorization Server returned authorization code: {}",
authorization_code.secret()
);
(authorization_code, nonce)
};
self.authorization_code = Some(authorization_code);
self.nonce = Some(nonce);
self
}
pub fn exchange_code(mut self) -> Self {
let token_response = self
.client
.exchange_code(
self.authorization_code
.take()
.expect("no authorization_code"),
)
.request(http_client)
.panic_if_fail("failed to exchange authorization code for token");
log_debug!(
"Authorization Server returned token response: {:?}",
token_response
);
self.access_token = Some(token_response.access_token().clone());
let id_token = (*token_response
.extra_fields()
.id_token()
.expect("no id_token"))
.clone();
self.id_token = Some(id_token);
self
}
pub fn id_token(&self) -> &CoreIdToken {
self.id_token.as_ref().expect("no id_token")
}
pub fn id_token_verifier(&self, jwks: CoreJsonWebKeySet) -> CoreIdTokenVerifier {
CoreIdTokenVerifier::new_confidential_client(
self.registration_response.client_id().clone(),
self.registration_response
.client_secret()
.expect("no client_secret")
.clone(),
self.provider_metadata.issuer().clone(),
jwks,
)
}
pub fn id_token_claims(&self) -> &CoreIdTokenClaims {
let verifier = self.id_token_verifier(self.jwks());
self.id_token()
.claims(&verifier, self.nonce.as_ref().expect("no nonce"))
.panic_if_fail("failed to validate claims")
}
pub fn id_token_claims_failure(&self) -> ClaimsVerificationError {
let verifier = self.id_token_verifier(self.jwks());
self.id_token()
.claims(&verifier, self.nonce.as_ref().expect("no nonce"))
.expect_err("claims verification succeeded but was expected to fail")
}
pub fn jwks(&self) -> CoreJsonWebKeySet {
CoreJsonWebKeySet::fetch(self.provider_metadata.jwks_uri(), http_client)
.panic_if_fail("failed to fetch JWK set")
}
pub fn set_auth_type(mut self, auth_type: AuthType) -> Self {
self.client = self.client.set_auth_type(auth_type);
self
}
pub fn user_info_claims(&self) -> CoreUserInfoClaims {
self.client
.user_info(
self.access_token().to_owned(),
Some(self.id_token_claims().subject().clone()),
)
.unwrap()
.require_signed_response(false)
.request(http_client)
.panic_if_fail("failed to get UserInfo")
}
pub fn user_info_claims_failure(
&self,
) -> UserInfoError<openidconnect::reqwest::HttpClientError> {
let user_info_result: Result<CoreUserInfoClaims, _> = self
.client
.user_info(
self.access_token().to_owned(),
Some(self.id_token_claims().subject().clone()),
)
.unwrap()
.require_signed_response(false)
.request(http_client);
match user_info_result {
Err(err) => err,
_ => panic!("claims verification succeeded but was expected to fail"),
}
}
}
#[test]
#[ignore]
fn rp_response_type_code() {
let test_state = TestState::init("rp-response_type-code", |reg| reg).authorize(&[]);
assert!(
test_state
.authorization_code
.expect("no authorization_code")
.secret()
!= ""
);
log_info!("SUCCESS");
}
#[test]
#[ignore]
fn rp_scope_userinfo_claims() {
let user_info_scopes = vec!["profile", "email", "address", "phone"]
.iter()
.map(|scope| Scope::new((*scope).to_string()))
.collect::<Vec<_>>();
let test_state = TestState::init("rp-scope-userinfo-claims", |reg| reg)
.authorize(&user_info_scopes)
.exchange_code();
let id_token_claims = test_state.id_token_claims();
log_debug!("ID token: {:?}", id_token_claims);
let user_info_claims = test_state.user_info_claims();
log_debug!("UserInfo response: {:?}", user_info_claims);
assert_eq!(id_token_claims.subject(), user_info_claims.subject());
assert!(!user_info_claims
.email()
.expect("no email returned by UserInfo endpoint")
.is_empty());
assert!(!user_info_claims
.address()
.expect("no address returned by UserInfo endpoint")
.street_address
.as_ref()
.expect("no street address returned by UserInfo endpoint")
.is_empty());
assert!(!user_info_claims
.phone_number()
.expect("no phone_number returned by UserInfo endpoint")
.is_empty());
log_info!("SUCCESS");
}
#[test]
#[ignore]
fn rp_nonce_invalid() {
let test_state = TestState::init("rp-nonce-invalid", |reg| reg)
.authorize(&[])
.exchange_code();
match test_state.id_token_claims_failure() {
ClaimsVerificationError::InvalidNonce(_) => {
log_error!("ID token contains invalid nonce (expected result)")
}
other => panic!("Unexpected result verifying ID token claims: {:?}", other),
}
log_info!("SUCCESS");
}
#[test]
#[ignore]
fn rp_token_endpoint_client_secret_basic() {
let test_state = TestState::init("rp-token_endpoint-client_secret_basic", |reg| {
reg.set_token_endpoint_auth_method(Some(CoreClientAuthMethod::ClientSecretBasic))
})
.set_auth_type(AuthType::BasicAuth)
.authorize(&[])
.exchange_code();
let id_token_claims = test_state.id_token_claims();
log_debug!("ID token: {:?}", id_token_claims);
log_info!("SUCCESS");
}
#[test]
#[ignore]
fn rp_token_endpoint_client_secret_post() {
let test_state = TestState::init("rp-token_endpoint-client_secret_post", |reg| {
reg.set_token_endpoint_auth_method(Some(CoreClientAuthMethod::ClientSecretPost))
})
.set_auth_type(AuthType::RequestBody)
.authorize(&[])
.exchange_code();
let id_token_claims = test_state.id_token_claims();
log_debug!("ID token: {:?}", id_token_claims);
log_info!("SUCCESS");
}
#[test]
#[ignore]
fn rp_id_token_kid_absent_single_jwks() {
let test_state = TestState::init("rp-id_token-kid-absent-single-jwks", |reg| reg)
.authorize(&[])
.exchange_code();
let id_token_claims = test_state.id_token_claims();
log_debug!("ID token: {:?}", id_token_claims);
log_info!("SUCCESS");
}
#[test]
#[ignore]
fn rp_id_token_iat() {
let mut test_state = TestState::init("rp-id_token-iat", |reg| reg).authorize(&[]);
let token_response = test_state
.client
.exchange_code(
test_state
.authorization_code
.take()
.expect("no authorization_code"),
)
.request(http_client);
match token_response {
Err(RequestTokenError::Parse(_, _)) => {
log_error!("ID token failed to parse without `iat` claim (expected result)")
}
other => panic!("Unexpected result verifying ID token claims: {:?}", other),
}
log_info!("SUCCESS");
}
#[test]
#[ignore]
fn rp_id_token_aud() {
let test_state = TestState::init("rp-id_token-aud", |reg| reg)
.authorize(&[])
.exchange_code();
match test_state.id_token_claims_failure() {
ClaimsVerificationError::InvalidAudience(_) => {
log_error!("ID token has invalid audience (expected result)")
}
other => panic!("Unexpected result verifying ID token claims: {:?}", other),
}
log_info!("SUCCESS");
}
#[test]
#[ignore]
fn rp_id_token_kid_absent_multiple_jwks() {
let test_state = TestState::init("rp-id_token-kid-absent-multiple-jwks", |reg| reg)
.authorize(&[])
.exchange_code();
match test_state.id_token_claims_failure() {
ClaimsVerificationError::SignatureVerification(
SignatureVerificationError::AmbiguousKeyId(_),
) => log_error!("ID token has ambiguous key identification without KID (expected result)"),
other => panic!("Unexpected result verifying ID token claims: {:?}", other),
}
log_info!("SUCCESS");
}
#[test]
#[ignore]
fn rp_id_token_sig_none() {
let test_state = TestState::init("rp-id_token-sig-none", |reg| reg)
.authorize(&[])
.exchange_code();
let verifier = test_state
.id_token_verifier(test_state.jwks())
.insecure_disable_signature_check();
let id_token_claims = test_state
.id_token()
.claims(&verifier, test_state.nonce.as_ref().expect("no nonce"))
.panic_if_fail("failed to validate claims");
log_debug!("ID token: {:?}", id_token_claims);
log_info!("SUCCESS");
}
#[test]
#[ignore]
fn rp_id_token_sig_rs256() {
let test_state = TestState::init("rp-id_token-sig-rs256", |reg| reg)
.authorize(&[])
.exchange_code();
let id_token_claims = test_state.id_token_claims();
log_debug!("ID token: {:?}", id_token_claims);
log_info!("SUCCESS");
}
#[test]
#[ignore]
fn rp_id_token_sig_hs256() {
let test_state = TestState::init("rp-id_token-sig-hs256", |reg| reg)
.authorize(&[])
.exchange_code();
let verifier = test_state
.id_token_verifier(test_state.jwks())
.set_allowed_algs(vec![CoreJwsSigningAlgorithm::HmacSha256]);
let id_token_claims = test_state
.id_token()
.claims(&verifier, test_state.nonce.as_ref().expect("no nonce"))
.panic_if_fail("failed to validate claims");
log_debug!("ID token: {:?}", id_token_claims);
log_info!("SUCCESS");
}
#[test]
#[ignore]
fn rp_id_token_sub() {
let mut test_state = TestState::init("rp-id_token-sub", |reg| reg).authorize(&[]);
let token_response = test_state
.client
.exchange_code(
test_state
.authorization_code
.take()
.expect("no authorization_code"),
)
.request(http_client);
match token_response {
Err(RequestTokenError::Parse(_, _)) => {
log_error!("ID token failed to parse without `sub` claim (expected result)")
}
other => panic!("Unexpected result verifying ID token claims: {:?}", other),
}
log_info!("SUCCESS");
}
#[test]
#[ignore]
fn rp_id_token_bad_sig_rs256() {
let test_state = TestState::init("rp-id_token-bad-sig-rs256", |reg| reg)
.authorize(&[])
.exchange_code();
match test_state.id_token_claims_failure() {
ClaimsVerificationError::SignatureVerification(
SignatureVerificationError::CryptoError(_),
) => log_error!("ID token has invalid signature (expected result)"),
other => panic!("Unexpected result verifying ID token claims: {:?}", other),
}
log_info!("SUCCESS");
}
#[test]
#[ignore]
fn rp_id_token_bad_sig_hs256() {
let test_state = TestState::init("rp-id_token-bad-sig-hs256", |reg| reg)
.authorize(&[])
.exchange_code();
let verifier = test_state
.id_token_verifier(test_state.jwks())
.set_allowed_algs(vec![CoreJwsSigningAlgorithm::HmacSha256]);
let id_token_err = test_state
.id_token()
.claims(&verifier, test_state.nonce.as_ref().expect("no nonce"))
.expect_err("claims verification succeeded but was expected to fail");
match id_token_err {
ClaimsVerificationError::SignatureVerification(
SignatureVerificationError::CryptoError(_),
) => log_error!("ID token has invalid signature (expected result)"),
other => panic!("Unexpected result verifying ID token claims: {:?}", other),
}
log_info!("SUCCESS");
}
#[test]
#[ignore]
fn rp_id_token_issuer_mismatch() {
let test_state = TestState::init("rp-id_token-issuer-mismatch", |reg| reg)
.authorize(&[])
.exchange_code();
match test_state.id_token_claims_failure() {
ClaimsVerificationError::InvalidIssuer(_) => {
log_error!("ID token has invalid issuer (expected result)")
}
other => panic!("Unexpected result verifying ID token claims: {:?}", other),
}
log_info!("SUCCESS");
}
#[test]
#[ignore]
fn rp_userinfo_bad_sub_claim() {
let test_state = TestState::init("rp-userinfo-bad-sub-claim", |reg| reg)
.authorize(&[Scope::new("profile".to_string())])
.exchange_code();
let id_token_claims = test_state.id_token_claims();
log_debug!("ID token: {:?}", id_token_claims);
match test_state.user_info_claims_failure() {
UserInfoError::ClaimsVerification(ClaimsVerificationError::InvalidSubject(_)) => {
log_error!("UserInfo response has invalid subject (expected result)")
}
other => panic!("Unexpected result verifying ID token claims: {:?}", other),
}
log_info!("SUCCESS");
}
#[test]
#[ignore]
fn rp_userinfo_bearer_header() {
let test_state = TestState::init("rp-userinfo-bearer-header", |reg| reg)
.authorize(&[Scope::new("profile".to_string())])
.exchange_code();
let id_token_claims = test_state.id_token_claims();
log_debug!("ID token: {:?}", id_token_claims);
let user_info_claims = test_state.user_info_claims();
log_debug!("UserInfo response: {:?}", user_info_claims);
log_info!("SUCCESS");
}
#[test]
#[ignore]
fn rp_userinfo_sig() {
let test_state = TestState::init("rp-userinfo-sig", |reg| {
reg.set_userinfo_signed_response_alg(Some(CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256))
})
.authorize(&[Scope::new("profile".to_string())])
.exchange_code();
let id_token_claims = test_state.id_token_claims();
log_debug!("ID token: {:?}", id_token_claims);
let user_info_claims: CoreUserInfoClaims = test_state
.client
.user_info(
test_state.access_token().to_owned(),
Some(id_token_claims.subject().clone()),
)
.unwrap()
.require_audience_match(false)
.require_issuer_match(false)
.request(http_client)
.panic_if_fail("failed to get UserInfo");
log_debug!("UserInfo response: {:?}", user_info_claims);
log_info!("SUCCESS");
}