use serde::{Deserialize, Serialize};
use crate::auth::{AbsoluteUrl, SecretBearer};
use crate::ids::TenantSlug;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(tag = "grant_type")]
pub enum TokenRequest {
#[serde(rename = "authorization_code")]
AuthorizationCode {
code: SecretBearer,
redirect_uri: String,
code_verifier: SecretBearer,
},
#[serde(rename = "refresh_token")]
RefreshToken {
refresh_token: SecretBearer,
},
#[serde(rename = "urn:ietf:params:oauth:grant-type:device_code")]
DeviceCode {
device_code: SecretBearer,
},
#[serde(rename = "urn:ietf:params:oauth:grant-type:token-exchange")]
TokenExchange {
subject_token: SecretBearer,
subject_token_type: ExchangeTokenType,
actor_token: Option<SecretBearer>,
actor_token_type: Option<ExchangeTokenType>,
audience: Option<TokenAudience>,
},
#[serde(rename = "urn:ietf:params:oauth:grant-type:jwt-bearer")]
JwtBearer {
assertion: SecretBearer,
tenant: Option<TenantSlug>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub enum ExchangeTokenType {
#[serde(rename = "urn:ietf:params:oauth:token-type:access_token")]
AccessToken,
#[serde(rename = "urn:wyrd:oauth:token-type:api_key")]
ApiKey,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum TokenAudience {
Wyrd,
Bifrost,
}
impl TokenAudience {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Wyrd => "wyrd",
Self::Bifrost => "bifrost",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct TokenResponse {
pub access_token: SecretBearer,
pub token_type: TokenType,
pub expires_in: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub refresh_token: Option<SecretBearer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub issued_token_type: Option<ExchangeTokenType>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(rename_all = "PascalCase")]
pub enum TokenType {
Bearer,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub enum OAuthClientId {
#[serde(rename = "wyrd-ui")]
WyrdUi,
#[serde(rename = "wyrd-cli")]
WyrdCli,
}
impl OAuthClientId {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::WyrdUi => "wyrd-ui",
Self::WyrdCli => "wyrd-cli",
}
}
#[must_use]
pub fn parse(client_id: &str) -> Option<Self> {
match client_id {
"wyrd-ui" => Some(Self::WyrdUi),
"wyrd-cli" => Some(Self::WyrdCli),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum OAuthErrorCode {
InvalidRequest,
InvalidClient,
InvalidGrant,
UnauthorizedClient,
UnsupportedGrantType,
UnsupportedResponseType,
InvalidTarget,
AccessDenied,
AuthorizationPending,
SlowDown,
ExpiredToken,
ServerError,
TemporarilyUnavailable,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct OAuthErrorResponse {
pub error: OAuthErrorCode,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error_description: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct AuthorizationServerMetadata {
pub issuer: AbsoluteUrl,
pub authorization_endpoint: AbsoluteUrl,
pub token_endpoint: AbsoluteUrl,
pub device_authorization_endpoint: AbsoluteUrl,
pub revocation_endpoint: AbsoluteUrl,
pub response_types_supported: Vec<String>,
pub grant_types_supported: Vec<String>,
pub token_endpoint_auth_methods_supported: Vec<String>,
pub revocation_endpoint_auth_methods_supported: Vec<String>,
pub code_challenge_methods_supported: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::{
ExchangeTokenType, OAuthClientId, OAuthErrorCode, OAuthErrorResponse, TokenAudience,
TokenRequest, TokenResponse, TokenType,
};
use crate::auth::SecretBearer;
fn parse(pairs: &[(&str, &str)]) -> Result<TokenRequest, serde_json::Error> {
let map = pairs
.iter()
.map(|(key, value)| ((*key).to_owned(), serde_json::json!(value)))
.collect::<serde_json::Map<_, _>>();
serde_json::from_value(serde_json::Value::Object(map))
}
#[test]
fn grants_decode_from_rfc_parameters() {
assert_eq!(
parse(&[
("grant_type", "authorization_code"),
("code", "c"),
("redirect_uri", "https://wyrd.example.com/login/callback"),
("code_verifier", "v"),
("client_id", "wyrd-ui"),
])
.expect("code grant decodes"),
TokenRequest::AuthorizationCode {
code: SecretBearer::new("c".to_owned()),
redirect_uri: "https://wyrd.example.com/login/callback".to_owned(),
code_verifier: SecretBearer::new("v".to_owned()),
}
);
assert_eq!(
parse(&[
(
"grant_type",
"urn:ietf:params:oauth:grant-type:token-exchange"
),
("subject_token", "wyrd_sk_key"),
("subject_token_type", "urn:wyrd:oauth:token-type:api_key"),
])
.expect("api-key exchange decodes"),
TokenRequest::TokenExchange {
subject_token: SecretBearer::new("wyrd_sk_key".to_owned()),
subject_token_type: ExchangeTokenType::ApiKey,
actor_token: None,
actor_token_type: None,
audience: None,
}
);
assert_eq!(
parse(&[
("grant_type", "refresh_token"),
("refresh_token", "r"),
("scope", "ignored"),
])
.expect("refresh decodes"),
TokenRequest::RefreshToken {
refresh_token: SecretBearer::new("r".to_owned()),
}
);
assert!(parse(&[("grant_type", "password"), ("username", "u")]).is_err());
assert!(parse(&[("grant_type", "wyrd_api_key"), ("api_key", "k")]).is_err());
}
#[test]
fn response_bodies_follow_rfc_6749_section_5() {
let response = TokenResponse {
access_token: SecretBearer::new("access".to_owned()),
token_type: TokenType::Bearer,
expires_in: 300,
refresh_token: None,
issued_token_type: Some(ExchangeTokenType::AccessToken),
};
assert_eq!(
serde_json::to_value(&response).expect("serializes"),
serde_json::json!({
"access_token": "access",
"token_type": "Bearer",
"expires_in": 300,
"issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
})
);
let error = OAuthErrorResponse {
error: OAuthErrorCode::AuthorizationPending,
error_description: None,
};
assert_eq!(
serde_json::to_value(&error).expect("serializes"),
serde_json::json!({ "error": "authorization_pending" })
);
assert_eq!(
serde_json::to_value(OAuthErrorCode::InvalidTarget).expect("serializes"),
"invalid_target"
);
assert_eq!(
OAuthClientId::parse("wyrd-cli"),
Some(OAuthClientId::WyrdCli)
);
assert_eq!(OAuthClientId::WyrdUi.as_str(), "wyrd-ui");
assert_eq!(OAuthClientId::parse("other"), None);
}
#[test]
fn secret_grants_redact_debug() {
let request = TokenRequest::JwtBearer {
assertion: SecretBearer::new("top-secret-assertion".to_owned()),
tenant: None,
};
let debug = format!("{request:?}");
assert!(debug.contains("REDACTED"));
assert!(!debug.contains("top-secret-assertion"));
}
#[test]
fn audience_wire_values_are_claim_values() {
for audience in [TokenAudience::Wyrd, TokenAudience::Bifrost] {
assert_eq!(
serde_json::to_value(audience).expect("serializes"),
audience.as_str()
);
}
}
}