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//!
8//! Copyright (c) systemprompt.io — Business Source License 1.1.
9//! See <https://systemprompt.io> for licensing details.
10
11pub mod generation;
12mod handler;
13pub mod validation;
14
15pub use handler::handle_token;
16
17#[cfg(feature = "test-api")]
18pub use handler::test_api as handler_test_api;
19
20use serde::{Deserialize, Serialize};
21
22use crate::routes::oauth::OAuthHttpError;
23
24pub type TokenResult<T> = Result<T, TokenError>;
25
26#[derive(Debug, Deserialize)]
27pub struct TokenRequest {
28    pub grant_type: String,
29    pub code: Option<String>,
30    pub redirect_uri: Option<String>,
31    pub client_id: Option<String>,
32    pub client_secret: Option<String>,
33    pub refresh_token: Option<String>,
34    pub scope: Option<String>,
35    pub code_verifier: Option<String>,
36    pub resource: Option<String>,
37    pub plugin_id: Option<String>,
38    pub audience: Option<String>,
39    pub subject_token: Option<String>,
40    pub subject_token_type: Option<String>,
41    pub actor_token: Option<String>,
42    pub actor_token_type: Option<String>,
43    pub requested_token_type: Option<String>,
44    pub assertion: Option<String>,
45}
46
47#[derive(Debug, Serialize)]
48pub struct TokenResponse {
49    pub access_token: String,
50    pub token_type: String,
51    pub expires_in: i64,
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub refresh_token: Option<String>,
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub scope: Option<String>,
56    // Why: RFC 8693 §2.2.1 issued_token_type. Only set by the
57    // urn:ietf:params:oauth:grant-type:token-exchange flow.
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub issued_token_type: Option<String>,
60}
61
62#[derive(Debug, thiserror::Error)]
63pub enum TokenError {
64    #[error("Invalid request: {field} {message}")]
65    InvalidRequest { field: String, message: String },
66
67    #[error("Unsupported grant type: {grant_type}")]
68    UnsupportedGrantType { grant_type: String },
69
70    #[error("Invalid client credentials")]
71    InvalidClient,
72
73    #[error("Invalid authorization code: {reason}")]
74    InvalidGrant { reason: String },
75
76    #[error("Invalid refresh token: {reason}")]
77    InvalidRefreshToken { reason: String },
78
79    #[error("Invalid credentials")]
80    InvalidCredentials,
81
82    #[error("Invalid client secret")]
83    InvalidClientSecret,
84
85    #[error("Authorization code expired")]
86    ExpiredCode,
87
88    #[error("Server error: {message}")]
89    ServerError { message: String },
90
91    #[error("Invalid target resource: {message}")]
92    InvalidTarget { message: String },
93
94    #[error("Invalid scope: {message}")]
95    InvalidScope { message: String },
96}
97
98impl From<TokenError> for OAuthHttpError {
99    fn from(error: TokenError) -> Self {
100        match error {
101            TokenError::InvalidRequest { field, message } => {
102                Self::invalid_request(format!("{field}: {message}"))
103            },
104            TokenError::UnsupportedGrantType { grant_type } => {
105                Self::unsupported_grant_type(format!("Grant type '{grant_type}' is not supported"))
106            },
107            TokenError::InvalidClient => Self::invalid_client("Client authentication failed"),
108            TokenError::InvalidGrant { reason } => Self::invalid_grant(reason),
109            TokenError::InvalidRefreshToken { reason } => {
110                Self::invalid_grant(format!("Refresh token invalid: {reason}"))
111            },
112            TokenError::InvalidCredentials => Self::invalid_grant("Invalid credentials"),
113            TokenError::InvalidClientSecret => Self::invalid_client("Invalid client secret"),
114            TokenError::ExpiredCode => Self::invalid_grant("Authorization code expired"),
115            TokenError::ServerError { message } => Self::server_error(message),
116            TokenError::InvalidTarget { message } => Self::invalid_target(message),
117            TokenError::InvalidScope { message } => Self::invalid_scope(message),
118        }
119    }
120}