use bon::Builder;
use serde::Serialize;
use crate::{
core::{secrets::SecretString, server_metadata::AuthorizationServerMetadata},
grant::core::{OAuth2ExchangeGrant, mk_scopes},
token::RefreshToken,
};
#[huskarl_macros::grant]
#[derive(Debug, Clone, Builder)]
#[builder(state_mod(name = "builder"), on(String, into))]
pub struct RefreshGrant {}
impl<
Auth: crate::core::client_auth::ClientAuthentication + 'static,
D: crate::core::dpop::AuthorizationServerDPoP + 'static,
> RefreshGrant<Auth, D>
{
pub fn builder_from_metadata(
metadata: &AuthorizationServerMetadata,
) -> RefreshGrantBuilder<Auth, D, SetCommonMetadata> {
RefreshGrant::builder().with_common_metadata(metadata)
}
}
#[huskarl_macros::grant_impl]
impl<
Auth: crate::core::client_auth::ClientAuthentication + 'static,
D: crate::core::dpop::AuthorizationServerDPoP + 'static,
> OAuth2ExchangeGrant for RefreshGrant<Auth, D>
{
type Parameters = RefreshGrantParameters;
type ClientAuth = Auth;
type DPoP = D;
type Form<'a> = RefreshGrantForm;
fn to_refresh_grant(&self) -> RefreshGrant<Auth, D> {
self.clone()
}
fn bound_dpop_jkt(params: &Self::Parameters) -> Option<&str> {
params.refresh_token.dpop_jkt()
}
fn build_form(&self, params: Self::Parameters) -> Self::Form<'_> {
RefreshGrantForm {
grant_type: "refresh_token",
refresh_token: params.refresh_token.token().clone(),
scope: params.scope,
resource: params.resource,
}
}
}
#[derive(Debug, Clone, Builder)]
pub struct RefreshGrantParameters {
refresh_token: RefreshToken,
#[builder(required, default, name = "scopes", with = |scopes: impl IntoIterator<Item = impl Into<String>>| mk_scopes(scopes))]
scope: Option<String>,
resource: Option<Vec<String>>,
}
impl RefreshGrantParameters {
#[must_use]
pub fn refresh_token(token: RefreshToken) -> Self {
Self::builder().refresh_token(token).build()
}
}
#[derive(Debug, Serialize)]
pub struct RefreshGrantForm {
grant_type: &'static str,
refresh_token: SecretString,
#[serde(skip_serializing_if = "Option::is_none")]
scope: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
resource: Option<Vec<String>>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn refresh_form_serializes_token_as_plain_string() {
let form = RefreshGrantForm {
grant_type: "refresh_token",
refresh_token: SecretString::new("my-refresh-token"),
scope: None,
resource: None,
};
let encoded = serde_html_form::to_string(&form).unwrap();
assert_eq!(encoded, "grant_type=refresh_token&refresh_token=my-refresh-token");
}
#[test]
fn refresh_form_resource_serializes_as_repeated_keys() {
let form = RefreshGrantForm {
grant_type: "refresh_token",
refresh_token: SecretString::new("tok"),
scope: None,
resource: Some(vec![
"https://api.example.com".to_string(),
"https://other.example.com".to_string(),
]),
};
let encoded = serde_html_form::to_string(&form).unwrap();
assert!(
encoded.contains("resource=https%3A%2F%2Fapi.example.com"),
"first resource not found in: {encoded}"
);
assert!(
encoded.contains("resource=https%3A%2F%2Fother.example.com"),
"second resource not found in: {encoded}"
);
assert!(
!encoded.contains(','),
"resource values should not be comma-joined: {encoded}"
);
}
}