Skip to main content

systemprompt_api/routes/oauth/endpoints/token/
mod.rs

1//! OAuth 2.0 token endpoint.
2//!
3//! Hosts the `/token` handler and the request/response types
4//! ([`TokenRequest`], [`TokenResponse`]) it binds. Per-grant token minting
5//! lives in [`generation`]; [`TokenError`] partitions failures by RFC 6749
6//! error code and maps onto the HTTP error surface.
7
8pub mod generation;
9mod handler;
10pub mod validation;
11
12pub use handler::handle_token;
13
14use serde::{Deserialize, Serialize};
15
16use crate::routes::oauth::OAuthHttpError;
17
18pub type TokenResult<T> = Result<T, TokenError>;
19
20#[derive(Debug, Deserialize)]
21pub struct TokenRequest {
22    pub grant_type: String,
23    pub code: Option<String>,
24    pub redirect_uri: Option<String>,
25    pub client_id: Option<String>,
26    pub client_secret: Option<String>,
27    pub refresh_token: Option<String>,
28    pub scope: Option<String>,
29    pub code_verifier: Option<String>,
30    pub resource: Option<String>,
31    pub plugin_id: Option<String>,
32    pub audience: Option<String>,
33    pub subject_token: Option<String>,
34    pub subject_token_type: Option<String>,
35    pub actor_token: Option<String>,
36    pub actor_token_type: Option<String>,
37    pub requested_token_type: Option<String>,
38}
39
40#[derive(Debug, Serialize)]
41pub struct TokenResponse {
42    pub access_token: String,
43    pub token_type: String,
44    pub expires_in: i64,
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub refresh_token: Option<String>,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub scope: Option<String>,
49    // RFC 8693 ยง2.2.1 issued_token_type. Only set by the
50    // urn:ietf:params:oauth:grant-type:token-exchange flow.
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub issued_token_type: Option<String>,
53}
54
55#[derive(Debug, thiserror::Error)]
56pub enum TokenError {
57    #[error("Invalid request: {field} {message}")]
58    InvalidRequest { field: String, message: String },
59
60    #[error("Unsupported grant type: {grant_type}")]
61    UnsupportedGrantType { grant_type: String },
62
63    #[error("Invalid client credentials")]
64    InvalidClient,
65
66    #[error("Invalid authorization code: {reason}")]
67    InvalidGrant { reason: String },
68
69    #[error("Invalid refresh token: {reason}")]
70    InvalidRefreshToken { reason: String },
71
72    #[error("Invalid credentials")]
73    InvalidCredentials,
74
75    #[error("Invalid client secret")]
76    InvalidClientSecret,
77
78    #[error("Authorization code expired")]
79    ExpiredCode,
80
81    #[error("Server error: {message}")]
82    ServerError { message: String },
83
84    #[error("Invalid target resource: {message}")]
85    InvalidTarget { message: String },
86
87    #[error("Invalid scope: {message}")]
88    InvalidScope { message: String },
89}
90
91impl From<TokenError> for OAuthHttpError {
92    fn from(error: TokenError) -> Self {
93        match error {
94            TokenError::InvalidRequest { field, message } => {
95                Self::invalid_request(format!("{field}: {message}"))
96            },
97            TokenError::UnsupportedGrantType { grant_type } => {
98                Self::unsupported_grant_type(format!("Grant type '{grant_type}' is not supported"))
99            },
100            TokenError::InvalidClient => Self::invalid_client("Client authentication failed"),
101            TokenError::InvalidGrant { reason } => Self::invalid_grant(reason),
102            TokenError::InvalidRefreshToken { reason } => {
103                Self::invalid_grant(format!("Refresh token invalid: {reason}"))
104            },
105            TokenError::InvalidCredentials => Self::invalid_grant("Invalid credentials"),
106            TokenError::InvalidClientSecret => Self::invalid_client("Invalid client secret"),
107            TokenError::ExpiredCode => Self::invalid_grant("Authorization code expired"),
108            TokenError::ServerError { message } => Self::server_error(message),
109            TokenError::InvalidTarget { message } => Self::invalid_target(message),
110            TokenError::InvalidScope { message } => Self::invalid_scope(message),
111        }
112    }
113}