use std::sync::Arc;
use base64::Engine as _;
use ppoppo_token::id_token::{IssueConfig, IssueRequest, ScopeSet};
use ppoppo_token::{Jwks, SigningKey};
use serde::Serialize;
use serde_json::json;
use url::Url;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
pub use crate::oauth::TokenResponse;
pub use crate::oidc::verifier::PasIdTokenVerifier;
const TEST_PUBLIC_KEY_SPKI_B64: &str =
"MCowBQYDK2VwAyEAh//e6j3It3xhjghg8Kpn2pM0jMCH/cvemGu4vv7D1Q4=";
pub struct FakePasServer {
server: MockServer,
signing_key: Arc<SigningKey>,
issuer: Url,
}
impl FakePasServer {
pub async fn start() -> Self {
let server: MockServer = MockServer::start().await;
let issuer: Url = format!("{}/", server.uri())
.parse()
.expect("wiremock URL parses as Url");
let (signing_key, _internal_keyset) = SigningKey::test_pair();
let spki = base64::engine::general_purpose::STANDARD
.decode(TEST_PUBLIC_KEY_SPKI_B64)
.expect("test SPKI base64 decodes");
let pk_bytes: [u8; 32] = spki[12..]
.try_into()
.expect("SPKI carries 32-byte raw pubkey at offset 12");
let kid = signing_key.kid().to_owned();
let jwks: Jwks = Jwks::from_ed25519_keys(&[(kid.as_str(), &pk_bytes)]);
Mock::given(method("GET"))
.and(path("/.well-known/openid-configuration"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"issuer": issuer.as_str().trim_end_matches('/'),
"authorization_endpoint": format!("{issuer}oauth/authorize"),
"token_endpoint": format!("{issuer}oauth/token"),
"jwks_uri": format!("{issuer}.well-known/jwks.json"),
"response_types_supported": ["code"],
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": ["EdDSA"],
})))
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/.well-known/jwks.json"))
.respond_with(ResponseTemplate::new(200).set_body_json(&jwks))
.mount(&server)
.await;
Self {
server,
signing_key: Arc::new(signing_key),
issuer,
}
}
#[must_use]
pub fn issuer_url(&self) -> Url {
self.issuer.clone()
}
#[must_use]
pub fn token_url(&self) -> Url {
let mut u = self.issuer.clone();
u.set_path("/oauth/token");
u
}
#[must_use]
pub fn jwks_url(&self) -> Url {
let mut u = self.issuer.clone();
u.set_path("/.well-known/jwks.json");
u
}
pub fn sign_id_token<S: ScopeSet>(
&self,
request: &IssueRequest<S>,
config: &IssueConfig,
) -> Result<String, ppoppo_token::id_token::IssueError> {
let now = time::OffsetDateTime::now_utc().unix_timestamp();
ppoppo_token::id_token::issue(request, config, &self.signing_key, now)
}
pub async fn expect_token_exchange(&self, body: TokenExchangeBody) {
Mock::given(method("POST"))
.and(path("/oauth/token"))
.respond_with(ResponseTemplate::new(200).set_body_json(&body))
.expect(1)
.mount(&self.server)
.await;
}
pub async fn reject_next_token_exchange(&self, status: u16, body: impl Into<String>) {
let body_owned = body.into();
Mock::given(method("POST"))
.and(path("/oauth/token"))
.respond_with(ResponseTemplate::new(status).set_body_string(body_owned))
.expect(1)
.mount(&self.server)
.await;
}
}
#[derive(Debug, Clone, Serialize)]
#[non_exhaustive]
pub struct TokenExchangeBody {
pub access_token: String,
pub token_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_in: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub refresh_token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id_token: Option<String>,
}
impl TokenExchangeBody {
#[must_use]
pub fn bearer(access_token: impl Into<String>) -> Self {
Self {
access_token: access_token.into(),
token_type: "Bearer".to_owned(),
expires_in: Some(3600),
refresh_token: None,
id_token: None,
}
}
#[must_use]
pub fn with_refresh_token(mut self, rt: impl Into<String>) -> Self {
self.refresh_token = Some(rt.into());
self
}
#[must_use]
pub fn with_id_token(mut self, id_token: impl Into<String>) -> Self {
self.id_token = Some(id_token.into());
self
}
#[must_use]
pub fn with_expires_in(mut self, secs: u64) -> Self {
self.expires_in = Some(secs);
self
}
}