use slave_framework::oidc::{OidcConfig, OidcVerifier, VerifyError};
use slave_framework::testing::{TEST_CLIENT_ID, TestIdentityProvider, TokenSpec};
async fn verifier(idp: &TestIdentityProvider, audiences: &[&str]) -> OidcVerifier {
OidcVerifier::discover(&OidcConfig {
issuer: idp.issuer().to_owned(),
audiences: audiences.iter().map(|a| (*a).to_owned()).collect(),
})
.await
.expect("discover the test provider")
}
#[tokio::test]
async fn accepts_valid_tokens_and_reads_the_identity() {
let idp = TestIdentityProvider::start();
let verifier = verifier(&idp, &[TEST_CLIENT_ID]).await;
let identity = verifier
.verify(&idp.id_token("ada@example.com"))
.await
.unwrap();
assert_eq!(identity.issuer, idp.issuer());
assert_eq!(identity.subject, "test|ada@example.com");
assert_eq!(identity.email.as_deref(), Some("ada@example.com"));
assert_eq!(identity.email_verified, Some(true));
assert_eq!(identity.name.as_deref(), Some("ada"));
}
#[tokio::test]
async fn rejects_wrong_audience_expired_and_malformed_tokens() {
let idp = TestIdentityProvider::start();
let verifier = verifier(&idp, &[TEST_CLIENT_ID]).await;
let mut other_app = TokenSpec::for_email("ada@example.com");
other_app.audience = "some-other-app".into();
let mut expired = TokenSpec::for_email("ada@example.com");
expired.expires_in = -600;
for token in [
idp.id_token_with(&other_app),
idp.id_token_with(&expired),
"not-a-jwt".to_owned(),
String::new(),
] {
let err = verifier.verify(&token).await.unwrap_err();
assert!(matches!(err, VerifyError::Invalid(_)), "{err}");
}
}
#[tokio::test]
async fn rejects_tokens_from_another_provider() {
let trusted = TestIdentityProvider::start();
let stranger = TestIdentityProvider::start();
let verifier = verifier(&trusted, &[TEST_CLIENT_ID]).await;
let err = verifier
.verify(&stranger.id_token("mallory@example.com"))
.await
.unwrap_err();
assert!(matches!(err, VerifyError::Invalid(_)), "{err}");
}
#[tokio::test]
async fn accepts_every_configured_audience() {
let idp = TestIdentityProvider::start();
let verifier = verifier(&idp, &["dashboard", "storefront"]).await;
for audience in ["dashboard", "storefront"] {
let mut spec = TokenSpec::for_email("ada@example.com");
spec.audience = audience.into();
assert!(
verifier.verify(&idp.id_token_with(&spec)).await.is_ok(),
"{audience}"
);
}
assert!(
verifier
.verify(&idp.id_token("ada@example.com"))
.await
.is_err()
);
}
#[tokio::test]
async fn discovery_fails_clearly_for_unreachable_issuers() {
let err = OidcVerifier::discover(&OidcConfig {
issuer: "http://127.0.0.1:9".into(),
audiences: vec!["x".into()],
})
.await
.unwrap_err();
assert!(err.to_string().contains("discovery"), "{err}");
}
mod sign_in {
use std::collections::HashMap;
use openidconnect::reqwest;
use slave_framework::oidc::{OidcClient, OidcConfig, OidcVerifier, PendingSignIn};
use slave_framework::testing::{TEST_CLIENT_ID, TestIdentityProvider};
const REDIRECT: &str = "http://127.0.0.1:1/callback";
async fn approve(idp: &TestIdentityProvider, authorize_url: &str) -> (String, String) {
let url = reqwest::Url::parse(authorize_url).unwrap();
let mut form: HashMap<String, String> = url.query_pairs().into_owned().collect();
form.insert("email".into(), "ada@example.com".into());
form.insert("name".into(), "Ada".into());
let http = reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()
.unwrap();
let response = http
.post(format!("{}/authorize", idp.issuer()))
.form(&form)
.send()
.await
.unwrap();
let location = response.headers()["location"].to_str().unwrap().to_owned();
let back = reqwest::Url::parse(&location).unwrap();
assert!(location.starts_with(REDIRECT), "{location}");
let params: HashMap<String, String> = back.query_pairs().into_owned().collect();
(params["code"].clone(), params["state"].clone())
}
#[tokio::test]
async fn completes_the_authorization_code_flow_with_pkce() {
let idp = TestIdentityProvider::start();
let client = OidcClient::discover(idp.issuer(), TEST_CLIENT_ID, None)
.await
.unwrap();
let (url, pending) = client.authorize_url(REDIRECT).unwrap();
assert!(url.contains("code_challenge="), "{url}");
assert!(url.contains("code_challenge_method=S256"), "{url}");
assert_eq!(
PendingSignIn::decode(&pending.encode()),
Some(pending.clone())
);
let (code, state) = approve(&idp, &url).await;
let id_token = client
.exchange(REDIRECT, &code, &state, &pending)
.await
.unwrap();
let verifier = OidcVerifier::discover(&OidcConfig {
issuer: idp.issuer().to_owned(),
audiences: vec![TEST_CLIENT_ID.to_owned()],
})
.await
.unwrap();
let identity = verifier.verify(&id_token).await.unwrap();
assert_eq!(identity.email.as_deref(), Some("ada@example.com"));
assert_eq!(identity.name.as_deref(), Some("Ada"));
assert!(
client
.exchange(REDIRECT, &code, &state, &pending)
.await
.is_err()
);
}
#[tokio::test]
async fn rejects_a_wrong_state_or_pkce_verifier() {
let idp = TestIdentityProvider::start();
let client = OidcClient::discover(idp.issuer(), TEST_CLIENT_ID, None)
.await
.unwrap();
let (url, pending) = client.authorize_url(REDIRECT).unwrap();
let (code, _) = approve(&idp, &url).await;
let err = client
.exchange(REDIRECT, &code, "forged-state", &pending)
.await
.unwrap_err();
assert!(err.to_string().contains("state"), "{err}");
let (url, pending) = client.authorize_url(REDIRECT).unwrap();
let (code, state) = approve(&idp, &url).await;
let tampered = PendingSignIn {
pkce_verifier: "a".repeat(43),
..pending
};
assert!(
client
.exchange(REDIRECT, &code, &state, &tampered)
.await
.is_err()
);
}
}