use std::{marker::PhantomData, sync::Arc};
use bon::Builder;
use serde::{Deserialize, Serialize};
use crate::{
core::{
BoxedError, EndpointUrl,
client_auth::ClientAuthentication,
crypto::verifier::JwsVerifierPlatform,
crypto::verifier::{BoxedJwsVerifier, JwsVerifierFactory},
dpop::AuthorizationServerDPoP,
platform::MaybeSendSync,
server_metadata::AuthorizationServerMetadata,
},
grant::{
authorization_code::{
grant::builder::{
SetAuthorizationEndpoint, SetAuthorizationResponseIssParameterSupported,
SetCodeChallengeMethodsSupported, SetJwksUri,
SetMtlsPushedAuthorizationRequestEndpoint, SetPushedAuthorizationRequestEndpoint,
SetRequirePushedAuthorizationRequests, State,
},
jar::{Jar, NoJar},
},
core::OAuth2ExchangeGrant,
refresh,
},
};
#[huskarl_macros::grant(vis(pub(super)))]
#[derive(Clone)]
pub struct AuthorizationCodeGrant<
J: Jar = NoJar,
IdClaims: Clone + for<'de> Deserialize<'de> + 'static = (),
> {
pub(super) jws_verifier: Option<BoxedJwsVerifier>,
pub(super) jar: J,
pub(super) authorization_endpoint: EndpointUrl,
pub(super) pushed_authorization_request_endpoint: Option<EndpointUrl>,
pub(super) mtls_pushed_authorization_request_endpoint: Option<EndpointUrl>,
pub(super) require_pushed_authorization_requests: bool,
pub(super) authorization_response_iss_parameter_supported: bool,
pub(super) code_challenge_methods_supported: Vec<String>,
pub(super) redirect_uri: String,
pub(super) prefer_pushed_authorization_requests: bool,
_phantom: PhantomData<IdClaims>,
}
#[huskarl_macros::grant_new]
#[bon::bon]
impl<
Auth: ClientAuthentication + 'static,
D: AuthorizationServerDPoP + 'static,
J: Jar + 'static,
IdClaims: Clone + for<'de> Deserialize<'de> + 'static,
> AuthorizationCodeGrant<Auth, D, J, IdClaims>
{
#[builder(
state_mod(name = "builder"),
generics(setters(vis = "", name = "with_{}_internal")),
on(String, into)
)]
pub async fn new(
jar: J,
#[endpoint_url] jwks_uri: Option<EndpointUrl>,
#[endpoint_url] authorization_endpoint: EndpointUrl,
#[endpoint_url] pushed_authorization_request_endpoint: Option<EndpointUrl>,
#[endpoint_url] mtls_pushed_authorization_request_endpoint: Option<EndpointUrl>,
#[builder(default)] require_pushed_authorization_requests: bool,
#[builder(default)] authorization_response_iss_parameter_supported: bool,
#[builder(default = vec!["S256".to_string()])] code_challenge_methods_supported: Vec<
String,
>,
redirect_uri: String,
#[builder(default = true)] prefer_pushed_authorization_requests: bool,
#[cfg(not(feature = "default-jws-verifier-platform"))] jws_verifier_platform: Option<
Arc<dyn JwsVerifierPlatform>,
>,
#[cfg(feature = "default-jws-verifier-platform")]
#[cfg_attr(feature = "default-jws-verifier-platform", builder(default = crate::DefaultJwsVerifierPlatform::default().into()))]
jws_verifier_platform: Arc<dyn JwsVerifierPlatform>,
jws_verifier_factory: Option<Arc<dyn JwsVerifierFactory>>,
) -> Result<Self, BoxedError> {
#[cfg(feature = "default-jws-verifier-platform")]
let jws_verifier_platform = Some(jws_verifier_platform);
let jws_verifier = if let Some(jws_verifier_platform) = jws_verifier_platform.clone()
&& let Some(jws_verifier_factory) = jws_verifier_factory.clone()
{
Some(
jws_verifier_factory
.build(jwks_uri.as_ref(), jws_verifier_platform)
.await?,
)
} else {
None
};
Ok(Self {
jws_verifier,
client_id,
client_auth,
dpop,
jar,
token_endpoint,
token_endpoint_auth_methods_supported,
authorization_endpoint,
issuer,
pushed_authorization_request_endpoint,
mtls_token_endpoint,
mtls_pushed_authorization_request_endpoint,
require_pushed_authorization_requests,
authorization_response_iss_parameter_supported,
code_challenge_methods_supported,
redirect_uri,
prefer_pushed_authorization_requests,
_phantom: PhantomData,
})
}
}
impl<Auth: ClientAuthentication + 'static, D: AuthorizationServerDPoP + 'static, J: Jar + 'static>
AuthorizationCodeGrant<Auth, D, J, ()>
{
#[must_use]
#[allow(clippy::type_complexity)]
pub fn builder_from_metadata(
metadata: &AuthorizationServerMetadata,
) -> Option<
AuthorizationCodeGrantBuilder<
Auth,
D,
J,
(),
SetMtlsPushedAuthorizationRequestEndpoint<
SetAuthorizationEndpoint<
SetJwksUri<
SetPushedAuthorizationRequestEndpoint<
SetCodeChallengeMethodsSupported<
SetAuthorizationResponseIssParameterSupported<
SetRequirePushedAuthorizationRequests<SetCommonMetadata>,
>,
>,
>,
>,
>,
>,
>,
> {
metadata
.authorization_endpoint
.as_ref()
.map(|authorization_endpoint| {
AuthorizationCodeGrant::builder()
.with_common_metadata(metadata)
.require_pushed_authorization_requests(
metadata.require_pushed_authorization_requests,
)
.authorization_response_iss_parameter_supported(
metadata.authorization_response_iss_parameter_supported,
)
.code_challenge_methods_supported(
metadata.code_challenge_methods_supported.clone(),
)
.maybe_pushed_authorization_request_endpoint_internal(
metadata.pushed_authorization_request_endpoint.clone(),
)
.maybe_jwks_uri_internal(metadata.jwks_uri.clone())
.authorization_endpoint_internal(authorization_endpoint.clone())
.maybe_mtls_pushed_authorization_request_endpoint_internal(
metadata
.mtls_endpoint_aliases
.as_ref()
.and_then(|a| a.pushed_authorization_request_endpoint.clone()),
)
})
}
}
impl<
Auth: ClientAuthentication + 'static,
D: AuthorizationServerDPoP + 'static,
J: Jar + 'static,
IdClaims: Clone + for<'de> Deserialize<'de> + MaybeSendSync + 'static,
S: State,
> AuthorizationCodeGrantBuilder<Auth, D, J, IdClaims, S>
{
pub fn with_id_claims<C: Clone + for<'de> Deserialize<'de> + MaybeSendSync + 'static>(
self,
) -> AuthorizationCodeGrantBuilder<Auth, D, J, C, S> {
self.with_id_claims_internal()
}
}
#[derive(Debug, Clone, Builder)]
pub struct AuthorizationCodeGrantParameters {
#[builder(into)]
pub dpop_jkt: Option<String>,
#[builder(into)]
pub code: String,
#[builder(into)]
pub pkce_verifier: Option<String>,
pub resource: Option<Vec<String>>,
}
#[derive(Debug, Serialize)]
pub struct AuthorizationCodeGrantForm<'a> {
grant_type: &'static str,
code: String,
redirect_uri: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
code_verifier: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
resource: Option<Vec<String>>,
}
#[huskarl_macros::grant_impl]
impl<
Auth: ClientAuthentication + Clone + 'static,
D: AuthorizationServerDPoP + 'static,
J: Jar + 'static,
IdClaims: Clone + for<'de> Deserialize<'de> + MaybeSendSync + 'static,
> OAuth2ExchangeGrant for AuthorizationCodeGrant<Auth, D, J, IdClaims>
{
type Parameters = AuthorizationCodeGrantParameters;
type ClientAuth = Auth;
type DPoP = D;
type Form<'a> = AuthorizationCodeGrantForm<'a>;
fn bound_dpop_jkt(params: &Self::Parameters) -> Option<&str> {
params.dpop_jkt.as_deref()
}
fn to_refresh_grant(&self) -> refresh::RefreshGrant<Self::ClientAuth, Self::DPoP> {
refresh::RefreshGrant::builder()
.client_id(self.client_id.clone())
.maybe_issuer(self.issuer.clone())
.client_auth(self.client_auth.clone())
.dpop(self.dpop.clone())
.token_endpoint(self.token_endpoint.clone())
.unwrap_or_else(|e| match e {})
.maybe_token_endpoint_auth_methods_supported(
self.token_endpoint_auth_methods_supported.clone(),
)
.build()
}
fn build_form(&self, params: Self::Parameters) -> Self::Form<'_> {
Self::Form {
grant_type: "authorization_code",
code: params.code,
redirect_uri: &self.redirect_uri,
code_verifier: params.pkce_verifier,
resource: params.resource,
}
}
}