use crate::config::Config; use crate::errors::ApiError; use chrono::{Duration, Utc}; use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation}; use rocket::response::status::Custom; use rocket::{http::SameSite, request::local_cache}; use rocket::{ http::Status, serde::{json::Json, Deserialize, Serialize}, }; use rocket::{ http::{Cookie, CookieJar}, outcome::Outcome, }; use rocket::{ outcome::IntoOutcome, request::{self, FromRequest, Request}, }; use rocket::{routes, Route, State}; use substring::Substring; #[derive(Clone, Debug)] pub(crate) struct User { username: String, //TODO more state and such here. } fn decode_token(token: &str, config: &Config) -> Result { let token_data = decode::( token, &DecodingKey::from_secret(config.jwt_secret.as_bytes()), &Validation::default(), )?; Ok(token_data.claims) } #[rocket::async_trait] impl<'r> FromRequest<'r> for User { type Error = ApiError; async fn from_request(req: &'r Request<'_>) -> request::Outcome { let config: Option<&Config> = req.rocket().state(); let auth_header = req .headers() .get_one("Authorization") .map(|auth| auth.substring("Bearer ".len(), auth.len())); let token = auth_header .zip(config) .map(|(encoded_token, app_cfg)| decode_token(encoded_token, app_cfg)) .unwrap_or(Err(ApiError::AuthenticationDenied("username".to_string()))); match token { Err(e) => Outcome::Failure((Status::Forbidden, e)), Ok(token) => Outcome::Success(User { username: token.sub, }), } } } pub(crate) fn routes() -> Vec { routes![login, refresh_token] } #[derive(Debug, Serialize, Deserialize)] struct Claims { exp: usize, sub: String, } #[derive(Deserialize)] struct LoginRequest<'a> { username: &'a str, password: &'a str, } fn create_token<'a>( username: &str, expiration: Duration, secret: &str, ) -> Result { let expiration = Utc::now() .checked_add_signed(expiration) .expect("clock went awry") .timestamp(); let claims = Claims { exp: expiration as usize, sub: username.to_owned(), }; let token = encode( &Header::default(), &claims, &EncodingKey::from_secret(secret.as_bytes()), )?; Ok(token) } #[derive(Serialize)] struct LoginResponse { jwt_token: String, } /// A strongly-typed representation of the refresh token, used with a /// FromRequest trait to decode it from the cookie. struct RefreshToken(String); #[rocket::async_trait] impl<'r> FromRequest<'r> for RefreshToken { type Error = (); async fn from_request(request: &'r Request<'_>) -> request::Outcome { let token: Option = request .cookies() .get_private("refresh_token") .and_then(|cookie| cookie.value().parse::().ok()) .map(|t| RefreshToken(t)); token.or_forward(()) } } #[rocket::post("/login", data = "")] async fn login( request: Json>, config: &State, cookies: &CookieJar<'_>, ) -> Result, ApiError> { let token = create_token(request.username, Duration::minutes(1), &config.jwt_secret)?; let refresh_token = create_token(request.username, Duration::weeks(1), &config.jwt_secret)?; let mut cookie = Cookie::new("refresh_token", refresh_token); cookie.set_same_site(SameSite::None); cookies.add_private(cookie); Ok(Json(LoginResponse { jwt_token: token })) } #[rocket::post("/refresh")] async fn refresh_token( config: &State, refresh_token: Option, ) -> Result, ApiError> { let refresh_token = refresh_token.ok_or(ApiError::RefreshTokenMissing)?; let refresh_token = decode_token(&refresh_token.0, config)?; //TODO check if token is valid? maybe decode takes care of it. let token = create_token(&refresh_token.sub, Duration::minutes(1), &config.jwt_secret)?; Ok(Json(LoginResponse { jwt_token: token })) }