1use crate::api::{ApiResponse, ApiState};
6use axum::{
7 Json,
8 extract::{Query, State},
9 http::{HeaderMap, StatusCode},
10 response::{IntoResponse, Redirect},
11};
12use serde::{Deserialize, Serialize};
13
14#[derive(Debug, Deserialize)]
16pub struct AuthorizeRequest {
17 pub response_type: String,
18 pub client_id: String,
19 pub redirect_uri: String,
20 pub scope: Option<String>,
21 pub state: Option<String>,
22 pub code_challenge: Option<String>,
23 pub code_challenge_method: Option<String>,
24}
25
26#[derive(Debug, Deserialize)]
28pub struct TokenRequest {
29 pub grant_type: String,
30 pub code: Option<String>,
31 pub client_id: String,
32 pub client_secret: Option<String>,
33 pub redirect_uri: Option<String>,
34 pub refresh_token: Option<String>,
35 pub code_verifier: Option<String>,
36}
37
38#[derive(Debug, Serialize)]
40pub struct TokenResponse {
41 pub access_token: String,
42 pub token_type: String,
43 pub expires_in: u64,
44 #[serde(skip_serializing_if = "Option::is_none")]
45 pub refresh_token: Option<String>,
46 #[serde(skip_serializing_if = "Option::is_none")]
47 pub scope: Option<String>,
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub id_token: Option<String>,
50}
51
52#[derive(Debug, Serialize)]
54pub struct OAuthError {
55 pub error: String,
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub error_description: Option<String>,
58 #[serde(skip_serializing_if = "Option::is_none")]
59 pub error_uri: Option<String>,
60 #[serde(skip_serializing_if = "Option::is_none")]
61 pub state: Option<String>,
62}
63
64#[derive(Debug, Serialize)]
66pub struct ClientInfo {
67 pub client_id: String,
68 pub name: String,
69 pub description: String,
70 pub redirect_uris: Vec<String>,
71 pub scopes: Vec<String>,
72}
73
74pub async fn authorize(
77 State(_state): State<ApiState>,
78 Query(params): Query<AuthorizeRequest>,
79) -> impl IntoResponse {
80 if params.response_type != "code" {
82 let error = OAuthError {
83 error: "unsupported_response_type".to_string(),
84 error_description: Some("Only 'code' response type is supported".to_string()),
85 error_uri: None,
86 state: params.state,
87 };
88 return (StatusCode::BAD_REQUEST, Json(error)).into_response();
89 }
90
91 if params.client_id.is_empty() {
92 let error = OAuthError {
93 error: "invalid_request".to_string(),
94 error_description: Some("client_id is required".to_string()),
95 error_uri: None,
96 state: params.state,
97 };
98 return (StatusCode::BAD_REQUEST, Json(error)).into_response();
99 }
100
101 if params.redirect_uri.is_empty() {
102 let error = OAuthError {
103 error: "invalid_request".to_string(),
104 error_description: Some("redirect_uri is required".to_string()),
105 error_uri: None,
106 state: params.state,
107 };
108 return (StatusCode::BAD_REQUEST, Json(error)).into_response();
109 }
110
111 let auth_code = format!("auth_code_{}", chrono::Utc::now().timestamp());
121 let mut redirect_url = params.redirect_uri;
122
123 redirect_url.push_str(&format!("?code={}", auth_code));
124 if let Some(state) = params.state {
125 redirect_url.push_str(&format!("&state={}", state));
126 }
127
128 tracing::info!("OAuth authorization for client: {}", params.client_id);
129 Redirect::to(&redirect_url).into_response()
130}
131
132pub async fn token(
135 State(state): State<ApiState>,
136 _headers: HeaderMap,
137 Json(req): Json<TokenRequest>,
138) -> ApiResponse<TokenResponse> {
139 match req.grant_type.as_str() {
141 "authorization_code" => handle_authorization_code_grant(state, req).await,
142 "refresh_token" => handle_refresh_token_grant(state, req).await,
143 "client_credentials" => handle_client_credentials_grant(state, req).await,
144 _ => ApiResponse::error_typed(
145 "unsupported_grant_type",
146 format!("Unsupported grant type: {}", req.grant_type),
147 ),
148 }
149}
150
151async fn handle_authorization_code_grant(
152 _state: ApiState,
153 req: TokenRequest,
154) -> ApiResponse<TokenResponse> {
155 if req.code.is_none() {
157 return ApiResponse::error_typed("invalid_request", "authorization code is required");
158 }
159
160 if req.redirect_uri.is_none() {
161 return ApiResponse::error_typed("invalid_request", "redirect_uri is required");
162 }
163
164 let response = TokenResponse {
172 access_token: format!("access_token_{}", chrono::Utc::now().timestamp()),
173 token_type: "Bearer".to_string(),
174 expires_in: 3600,
175 refresh_token: Some(format!("refresh_token_{}", chrono::Utc::now().timestamp())),
176 scope: Some("read write".to_string()),
177 id_token: None,
178 };
179
180 tracing::info!("Authorization code exchanged for client: {}", req.client_id);
181 ApiResponse::<TokenResponse>::success(response)
182}
183
184async fn handle_refresh_token_grant(
185 _state: ApiState,
186 req: TokenRequest,
187) -> ApiResponse<TokenResponse> {
188 if req.refresh_token.is_none() {
189 return ApiResponse::error_typed("invalid_request", "refresh_token is required");
190 }
191
192 let response = TokenResponse {
199 access_token: format!("new_access_token_{}", chrono::Utc::now().timestamp()),
200 token_type: "Bearer".to_string(),
201 expires_in: 3600,
202 refresh_token: req.refresh_token, scope: Some("read write".to_string()),
204 id_token: None,
205 };
206
207 tracing::info!("Refresh token used for client: {}", req.client_id);
208 ApiResponse::<TokenResponse>::success(response)
209}
210
211async fn handle_client_credentials_grant(
212 _state: ApiState,
213 req: TokenRequest,
214) -> ApiResponse<TokenResponse> {
215 let response = TokenResponse {
221 access_token: format!("client_access_token_{}", chrono::Utc::now().timestamp()),
222 token_type: "Bearer".to_string(),
223 expires_in: 7200, refresh_token: None, scope: Some("api:read api:write".to_string()),
226 id_token: None,
227 };
228
229 tracing::info!("Client credentials grant for client: {}", req.client_id);
230 ApiResponse::<TokenResponse>::success(response)
231}
232
233#[derive(Debug, Deserialize)]
236pub struct RevokeRequest {
237 pub token: String,
238 pub token_type_hint: Option<String>,
239}
240
241pub async fn revoke_token(
242 State(_state): State<ApiState>,
243 Json(req): Json<RevokeRequest>,
244) -> ApiResponse<()> {
245 if req.token.is_empty() {
246 return ApiResponse::validation_error_typed("token is required");
247 }
248
249 tracing::info!("Token revoked: {}", &req.token[..10]);
256 ApiResponse::<()>::ok_with_message("Token revoked successfully")
257}
258
259#[derive(Debug, Deserialize)]
262pub struct IntrospectRequest {
263 pub token: String,
264 pub token_type_hint: Option<String>,
265}
266
267#[derive(Debug, Serialize)]
268pub struct IntrospectResponse {
269 pub active: bool,
270 #[serde(skip_serializing_if = "Option::is_none")]
271 pub scope: Option<String>,
272 #[serde(skip_serializing_if = "Option::is_none")]
273 pub client_id: Option<String>,
274 #[serde(skip_serializing_if = "Option::is_none")]
275 pub username: Option<String>,
276 #[serde(skip_serializing_if = "Option::is_none")]
277 pub token_type: Option<String>,
278 #[serde(skip_serializing_if = "Option::is_none")]
279 pub exp: Option<u64>,
280 #[serde(skip_serializing_if = "Option::is_none")]
281 pub iat: Option<u64>,
282 #[serde(skip_serializing_if = "Option::is_none")]
283 pub sub: Option<String>,
284}
285
286pub async fn introspect_token(
287 State(_state): State<ApiState>,
288 Json(req): Json<IntrospectRequest>,
289) -> ApiResponse<IntrospectResponse> {
290 if req.token.is_empty() {
291 return ApiResponse::validation_error_typed("token is required");
292 }
293
294 let response = IntrospectResponse {
301 active: true, scope: Some("read write".to_string()),
303 client_id: Some("example_client".to_string()),
304 username: Some("user@example.com".to_string()),
305 token_type: Some("Bearer".to_string()),
306 exp: Some(chrono::Utc::now().timestamp() as u64 + 3600),
307 iat: Some(chrono::Utc::now().timestamp() as u64),
308 sub: Some("user_123".to_string()),
309 };
310
311 tracing::info!("Token introspected: {}", &req.token[..10]);
312 ApiResponse::<IntrospectResponse>::success(response)
313}
314
315pub async fn get_client_info(
318 State(_state): State<ApiState>,
319 axum::extract::Path(client_id): axum::extract::Path<String>,
320) -> ApiResponse<ClientInfo> {
321 let client = ClientInfo {
323 client_id: client_id.clone(),
324 name: format!("Client {}", client_id),
325 description: "OAuth 2.0 client application".to_string(),
326 redirect_uris: vec![
327 "https://example.com/callback".to_string(),
328 "https://app.example.com/auth/callback".to_string(),
329 ],
330 scopes: vec![
331 "read".to_string(),
332 "write".to_string(),
333 "profile".to_string(),
334 ],
335 };
336
337 ApiResponse::<ClientInfo>::success(client)
338}
339
340