use huskarl::{
core::{
BoxedError, client_auth::ClientAuthentication, dpop::AuthorizationServerDPoP,
http::HttpClient,
},
grant::{
authorization_code::{
AuthorizationCodeGrant, CompleteInput, Jar, PendingState, StartInput, StartOutput,
},
core::{OAuth2ExchangeGrant, TokenResponse},
refresh::RefreshGrantParameters,
},
token::{RefreshToken, id_token::IdTokenClaims},
};
use serde::{Deserialize, Serialize};
#[derive(bon::Builder)]
pub struct CompletedLogin {
token_response: TokenResponse,
id_token_claims: Option<IdTokenClaims>,
}
impl CompletedLogin {
#[must_use]
pub fn token_response(&self) -> &TokenResponse {
&self.token_response
}
#[must_use]
pub fn id_token_claims(&self) -> Option<&IdTokenClaims> {
self.id_token_claims.as_ref()
}
#[must_use]
pub fn into_parts(self) -> (TokenResponse, Option<IdTokenClaims>) {
(self.token_response, self.id_token_claims)
}
}
pub trait LoginGrant: Send + Sync {
fn start(
&self,
http_client: &impl HttpClient,
scopes: Vec<String>,
) -> impl Future<Output = Result<StartOutput, BoxedError>> + Send;
fn complete(
&self,
http_client: &impl HttpClient,
pending_state: &PendingState,
code: String,
state: String,
iss: Option<String>,
) -> impl Future<Output = Result<CompletedLogin, BoxedError>> + Send;
fn refresh(
&self,
http_client: &impl HttpClient,
refresh_token: &RefreshToken,
) -> impl Future<Output = Result<TokenResponse, BoxedError>> + Send;
}
impl<Auth, D, J, Extra> LoginGrant for AuthorizationCodeGrant<Auth, D, J, Extra>
where
Auth: ClientAuthentication + Clone + Send + Sync + 'static,
D: AuthorizationServerDPoP + Send + Sync + 'static,
J: Jar + Send + Sync + 'static,
Extra: Clone + Serialize + for<'de> Deserialize<'de> + Send + Sync + 'static,
{
async fn start(
&self,
http_client: &impl HttpClient,
scopes: Vec<String>,
) -> Result<StartOutput, BoxedError> {
self.start(http_client, StartInput::scopes(scopes))
.await
.map_err(BoxedError::from_err)
}
async fn complete(
&self,
http_client: &impl HttpClient,
pending_state: &PendingState,
code: String,
state: String,
iss: Option<String>,
) -> Result<CompletedLogin, BoxedError> {
let input = CompleteInput::builder()
.code(code)
.state(state)
.maybe_iss(iss)
.build();
let (token_response, validated_id_token) = self
.complete_oidc(http_client, pending_state, input)
.await
.map_err(BoxedError::from_err)?;
let id_token_claims = validated_id_token
.map(|jwt| {
serde_json::to_value(&jwt.claims)
.and_then(serde_json::from_value::<IdTokenClaims>)
.map_err(|e| BoxedError::from_err(ClaimsReshapeError(e)))
})
.transpose()?;
Ok(CompletedLogin::builder()
.token_response(token_response)
.maybe_id_token_claims(id_token_claims)
.build())
}
async fn refresh(
&self,
http_client: &impl HttpClient,
refresh_token: &RefreshToken,
) -> Result<TokenResponse, BoxedError> {
self.to_refresh_grant()
.exchange(
http_client,
RefreshGrantParameters::refresh_token(refresh_token.clone()),
)
.await
.map_err(BoxedError::from_err)
}
}
#[derive(Debug)]
struct ClaimsReshapeError(serde_json::Error);
impl std::fmt::Display for ClaimsReshapeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "failed to reshape id token claims: {}", self.0)
}
}
impl std::error::Error for ClaimsReshapeError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.0)
}
}
impl huskarl::core::Error for ClaimsReshapeError {
fn is_retryable(&self) -> bool {
false
}
}