use std::fmt;
use url::Url;
use pubky_common::{
auth::jws::ClientId,
crypto::{Keypair, PublicKey, random_bytes},
};
use crate::actors::DEFAULT_HTTP_RELAY_INBOX;
use crate::actors::auth::deep_links::{
DeepLink, DeepLinkScheme, GrantApprovalFormat, GrantRelayChannel, SigninGrantDeepLink,
SigninGrantParams, SignupGrantDeepLink, SignupGrantParams, XCallbackParams,
};
use crate::actors::auth::grant::approval_encryption::ApprovalRecipientSecret;
use crate::actors::auth::grant::flow::PubkyGrantAuthFlow;
use crate::actors::auth::grant::pop_signer::{DelegatedSignFn, GrantPopSigner};
use crate::actors::auth::kind::AuthFlowKind;
use crate::actors::auth::relay::auth_relay_listener::AuthRelayListener;
use crate::errors::Result;
use crate::{Capabilities, PubkyHttpClient};
#[derive(Clone)]
pub struct GrantAuthFlowBuilder {
caps: Capabilities,
base_relay: Url,
client: Option<PubkyHttpClient>,
auth_kind: AuthFlowKind,
client_secret: [u8; 32],
client_id: ClientId,
client_signer: GrantPopSigner,
x_callback: XCallbackParams,
approval_format: GrantApprovalFormat,
}
impl fmt::Debug for GrantAuthFlowBuilder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("GrantAuthFlowBuilder")
.field("caps", &self.caps)
.field("base_relay", &self.base_relay)
.field("client", &self.client)
.field("auth_kind", &self.auth_kind)
.field("client_secret", &"<redacted>")
.field("client_id", &self.client_id)
.field("client_signer", &self.client_signer)
.field("x_callback", &self.x_callback)
.field("approval_format", &self.approval_format)
.finish()
}
}
impl GrantAuthFlowBuilder {
pub(crate) fn new(caps: Capabilities, auth_kind: AuthFlowKind, client_id: ClientId) -> Self {
Self {
caps,
base_relay: Url::parse(DEFAULT_HTTP_RELAY_INBOX)
.expect("Should be able to parse the default HTTP relay"),
client: None,
auth_kind,
client_secret: random_bytes::<32>(),
client_id,
client_signer: GrantPopSigner::local(Keypair::random()),
x_callback: XCallbackParams::default(),
approval_format: GrantApprovalFormat::BareGrant,
}
}
#[must_use]
pub fn approval_format(mut self, format: GrantApprovalFormat) -> Self {
self.approval_format = format;
self
}
#[must_use]
pub fn relay(mut self, url: Url) -> Self {
self.base_relay = url;
self
}
#[must_use]
pub fn client(mut self, client: PubkyHttpClient) -> Self {
self.client = Some(client);
self
}
#[must_use]
pub fn client_secret(mut self, client_secret: [u8; 32]) -> Self {
self.client_secret = client_secret;
self
}
#[must_use]
pub fn client_keypair(mut self, keypair: Keypair) -> Self {
self.client_signer = GrantPopSigner::local(keypair);
self
}
#[must_use]
pub fn x_callback(mut self, x_callback: XCallbackParams) -> Self {
self.x_callback = x_callback;
self
}
#[must_use]
#[doc(hidden)]
pub fn delegated_client_signer(
mut self,
key_id: String,
public_key: PublicKey,
sign: DelegatedSignFn,
) -> Self {
self.client_signer = GrantPopSigner::delegated(key_id, public_key, sign);
self
}
pub fn start(self) -> Result<PubkyGrantAuthFlow> {
let Self {
caps,
base_relay,
client,
auth_kind,
client_secret,
client_id,
client_signer,
x_callback,
approval_format,
} = self;
approval_format.validate_capabilities(&caps)?;
let client = match client {
Some(c) => c,
None => PubkyHttpClient::new()?,
};
let client_pk = client_signer.public_key();
let (approval_key_secret, relay_channel) =
if approval_format == GrantApprovalFormat::SignedApprovalV1 {
let (secret, public_key) = ApprovalRecipientSecret::generate();
(
Some(secret),
GrantRelayChannel::Hpke {
ephemeral_public_key: public_key,
},
)
} else {
(None, GrantRelayChannel::SharedSecret(client_secret))
};
let auth_url = match auth_kind {
AuthFlowKind::SignIn => DeepLink::SigninGrant(SigninGrantDeepLink::new(
DeepLinkScheme::PubkyAuth,
SigninGrantParams {
capabilities: caps,
relay: base_relay.clone(),
relay_channel,
client_id,
client_pk,
approval_format,
},
)),
AuthFlowKind::SignUp {
homeserver_public_key,
signup_token,
} => {
let hs_pk = *homeserver_public_key;
DeepLink::SignupGrant(SignupGrantDeepLink::new(
DeepLinkScheme::PubkyAuth,
SignupGrantParams {
capabilities: caps,
relay: base_relay.clone(),
relay_channel,
homeserver: hs_pk,
signup_token: signup_token.clone(),
client_id,
client_pk,
approval_format,
},
))
}
}
.with_x_callback(x_callback);
let relay_listener = AuthRelayListener::builder_for_channel(relay_channel)
.relay_base_url(base_relay)
.client(client.clone())
.start()?;
Ok(PubkyGrantAuthFlow::new(
relay_listener,
client,
auth_url.into(),
client_signer,
approval_key_secret,
))
}
}