1use std::collections::HashMap;
77use std::sync::{Arc, Mutex};
78use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
79
80use sha2::{Digest, Sha256};
81
82use crate::application::Application;
83use crate::body::form_urlencoded::FormUrlEncoded;
84use crate::core::New;
85use crate::extract::{FromRequest, Query};
86use crate::header::Header;
87use crate::middleware::Middleware;
88use crate::mime_type::MimeType;
89use crate::range::Range;
90use crate::request::{Request, METHOD};
91use crate::response::{Response, StatusCodeReasonPhrase, STATUS_CODE_REASON_PHRASE};
92use crate::server::ConnectionInfo;
93use crate::session::{session_id_from_request, SessionStore};
94
95use super::client_store::{ClientStore, GrantType};
96use super::pkce::base64url_encode;
97
98const TOKEN_PATH: &str = "/oauth/token";
99const AUTHORIZE_PATH: &str = "/oauth/authorize";
100const DISCOVERY_PATH: &str = "/.well-known/openid-configuration";
101const JWKS_PATH: &str = "/.well-known/jwks.json";
102const SESSION_COOKIE: &str = "_rws_authz_sid";
103const AUTH_CODE_TTL_SECS: u64 = 60;
104
105pub struct AuthServerConfig {
107 pub issuer: String,
110 pub signing_secret: String,
113 pub access_token_ttl: Duration,
115 pub refresh_token_ttl: Duration,
117 pub clients: ClientStore,
119 pub sessions: Arc<SessionStore>,
122}
123
124struct AuthCodeRecord {
125 client_id: String,
126 redirect_uri: String,
127 scope: String,
128 subject: String,
129 code_challenge: Option<String>,
130 expires_at: Instant,
131}
132
133#[derive(Clone)]
134struct RefreshTokenRecord {
135 client_id: String,
136 subject: String,
137 scope: String,
138 expires_at: Instant,
139}
140
141pub struct AuthServer {
143 config: Arc<AuthServerConfig>,
144 login_url: String,
145 subject_session_key: String,
146 codes: Mutex<HashMap<String, AuthCodeRecord>>,
147 refresh_tokens: Mutex<HashMap<String, RefreshTokenRecord>>,
148}
149
150impl AuthServer {
151 pub fn new(config: AuthServerConfig) -> Self {
155 AuthServer {
156 config: Arc::new(config),
157 login_url: "/login".to_string(),
158 subject_session_key: "user_id".to_string(),
159 codes: Mutex::new(HashMap::new()),
160 refresh_tokens: Mutex::new(HashMap::new()),
161 }
162 }
163
164 pub fn login_url(mut self, url: &str) -> Self {
167 self.login_url = url.to_string();
168 self
169 }
170
171 pub fn subject_session_key(mut self, key: &str) -> Self {
174 self.subject_session_key = key.to_string();
175 self
176 }
177
178 fn handle_authorize(&self, request: &Request) -> Response {
181 let query = match Query::from_request(request) {
182 Ok(q) => q,
183 Err(_) => return Self::oauth_error(STATUS_CODE_REASON_PHRASE.n400_bad_request, "invalid_request", "malformed query string"),
184 };
185
186 if query.get("response_type").map(String::as_str) != Some("code") {
187 return Self::oauth_error(STATUS_CODE_REASON_PHRASE.n400_bad_request, "unsupported_response_type", "only response_type=code is supported");
188 }
189 let client_id = match query.get("client_id") {
190 Some(c) => c.clone(),
191 None => return Self::oauth_error(STATUS_CODE_REASON_PHRASE.n400_bad_request, "invalid_request", "missing client_id"),
192 };
193 let redirect_uri = match query.get("redirect_uri") {
194 Some(r) => r.clone(),
195 None => return Self::oauth_error(STATUS_CODE_REASON_PHRASE.n400_bad_request, "invalid_request", "missing redirect_uri"),
196 };
197 let client = match self.config.clients.get(&client_id) {
198 Some(c) => c,
199 None => return Self::oauth_error(STATUS_CODE_REASON_PHRASE.n400_bad_request, "invalid_client", "unknown client_id"),
200 };
201 if !client.grants.contains(&GrantType::AuthorizationCode) {
202 return Self::oauth_error(STATUS_CODE_REASON_PHRASE.n400_bad_request, "unauthorized_client", "client is not authorized for the authorization_code grant");
203 }
204 if !client.redirect_uris.iter().any(|u| u == &redirect_uri) {
205 return Self::oauth_error(STATUS_CODE_REASON_PHRASE.n400_bad_request, "invalid_request", "redirect_uri is not registered for this client");
206 }
207 let code_challenge = query.get("code_challenge").cloned();
208 if let Some(method) = query.get("code_challenge_method") {
209 if method != "S256" {
210 return Self::oauth_error(STATUS_CODE_REASON_PHRASE.n400_bad_request, "invalid_request", "only code_challenge_method=S256 is supported");
211 }
212 }
213 let scope = query.get("scope").cloned().unwrap_or_default();
214 let state = query.get("state").cloned();
215
216 let subject = session_id_from_request(request, SESSION_COOKIE)
217 .and_then(|sid| self.config.sessions.load(&sid))
218 .and_then(|s| s.get(&self.subject_session_key).map(|v| v.to_string()));
219
220 let subject = match subject {
221 Some(s) => s,
222 None => {
223 let return_to = super::client::url_encode(&request.request_uri);
224 return Self::redirect(&format!("{}?return_to={}", self.login_url, return_to));
225 }
226 };
227
228 let code = Self::random_token();
229 self.codes.lock().unwrap().insert(
230 code.clone(),
231 AuthCodeRecord {
232 client_id,
233 redirect_uri: redirect_uri.clone(),
234 scope,
235 subject,
236 code_challenge,
237 expires_at: Instant::now() + Duration::from_secs(AUTH_CODE_TTL_SECS),
238 },
239 );
240
241 let mut location = format!("{redirect_uri}?code={code}");
242 if let Some(s) = state {
243 location.push_str(&format!("&state={}", super::client::url_encode(&s)));
244 }
245 Self::redirect(&location)
246 }
247
248 fn handle_token(&self, request: &Request) -> Response {
251 let form = match FormUrlEncoded::parse(request.body.to_vec()) {
252 Ok(f) => f,
253 Err(_) => return Self::oauth_error(STATUS_CODE_REASON_PHRASE.n400_bad_request, "invalid_request", "malformed request body"),
254 };
255 match form.get("grant_type").map(String::as_str) {
256 Some("client_credentials") => self.handle_client_credentials(&form),
257 Some("authorization_code") => self.handle_authorization_code(&form),
258 Some("refresh_token") => self.handle_refresh_token(&form),
259 _ => Self::oauth_error(
260 STATUS_CODE_REASON_PHRASE.n400_bad_request,
261 "unsupported_grant_type",
262 "grant_type must be client_credentials, authorization_code, or refresh_token",
263 ),
264 }
265 }
266
267 fn handle_client_credentials(&self, form: &HashMap<String, String>) -> Response {
268 let client_id = form.get("client_id").cloned().unwrap_or_default();
269 let client_secret = form.get("client_secret").cloned().unwrap_or_default();
270 let client = match self.config.clients.get(&client_id) {
271 Some(c) => c,
272 None => return Self::oauth_error(STATUS_CODE_REASON_PHRASE.n401_unauthorized, "invalid_client", "unknown client_id"),
273 };
274 if client.client_secret.as_deref() != Some(client_secret.as_str()) {
275 return Self::oauth_error(STATUS_CODE_REASON_PHRASE.n401_unauthorized, "invalid_client", "client authentication failed");
276 }
277 if !client.grants.contains(&GrantType::ClientCredentials) {
278 return Self::oauth_error(STATUS_CODE_REASON_PHRASE.n400_bad_request, "unauthorized_client", "client is not authorized for the client_credentials grant");
279 }
280 let scope = match Self::negotiate_scope(form.get("scope").map(String::as_str), &client.scopes) {
281 Some(s) => s,
282 None => return Self::oauth_error(STATUS_CODE_REASON_PHRASE.n400_bad_request, "invalid_scope", "requested scope exceeds client's authorized scopes"),
283 };
284
285 let access_token = self.issue_jwt(&client_id, &client_id, &scope);
286 Self::token_response(&access_token, self.config.access_token_ttl, None, None)
287 }
288
289 fn handle_authorization_code(&self, form: &HashMap<String, String>) -> Response {
290 let code = match form.get("code") {
291 Some(c) => c.clone(),
292 None => return Self::oauth_error(STATUS_CODE_REASON_PHRASE.n400_bad_request, "invalid_request", "missing code"),
293 };
294 let redirect_uri = form.get("redirect_uri").cloned().unwrap_or_default();
295 let client_id = form.get("client_id").cloned().unwrap_or_default();
296
297 let record = self.codes.lock().unwrap().remove(&code);
298 let record = match record {
299 Some(r) if r.expires_at > Instant::now() => r,
300 _ => return Self::oauth_error(STATUS_CODE_REASON_PHRASE.n400_bad_request, "invalid_grant", "code is invalid, expired, or already used"),
301 };
302 if record.client_id != client_id || record.redirect_uri != redirect_uri {
303 return Self::oauth_error(STATUS_CODE_REASON_PHRASE.n400_bad_request, "invalid_grant", "client_id or redirect_uri does not match the original request");
304 }
305 let client = match self.config.clients.get(&client_id) {
306 Some(c) => c,
307 None => return Self::oauth_error(STATUS_CODE_REASON_PHRASE.n400_bad_request, "invalid_client", "unknown client_id"),
308 };
309 if let Some(secret) = &client.client_secret {
310 if form.get("client_secret").map(String::as_str) != Some(secret.as_str()) {
311 return Self::oauth_error(STATUS_CODE_REASON_PHRASE.n401_unauthorized, "invalid_client", "client authentication failed");
312 }
313 }
314 if let Some(challenge) = &record.code_challenge {
315 let verifier = form.get("code_verifier").map(String::as_str).unwrap_or("");
316 let computed = base64url_encode(&Sha256::digest(verifier.as_bytes()));
317 if &computed != challenge {
318 return Self::oauth_error(STATUS_CODE_REASON_PHRASE.n400_bad_request, "invalid_grant", "code_verifier does not match code_challenge");
319 }
320 }
321
322 let token = self.issue_jwt(&record.subject, &client_id, &record.scope);
326 let refresh_token = Self::random_token();
327 self.refresh_tokens.lock().unwrap().insert(
328 refresh_token.clone(),
329 RefreshTokenRecord {
330 client_id,
331 subject: record.subject,
332 scope: record.scope,
333 expires_at: Instant::now() + self.config.refresh_token_ttl,
334 },
335 );
336 Self::token_response(&token, self.config.access_token_ttl, Some(&refresh_token), Some(&token))
337 }
338
339 fn handle_refresh_token(&self, form: &HashMap<String, String>) -> Response {
340 let token = match form.get("refresh_token") {
341 Some(t) => t.clone(),
342 None => return Self::oauth_error(STATUS_CODE_REASON_PHRASE.n400_bad_request, "invalid_request", "missing refresh_token"),
343 };
344 let record = self.refresh_tokens.lock().unwrap().get(&token).cloned();
345 let record = match record {
346 Some(r) if r.expires_at > Instant::now() => r,
347 _ => return Self::oauth_error(STATUS_CODE_REASON_PHRASE.n400_bad_request, "invalid_grant", "refresh_token is invalid or expired"),
348 };
349 let access_token = self.issue_jwt(&record.subject, &record.client_id, &record.scope);
350 Self::token_response(&access_token, self.config.access_token_ttl, None, None)
351 }
352
353 fn negotiate_scope(requested: Option<&str>, allowed: &[String]) -> Option<String> {
354 match requested {
355 None => Some(allowed.join(" ")),
356 Some(r) if r.is_empty() => Some(allowed.join(" ")),
357 Some(r) => {
358 let tokens: Vec<&str> = r.split_whitespace().collect();
359 if tokens.iter().all(|t| allowed.iter().any(|a| a == t)) {
360 Some(tokens.join(" "))
361 } else {
362 None
363 }
364 }
365 }
366 }
367
368 fn issue_jwt(&self, sub: &str, aud: &str, scope: &str) -> String {
369 let now = unix_now();
370 let exp = now + self.config.access_token_ttl.as_secs();
371 let claims = format!(
372 r#"{{"sub":"{}","iss":"{}","aud":"{}","exp":{},"iat":{},"scope":"{}"}}"#,
373 json_escape(sub),
374 json_escape(&self.config.issuer),
375 json_escape(aud),
376 exp,
377 now,
378 json_escape(scope),
379 );
380 crate::auth::build_jwt(&claims, self.config.signing_secret.as_bytes())
381 }
382
383 fn discovery_document(&self) -> Response {
386 let issuer = &self.config.issuer;
387 let body = format!(
388 r#"{{"issuer":"{issuer}","authorization_endpoint":"{issuer}{AUTHORIZE_PATH}","token_endpoint":"{issuer}{TOKEN_PATH}","jwks_uri":"{issuer}{JWKS_PATH}","response_types_supported":["code"],"grant_types_supported":["authorization_code","client_credentials","refresh_token"]}}"#
389 );
390 Self::json_response(STATUS_CODE_REASON_PHRASE.n200_ok, &body)
391 }
392
393 fn jwks_document(&self) -> Response {
394 Self::json_response(STATUS_CODE_REASON_PHRASE.n200_ok, r#"{"keys":[]}"#)
397 }
398
399 fn random_token() -> String {
402 use rand_core::{OsRng, RngCore};
403 let mut bytes = [0u8; 24];
404 OsRng.fill_bytes(&mut bytes);
405 bytes.iter().map(|b| format!("{:02x}", b)).collect()
406 }
407
408 fn redirect(url: &str) -> Response {
409 let mut r = Response::new();
410 r.status_code = *STATUS_CODE_REASON_PHRASE.n302_found.status_code;
411 r.reason_phrase = STATUS_CODE_REASON_PHRASE.n302_found.reason_phrase.to_string();
412 r.headers.push(Header { name: "Location".into(), value: url.to_string().into() });
413 r
414 }
415
416 fn json_response(status: &'static StatusCodeReasonPhrase, body: &str) -> Response {
417 let mut r = Response::new();
418 r.status_code = *status.status_code;
419 r.reason_phrase = status.reason_phrase.to_string();
420 r.content_range_list = vec![Range::get_content_range(body.as_bytes().to_vec(), MimeType::APPLICATION_JSON.to_string())];
421 r
422 }
423
424 fn oauth_error(status: &'static StatusCodeReasonPhrase, error: &str, description: &str) -> Response {
425 let body = format!(r#"{{"error":"{error}","error_description":"{}"}}"#, json_escape(description));
426 Self::json_response(status, &body)
427 }
428
429 fn token_response(access_token: &str, ttl: Duration, refresh_token: Option<&str>, id_token: Option<&str>) -> Response {
430 let mut body = format!(
431 r#"{{"access_token":"{}","token_type":"Bearer","expires_in":{}"#,
432 access_token,
433 ttl.as_secs(),
434 );
435 if let Some(rt) = refresh_token {
436 body.push_str(&format!(r#","refresh_token":"{rt}""#));
437 }
438 if let Some(idt) = id_token {
439 body.push_str(&format!(r#","id_token":"{idt}""#));
440 }
441 body.push('}');
442 Self::json_response(STATUS_CODE_REASON_PHRASE.n200_ok, &body)
443 }
444}
445
446impl Middleware for AuthServer {
447 fn handle(&self, request: &Request, connection: &ConnectionInfo, next: &dyn Application) -> Result<Response, String> {
448 let path = request.request_uri.split('?').next().unwrap_or("");
449 let is_get = request.method.eq_ignore_ascii_case(METHOD.get);
450 let is_post = request.method.eq_ignore_ascii_case(METHOD.post);
451
452 if path == AUTHORIZE_PATH && is_get {
453 return Ok(self.handle_authorize(request));
454 }
455 if path == TOKEN_PATH && is_post {
456 return Ok(self.handle_token(request));
457 }
458 if path == DISCOVERY_PATH && is_get {
459 return Ok(self.discovery_document());
460 }
461 if path == JWKS_PATH && is_get {
462 return Ok(self.jwks_document());
463 }
464
465 next.execute(request, connection)
466 }
467}
468
469fn unix_now() -> u64 {
470 SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
471}
472
473fn json_escape(s: &str) -> String {
474 crate::core::json::escape(s)
475}