use std::any::Any;
use std::fmt;
use std::sync::Arc;
use async_trait::async_trait;
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use pubky_common::{
auth::{
grant::GrantClaims,
grant_session_responses::{GrantSessionInfo, GrantSessionResponse},
jws::{POP_JWS_TYP, PopNonce},
pop::PopProofClaims,
},
crypto::{Keypair, PublicKey},
};
use reqwest::{Method, RequestBuilder, StatusCode};
use tokio::sync::Mutex;
use super::{
grant_exchange::{credential_from_grant_exchange, post_grant_session},
pop_signer::{DelegatedSignFn, GrantPopSigner},
shared_session::GrantSessionCoordinator,
};
use crate::actors::session::core::PubkySession;
use crate::actors::session::credential::{SessionCredential, credential_session_missing};
use crate::{
PubkyHttpClient,
actors::session::SessionInfo,
cross_log,
errors::{AuthError, Error, RequestError, Result},
};
pub(crate) const REFRESH_SLACK_SECS: u64 = 300;
const GRANT_SESSION_PATH: &str = "/auth/grant/session";
const STORED_GRANT_CREDENTIAL_PREFIX: &str = "pubky-grant-credential-v1";
const STORED_GRANT_CREDENTIAL_PREFIX_FAMILY: &str = "pubky-grant-credential-";
pub(crate) fn now_unix() -> u64 {
web_time::SystemTime::now()
.duration_since(web_time::UNIX_EPOCH)
.map(|d| d.as_secs())
.expect("System time duration_since should always valid")
}
#[derive(Debug)]
pub(crate) struct GrantCredentialState {
pub bearer: String,
pub grant_jws: String,
pub grant_claims: GrantClaims,
pub client_signer: GrantPopSigner,
pub homeserver_pk: PublicKey,
pub session: GrantSessionInfo,
pub coordinator: Option<Arc<dyn GrantSessionCoordinator>>,
}
impl GrantCredentialState {
pub(super) fn needs_refresh(&self, now: u64, slack: u64) -> bool {
self.session.token_expires_at <= now
|| (self.session.token_expires_at < self.grant_claims.exp
&& self.session.token_expires_at.saturating_sub(slack) <= now)
}
}
#[derive(Clone, Debug)]
pub struct GrantCredential {
pub(crate) state: Arc<Mutex<GrantCredentialState>>,
pub(crate) info: SessionInfo,
}
#[derive(Clone, PartialEq, Eq)]
struct StoredGrantCredential {
grant_jws: String,
client_key_secret: [u8; 32],
homeserver_pk: PublicKey,
}
#[derive(Clone, PartialEq, Eq)]
pub struct DelegatedGrantCredentialState {
pub grant_jws: String,
pub homeserver_pk: PublicKey,
pub key_id: String,
pub client_pk: PublicKey,
}
impl fmt::Debug for DelegatedGrantCredentialState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DelegatedGrantCredentialState")
.field("grant_jws", &"<redacted>")
.field("homeserver_pk", &self.homeserver_pk)
.field("key_id", &self.key_id)
.field("client_pk", &self.client_pk)
.finish()
}
}
impl fmt::Debug for StoredGrantCredential {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("StoredGrantCredential")
.field("grant_jws", &"<redacted>")
.field("client_key_secret", &"<redacted>")
.field("homeserver_pk", &self.homeserver_pk)
.finish()
}
}
impl StoredGrantCredential {
#[must_use]
fn encode(&self) -> String {
let secret = URL_SAFE_NO_PAD.encode(self.client_key_secret);
format!(
"{STORED_GRANT_CREDENTIAL_PREFIX}:{}:{secret}:{}",
self.homeserver_pk.z32(),
self.grant_jws
)
}
fn decode(token: &str) -> Result<Self> {
let (prefix, rest) = token.split_once(':').ok_or_else(invalid_stored_grant)?;
if prefix != STORED_GRANT_CREDENTIAL_PREFIX {
return Err(RequestError::Validation {
message: "unsupported grant credential token version".into(),
}
.into());
}
let (homeserver, rest) = rest.split_once(':').ok_or_else(invalid_stored_grant)?;
let (secret, grant_jws) = rest.split_once(':').ok_or_else(invalid_stored_grant)?;
if grant_jws.is_empty() {
return Err(invalid_stored_grant().into());
}
let homeserver_pk =
PublicKey::try_from_z32(homeserver).map_err(|_err| RequestError::Validation {
message: "invalid stored grant credential homeserver public key".into(),
})?;
let secret = URL_SAFE_NO_PAD
.decode(secret)
.map_err(|_err| RequestError::Validation {
message: "invalid stored grant credential client secret".into(),
})?;
let client_key_secret =
<[u8; 32]>::try_from(secret.as_slice()).map_err(|_err| RequestError::Validation {
message: "stored grant credential client secret must be 32 bytes".into(),
})?;
Ok(Self {
grant_jws: grant_jws.to_string(),
client_key_secret,
homeserver_pk,
})
}
}
impl GrantCredential {
pub(crate) fn from_response(
response: GrantSessionResponse,
grant_jws: String,
grant_claims: GrantClaims,
client_signer: GrantPopSigner,
homeserver_pk: PublicKey,
) -> Self {
let info = to_session_info(&response.session);
let state = GrantCredentialState {
bearer: response.token,
grant_jws,
grant_claims,
client_signer,
homeserver_pk,
session: response.session,
coordinator: None,
};
Self {
state: Arc::new(Mutex::new(state)),
info,
}
}
#[doc(hidden)]
pub fn from_shared_secret(token: &str) -> Result<Self> {
let material = restore_material(StoredGrantCredential::decode(token)?, true)?;
Ok(Self::from_shared_material(material))
}
#[doc(hidden)]
pub fn from_shared_delegated_state(
state: DelegatedGrantCredentialState,
sign: DelegatedSignFn,
) -> Result<Self> {
Ok(Self::from_shared_material(restore_delegated_material(
state, sign, true,
)?))
}
fn from_shared_material(
(jws, claims, signer, homeserver): (String, GrantClaims, GrantPopSigner, PublicKey),
) -> Self {
let response = GrantSessionResponse {
token: String::new(),
session: GrantSessionInfo {
homeserver: homeserver.clone(),
pubky: claims.iss.clone(),
client_id: claims.client_id.clone(),
capabilities: claims.caps.clone(),
grant_id: claims.jti.clone(),
token_expires_at: 0,
grant_expires_at: claims.exp,
created_at: 0,
},
};
Self::from_response(response, jws, claims, signer, homeserver)
}
pub(crate) async fn current_bearer(&self) -> String {
self.state.lock().await.bearer.clone()
}
pub async fn export_local_secret(&self) -> Option<String> {
let state = self.state.lock().await;
let client_key_secret = state.client_signer.local_secret()?;
Some(
StoredGrantCredential {
grant_jws: state.grant_jws.clone(),
client_key_secret,
homeserver_pk: state.homeserver_pk.clone(),
}
.encode(),
)
}
pub async fn export_delegated_restore_state(&self) -> Option<DelegatedGrantCredentialState> {
let state = self.state.lock().await;
let signer = state.client_signer.delegated_state()?;
Some(DelegatedGrantCredentialState {
grant_jws: state.grant_jws.clone(),
homeserver_pk: state.homeserver_pk.clone(),
key_id: signer.key_id,
client_pk: signer.public_key,
})
}
pub(crate) fn is_secret_token(token: &str) -> bool {
token.starts_with(STORED_GRANT_CREDENTIAL_PREFIX_FAMILY)
}
pub async fn import_secret(token: &str, client: &PubkyHttpClient) -> Result<Self> {
let saved = StoredGrantCredential::decode(token)?;
let (grant_jws, grant_claims, client_signer, homeserver_pk) =
restore_material(saved, false)?;
credential_from_grant_exchange(
client,
grant_jws,
grant_claims,
client_signer,
homeserver_pk,
)
.await
}
pub async fn import_delegated_state(
state: DelegatedGrantCredentialState,
client: &PubkyHttpClient,
sign: DelegatedSignFn,
) -> Result<Self> {
let (grant_jws, grant_claims, client_signer, homeserver_pk) =
restore_delegated_material(state, sign, false)?;
credential_from_grant_exchange(
client,
grant_jws,
grant_claims,
client_signer,
homeserver_pk,
)
.await
}
pub(crate) async fn refresh(&self, client: &PubkyHttpClient) -> Result<()> {
if let Some(coordinator) = self.coordinator().await {
return self
.refresh_shared(client, coordinator.as_ref(), None)
.await;
}
cross_log!(info, "Refreshing grant credential");
let mut state = self.state.lock().await;
if !state.needs_refresh(now_unix(), REFRESH_SLACK_SECS / 2) {
return Ok(());
}
let parsed = post_grant_session(
client,
&state.grant_jws,
&state.grant_claims,
&state.client_signer,
&state.homeserver_pk,
)
.await?;
state.bearer = parsed.token;
state.session = parsed.session;
Ok(())
}
async fn grant_session_request(
&self,
client: &PubkyHttpClient,
method: Method,
) -> Result<RequestBuilder> {
let (homeserver, user) = {
let state = self.state.lock().await;
(state.homeserver_pk.clone(), state.grant_claims.iss.clone())
};
client
.cross_request_via_homeserver(method, &homeserver, &user, GRANT_SESSION_PATH)
.await
}
}
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
impl SessionCredential for GrantCredential {
fn info(&self) -> SessionInfo {
self.info.clone()
}
async fn signout(&self, client: &PubkyHttpClient) -> Result<()> {
let lease = match self.coordinator().await {
Some(coordinator) => Some(coordinator.acquire(true).await?),
None => None,
};
if let Some(lease) = &lease {
let Some(mut shared) = lease.load().await? else {
return Ok(());
};
self.state.lock().await.adopt(shared.response.clone())?;
shared.logout_pending = true;
lease.store(&shared).await?;
}
let request = self.grant_session_request(client, Method::DELETE).await?;
let homeserver = self.state.lock().await.homeserver_pk.clone();
let supports_proof_logout = client
.features
.supports(
client,
&homeserver,
pubky_common::constants::features::GRANT_PROOF_LOGOUT,
)
.await;
let proof = {
let state = self.state.lock().await;
if supports_proof_logout {
let pop = sign_pop_for_grant(
&state.client_signer,
&state.homeserver_pk,
&state.grant_claims.jti,
)
.await?;
Some(serde_json::json!({ "grant": state.grant_jws, "pop": pop }))
} else {
None
}
};
let request = match proof {
Some(proof) => request.json(&proof),
None => request.bearer_auth(self.current_bearer().await),
};
let response = request.send().await?;
client.check_http_status(response).await?;
if let Some(lease) = lease {
lease.remove().await?;
}
Ok(())
}
async fn send(
&self,
rb: RequestBuilder,
client: &PubkyHttpClient,
) -> Result<reqwest::Response> {
if let Some(coordinator) = self.coordinator().await {
return self.send_shared(rb, client, coordinator.as_ref()).await;
}
Ok(self.attach(rb, client).await?.send().await?)
}
async fn attach(&self, rb: RequestBuilder, client: &PubkyHttpClient) -> Result<RequestBuilder> {
let needs_refresh = {
let grant_state = self.state.lock().await;
grant_state.needs_refresh(now_unix(), REFRESH_SLACK_SECS)
};
if needs_refresh {
self.refresh(client).await?;
}
let bearer = self.state.lock().await.bearer.clone();
Ok(rb.bearer_auth(bearer))
}
async fn can_attach_to(&self, homeserver: &PublicKey) -> bool {
&self.state.lock().await.homeserver_pk == homeserver
}
async fn revalidate(
&self,
client: &PubkyHttpClient,
_user: &PublicKey,
) -> Result<Option<SessionInfo>> {
let request = self.grant_session_request(client, Method::GET).await?;
let response = if let Some(coordinator) = self.coordinator().await {
match self
.send_shared(request, client, coordinator.as_ref())
.await
{
Err(Error::Request(RequestError::Server {
status: StatusCode::UNAUTHORIZED | StatusCode::NOT_FOUND,
..
})) => return Ok(None),
result => result?,
}
} else {
request
.bearer_auth(self.current_bearer().await)
.send()
.await?
};
if credential_session_missing(&response) {
return Ok(None);
}
let response = client.check_http_status(response).await?;
let session: GrantSessionInfo =
response
.json()
.await
.map_err(|e| RequestError::DecodeJson {
message: format!("decoding /auth/grant/session response: {e}"),
})?;
Ok(Some(to_session_info(&session)))
}
fn as_any(&self) -> &dyn Any {
self
}
}
impl PubkySession {
#[must_use]
pub fn from_grant_credential(client: PubkyHttpClient, credential: GrantCredential) -> Self {
Self::from_credential(client, Arc::new(credential))
}
pub async fn import_grant_secret(token: &str, client: Option<PubkyHttpClient>) -> Result<Self> {
let client = match client {
Some(client) => client,
None => PubkyHttpClient::new()?,
};
let credential = GrantCredential::import_secret(token, &client).await?;
Ok(Self::from_grant_credential(client, credential))
}
}
fn to_session_info(session: &GrantSessionInfo) -> SessionInfo {
SessionInfo::new(session.pubky.clone(), session.capabilities.clone())
}
fn restore_material(
saved: StoredGrantCredential,
allow_expired: bool,
) -> Result<(String, GrantClaims, GrantPopSigner, PublicKey)> {
let grant_claims = GrantClaims::decode(&saved.grant_jws).map_err(|err| {
AuthError::Validation(format!("invalid stored grant credential grant JWS: {err}"))
})?;
if !allow_expired && grant_claims.exp <= now_unix() {
return Err(AuthError::Validation("stored grant credential has expired".into()).into());
}
let client_keypair = Keypair::from_secret(&saved.client_key_secret);
if client_keypair.public_key() != grant_claims.cnf {
return Err(AuthError::Validation(
"stored grant credential client key does not match the grant cnf".into(),
)
.into());
}
Ok((
saved.grant_jws,
grant_claims,
GrantPopSigner::local(client_keypair),
saved.homeserver_pk,
))
}
fn restore_delegated_material(
saved: DelegatedGrantCredentialState,
sign: DelegatedSignFn,
allow_expired: bool,
) -> Result<(String, GrantClaims, GrantPopSigner, PublicKey)> {
let grant_claims = GrantClaims::decode(&saved.grant_jws).map_err(|err| {
AuthError::Validation(format!(
"invalid delegated grant credential grant JWS: {err}"
))
})?;
if !allow_expired && grant_claims.exp <= now_unix() {
return Err(AuthError::Validation("delegated grant credential has expired".into()).into());
}
if saved.client_pk != grant_claims.cnf {
return Err(AuthError::Validation(
"delegated grant credential client key does not match the grant cnf".into(),
)
.into());
}
Ok((
saved.grant_jws,
grant_claims,
GrantPopSigner::delegated(saved.key_id, saved.client_pk, sign),
saved.homeserver_pk,
))
}
fn invalid_stored_grant() -> AuthError {
AuthError::Validation(format!(
"invalid stored grant credential: expected `{STORED_GRANT_CREDENTIAL_PREFIX}:<homeserver>:<client_secret>:<grant_jws>`"
))
}
pub(crate) async fn sign_pop_for_grant(
client_signer: &GrantPopSigner,
homeserver_pk: &PublicKey,
grant_id: &pubky_common::auth::jws::GrantId,
) -> Result<String> {
let claims = PopProofClaims {
aud: homeserver_pk.clone(),
gid: grant_id.clone(),
nonce: PopNonce::generate(),
iat: now_unix(),
};
client_signer.sign_jws(POP_JWS_TYP, &claims).await
}
#[cfg(test)]
mod tests {
use std::num::NonZeroUsize;
use pkarr::{Cache, InMemoryCache};
use pubky_common::{
auth::jws::{ClientId, GRANT_JWS_TYP, GrantId},
capabilities::Capability,
};
use pubky_testnet::EphemeralTestnet;
use super::*;
#[tokio::test]
#[pubky_testnet::test]
async fn capped_bearer_requests_and_legacy_logout_do_not_refresh() {
let testnet = EphemeralTestnet::builder().build().await.unwrap();
let user = Keypair::random();
let homeserver = testnet.homeserver_app().public_key();
testnet
.sdk()
.unwrap()
.signer(user.clone())
.signup(&homeserver, None)
.await
.unwrap();
let mut builder = PubkyHttpClient::builder();
builder.pkarr(|b| {
*b = testnet.pkarr_client_builder();
b
});
let client = builder.build().unwrap();
client.features.insert(&homeserver, &[]);
let pop_key = Keypair::random();
let now = now_unix();
let grant = GrantClaims {
iss: user.public_key(),
client_id: ClientId::new("refresh.test").unwrap(),
caps: vec![Capability::root()],
cnf: pop_key.public_key(),
jti: GrantId::generate(),
iat: now,
exp: now + 120,
};
let jws = grant.sign(&user, GRANT_JWS_TYP);
let signer = GrantPopSigner::local(pop_key);
let response = post_grant_session(&client, &jws, &grant, &signer, &homeserver)
.await
.unwrap();
let bearer = response.token.clone();
assert_eq!(response.session.token_expires_at, grant.exp);
let credential = GrantCredential::from_response(response, jws, grant, signer, homeserver);
for _ in 0..3 {
credential.refresh(&client).await.unwrap();
let request = credential
.grant_session_request(&client, Method::GET)
.await
.unwrap();
let response = credential
.attach(request, &client)
.await
.unwrap()
.send()
.await
.unwrap();
client.check_http_status(response).await.unwrap();
assert_eq!(credential.current_bearer().await, bearer);
}
credential.state.lock().await.session.token_expires_at = now_unix() + 60;
credential.signout(&client).await.unwrap();
assert!(
credential
.revalidate(&client, &user.public_key())
.await
.unwrap()
.is_none()
);
}
#[test]
fn refresh_policy_preserves_capped_bearers_until_expiry() {
let (stored, claims) = stored_credential(5_000);
let signer = GrantPopSigner::local(Keypair::from_secret(&stored.client_key_secret));
let credential = test_credential(stored, claims, signer);
let mut state = credential.state.blocking_lock();
let now = 1_000;
for (bearer_exp, grant_exp, slack, expected) in [
(1_301, 5_000, 300, false),
(1_300, 5_000, 300, true),
(1_151, 5_000, 150, false),
(1_150, 5_000, 150, true),
(1_120, 1_120, 150, false),
(1_120, 1_100, 150, false),
(1_000, 1_000, 150, true),
(999, 999, 150, true),
(0, 1_120, 150, true),
] {
state.session.token_expires_at = bearer_exp;
state.grant_claims.exp = grant_exp;
assert_eq!(
state.needs_refresh(now, slack),
expected,
"bearer_exp={bearer_exp}, grant_exp={grant_exp}, slack={slack}"
);
}
}
#[test]
fn stored_grant_credential_encode_decode_round_trips() {
let (stored, _claims) = stored_credential(now_unix() + 3600);
let encoded = stored.encode();
let decoded = StoredGrantCredential::decode(&encoded).unwrap();
assert_eq!(decoded, stored);
}
#[test]
fn restore_material_rejects_mismatched_client_key() {
let (mut stored, _claims) = stored_credential(now_unix() + 3600);
stored.client_key_secret = Keypair::random().secret();
let error = restore_material(stored, false).unwrap_err().to_string();
assert!(error.contains("client key does not match"));
}
#[test]
fn restore_delegated_material_rejects_mismatched_client_key() {
let (stored, _claims) = stored_credential(now_unix() + 3600);
let saved = DelegatedGrantCredentialState {
grant_jws: stored.grant_jws,
homeserver_pk: stored.homeserver_pk,
key_id: "delegated-test-key".into(),
client_pk: Keypair::random().public_key(),
};
let error = restore_delegated_material(saved, test_delegated_signer(), false)
.unwrap_err()
.to_string();
assert!(error.contains("client key does not match"));
}
#[test]
fn restore_delegated_material_rejects_expired_grant() {
let (stored, claims) = stored_credential(now_unix().saturating_sub(1));
let saved = DelegatedGrantCredentialState {
grant_jws: stored.grant_jws,
homeserver_pk: stored.homeserver_pk,
key_id: "delegated-test-key".into(),
client_pk: claims.cnf,
};
let error = restore_delegated_material(saved, test_delegated_signer(), false)
.unwrap_err()
.to_string();
assert!(error.contains("has expired"));
}
#[tokio::test]
async fn export_local_secret_is_only_available_for_local_signers() {
let (stored, claims) = stored_credential(now_unix() + 3600);
let local_signer = GrantPopSigner::local(Keypair::from_secret(&stored.client_key_secret));
let delegated_signer = GrantPopSigner::delegated(
"delegated-test-key".into(),
claims.cnf.clone(),
test_delegated_signer(),
);
let local = test_credential(stored.clone(), claims.clone(), local_signer);
let delegated = test_credential(stored, claims, delegated_signer);
assert!(local.export_local_secret().await.is_some());
assert!(delegated.export_local_secret().await.is_none());
assert!(delegated.export_delegated_restore_state().await.is_some());
}
#[test]
fn restore_material_rejects_expired_grant() {
let (stored, _claims) = stored_credential(now_unix().saturating_sub(1));
let error = restore_material(stored, false).unwrap_err().to_string();
assert!(error.contains("has expired"));
}
#[test]
fn shared_restore_keeps_expired_material_for_logout() {
let (stored, claims) = stored_credential(now_unix().saturating_sub(1));
GrantCredential::from_shared_secret(&stored.encode()).unwrap();
let delegated = DelegatedGrantCredentialState {
grant_jws: stored.grant_jws,
homeserver_pk: stored.homeserver_pk,
key_id: "delegated-test-key".into(),
client_pk: claims.cnf,
};
GrantCredential::from_shared_delegated_state(delegated, test_delegated_signer()).unwrap();
}
#[test]
fn stored_grant_credential_decode_rejects_wrong_prefix() {
let error = StoredGrantCredential::decode("wrong:v:secret:grant")
.unwrap_err()
.to_string();
assert!(error.contains("unsupported grant credential token version"));
}
fn stored_credential(exp: u64) -> (StoredGrantCredential, GrantClaims) {
let user_keypair = Keypair::random();
let client_keypair = Keypair::random();
let homeserver_keypair = Keypair::random();
let claims = GrantClaims {
iss: user_keypair.public_key(),
client_id: ClientId::new("stored-grant.test").unwrap(),
caps: vec![Capability::root()],
cnf: client_keypair.public_key(),
jti: GrantId::generate(),
iat: now_unix(),
exp,
};
let grant_jws = claims.sign(&user_keypair, GRANT_JWS_TYP);
let stored = StoredGrantCredential {
grant_jws,
client_key_secret: client_keypair.secret(),
homeserver_pk: homeserver_keypair.public_key(),
};
(stored, claims)
}
fn test_credential(
stored: StoredGrantCredential,
claims: GrantClaims,
client_signer: GrantPopSigner,
) -> GrantCredential {
let now = now_unix();
GrantCredential::from_response(
GrantSessionResponse {
token: "test-bearer".into(),
session: GrantSessionInfo {
homeserver: stored.homeserver_pk.clone(),
pubky: claims.iss.clone(),
client_id: claims.client_id.clone(),
capabilities: claims.caps.clone(),
grant_id: claims.jti.clone(),
token_expires_at: now + 300,
grant_expires_at: claims.exp,
created_at: now,
},
},
stored.grant_jws,
claims,
client_signer,
stored.homeserver_pk,
)
}
fn test_delegated_signer() -> DelegatedSignFn {
super::super::pop_signer::delegated_sign_callback(|_| async { Ok(vec![0; 64]) })
}
#[tokio::test]
async fn can_attach_to_only_matches_bound_homeserver() {
let bound = Keypair::random().public_key();
let other = Keypair::random().public_key();
let (mut stored, claims) = stored_credential(now_unix() + 3600);
stored.homeserver_pk = bound.clone();
let client_signer = GrantPopSigner::local(Keypair::from_secret(&stored.client_key_secret));
let credential = test_credential(stored, claims, client_signer);
assert!(
credential.can_attach_to(&bound).await,
"grant credential must attach to the homeserver it was minted for"
);
assert!(
!credential.can_attach_to(&other).await,
"grant credential must NOT attach to a homeserver it was not minted for"
);
}
#[tokio::test]
async fn grant_session_request_routes_through_bound_homeserver() {
use pkarr::{SignedPacket, dns::rdata::SVCB};
let (mut stored, claims) = stored_credential(now_unix() + 3600);
let homeserver_keypair = pkarr::Keypair::random();
stored.homeserver_pk =
PublicKey::try_from_z32(&homeserver_keypair.public_key().to_string()).unwrap();
let icann = SVCB::new(10, "example.com".try_into().unwrap());
let homeserver_packet = SignedPacket::builder()
.https(".".try_into().unwrap(), icann, 3600)
.sign(&homeserver_keypair)
.unwrap();
let client_signer = GrantPopSigner::local(Keypair::from_secret(&stored.client_key_secret));
let credential = test_credential(stored, claims.clone(), client_signer);
let cache = Arc::new(InMemoryCache::new(NonZeroUsize::MIN));
let mut builder = PubkyHttpClient::builder();
builder
.isolated_pkarr_test()
.pkarr(|b| b.cache(Arc::<InMemoryCache>::clone(&cache)));
let client = builder.build().unwrap();
cache.put(&homeserver_keypair.public_key().into(), &homeserver_packet);
let request = credential
.grant_session_request(&client, Method::POST)
.await
.unwrap()
.build()
.unwrap();
assert_eq!(request.url().host_str(), Some("example.com"));
assert_eq!(request.url().path(), "/auth/grant/session");
assert_eq!(
request.headers().get("pubky-host").unwrap(),
&claims.iss.z32()
);
}
}