From eb1e2990f9327f57e59304ff1d3bb290c25dbbdb Mon Sep 17 00:00:00 2001 From: phoenix Date: Mon, 11 Aug 2025 22:15:17 +0000 Subject: [PATCH] tsk-51: Refresh token endpoint (#54) Closes #51 Reviewed-on: https://git.kundeng.us/phoenix/icarus_auth/pulls/54 Co-authored-by: phoenix Co-committed-by: phoenix --- Cargo.lock | 6 +- Cargo.toml | 4 +- migrations/20250802185652_passphrase_data.sql | 2 +- src/callers/login.rs | 94 ++++++++++++++++++- src/callers/mod.rs | 1 + src/main.rs | 71 ++++++++++++++ src/repo/mod.rs | 24 +++++ src/token_stuff/mod.rs | 89 ++++++++++++++++-- 8 files changed, 273 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ba72c60..c5b54f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -728,7 +728,7 @@ dependencies = [ [[package]] name = "icarus_auth" -version = "0.4.2" +version = "0.4.3" dependencies = [ "argon2", "axum", @@ -761,8 +761,8 @@ dependencies = [ [[package]] name = "icarus_models" -version = "0.5.4" -source = "git+ssh://git@git.kundeng.us/phoenix/icarus_models.git?tag=v0.5.4-devel-1e95822b5a-111#1e95822b5a349bd73cc501d921052f289105ec55" +version = "0.5.5" +source = "git+ssh://git@git.kundeng.us/phoenix/icarus_models.git?tag=v0.5.5-devel-bd793db08e-111#bd793db08e06b256ffecd9f4528e55e3026fede7" dependencies = [ "josekit", "rand 0.9.1", diff --git a/Cargo.toml b/Cargo.toml index caa4c42..8d25e4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "icarus_auth" -version = "0.4.2" +version = "0.4.3" edition = "2024" rust-version = "1.88" @@ -18,7 +18,7 @@ argon2 = { version = "0.5.3", features = ["std"] } # Use the latest 0.5.x versio rand = { version = "0.9.1" } time = { version = "0.3.41", features = ["macros", "serde"] } josekit = { version = "0.10.3" } -icarus_models = { git = "ssh://git@git.kundeng.us/phoenix/icarus_models.git", tag = "v0.5.4-devel-1e95822b5a-111" } +icarus_models = { git = "ssh://git@git.kundeng.us/phoenix/icarus_models.git", tag = "v0.5.5-devel-bd793db08e-111" } icarus_envy = { git = "ssh://git@git.kundeng.us/phoenix/icarus_envy.git", tag = "v0.3.1-main-3cd42dab6b-006" } [dev-dependencies] diff --git a/migrations/20250802185652_passphrase_data.sql b/migrations/20250802185652_passphrase_data.sql index e18d2a1..c1edf34 100644 --- a/migrations/20250802185652_passphrase_data.sql +++ b/migrations/20250802185652_passphrase_data.sql @@ -1,2 +1,2 @@ -- Add migration script here -INSERT INTO "passphrase" (passphrase) VALUES('iUOo1fxshf3y1tUGn1yU8l9raPApHCdinW0VdCHdRFEjqhR3Bf02aZzsKbLtaDFH'); +INSERT INTO "passphrase" (id, passphrase) VALUES('22f9c775-cce9-457a-a147-9dafbb801f61', 'iUOo1fxshf3y1tUGn1yU8l9raPApHCdinW0VdCHdRFEjqhR3Bf02aZzsKbLtaDFH'); diff --git a/src/callers/login.rs b/src/callers/login.rs index 6d5fd20..fb065e5 100644 --- a/src/callers/login.rs +++ b/src/callers/login.rs @@ -13,6 +13,13 @@ pub mod request { pub passphrase: String, } } + + pub mod refresh_token { + #[derive(Debug, serde::Deserialize, serde::Serialize)] + pub struct Request { + pub access_token: String, + } + } } pub mod response { @@ -31,6 +38,14 @@ pub mod response { pub data: Vec, } } + + pub mod refresh_token { + #[derive(Debug, Default, serde::Deserialize, serde::Serialize)] + pub struct Response { + pub message: String, + pub data: Vec, + } + } } pub mod endpoint { @@ -43,6 +58,10 @@ pub mod endpoint { use super::request; use super::response; + // TODO: At some point, get the username from the DB + // Name of service username when returning a login result + pub const SERVICE_USERNAME: &str = "service"; + async fn not_found(message: &str) -> (StatusCode, Json) { ( StatusCode::NOT_FOUND, @@ -63,7 +82,8 @@ pub mod endpoint { if hashing::verify_password(&payload.password, user.password.clone()).unwrap() { // Create token let key = icarus_envy::environment::get_secret_key().await; - let (token_literal, duration) = token_stuff::create_token(&key).unwrap(); + let (token_literal, duration) = + token_stuff::create_token(&key, &user.id).unwrap(); if token_stuff::verify_token(&key, &token_literal) { let current_time = time::OffsetDateTime::now_utc(); @@ -107,12 +127,13 @@ pub mod endpoint { match repo::service::valid_passphrase(&pool, &payload.passphrase).await { Ok((id, _passphrase, _date_created)) => { let key = icarus_envy::environment::get_secret_key().await; - let (token_literal, duration) = token_stuff::create_service_token(&key).unwrap(); + let (token_literal, duration) = + token_stuff::create_service_token(&key, &id).unwrap(); if token_stuff::verify_token(&key, &token_literal) { let login_result = icarus_models::login_result::LoginResult { id, - username: String::from("service"), + username: String::from(SERVICE_USERNAME), token: token_literal, token_type: String::from(icarus_models::token::TOKEN_TYPE), expiration: duration, @@ -132,4 +153,71 @@ pub mod endpoint { } } } + + pub async fn refresh_token( + axum::Extension(pool): axum::Extension, + axum::Json(payload): axum::Json, + ) -> ( + axum::http::StatusCode, + axum::Json, + ) { + let mut response = response::refresh_token::Response::default(); + let key = icarus_envy::environment::get_secret_key().await; + + if token_stuff::verify_token(&key, &payload.access_token) { + let token_type = token_stuff::get_token_type(&key, &payload.access_token).unwrap(); + + if token_stuff::is_token_type_valid(&token_type) { + // Get passphrase record with id + match token_stuff::extract_id_from_token(&key, &payload.access_token) { + Ok(id) => match repo::service::get_passphrase(&pool, &id).await { + Ok((returned_id, _, _)) => { + match token_stuff::create_service_refresh_token(&key, &returned_id) { + Ok((access_token, exp_dur)) => { + let login_result = icarus_models::login_result::LoginResult { + id: returned_id, + token: access_token, + expiration: exp_dur, + token_type: String::from(icarus_models::token::TOKEN_TYPE), + username: String::from(SERVICE_USERNAME), + }; + response.message = String::from("Successful"); + response.data.push(login_result); + + (axum::http::StatusCode::OK, axum::Json(response)) + } + Err(err) => { + response.message = err.to_string(); + ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response), + ) + } + } + } + Err(err) => { + response.message = err.to_string(); + ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response), + ) + } + }, + Err(err) => { + response.message = err.to_string(); + ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response), + ) + } + } + } else { + response.message = String::from("Invalid token type"); + (axum::http::StatusCode::NOT_FOUND, axum::Json(response)) + } + } else { + response.message = String::from("Could not verify token"); + (axum::http::StatusCode::BAD_REQUEST, axum::Json(response)) + } + } } diff --git a/src/callers/mod.rs b/src/callers/mod.rs index 4fdaf82..f280a4f 100644 --- a/src/callers/mod.rs +++ b/src/callers/mod.rs @@ -8,4 +8,5 @@ pub mod endpoints { pub const DBTEST: &str = "/api/v2/test/db"; pub const LOGIN: &str = "/api/v2/login"; pub const SERVICE_LOGIN: &str = "/api/v2/service/login"; + pub const REFRESH_TOKEN: &str = "/api/v2/token/refresh"; } diff --git a/src/main.rs b/src/main.rs index 5d8a4ab..e052521 100644 --- a/src/main.rs +++ b/src/main.rs @@ -45,6 +45,10 @@ mod init { callers::endpoints::SERVICE_LOGIN, post(callers::login::endpoint::service_login), ) + .route( + callers::endpoints::REFRESH_TOKEN, + post(callers::login::endpoint::refresh_token), + ) } pub async fn app() -> Router { @@ -397,4 +401,71 @@ mod tests { let _ = db_mgr::drop_database(&tm_pool, &db_name).await; } + + #[tokio::test] + async fn test_refresh_token() { + let tm_pool = db_mgr::get_pool().await.unwrap(); + + let db_name = db_mgr::generate_db_name().await; + + match db_mgr::create_database(&tm_pool, &db_name).await { + Ok(_) => { + println!("Success"); + } + Err(e) => { + assert!(false, "Error: {:?}", e.to_string()); + } + } + + let pool = db_mgr::connect_to_db(&db_name).await.unwrap(); + + icarus_auth::db::migrations(&pool).await; + + let app = init::routes().await.layer(axum::Extension(pool)); + let id = uuid::Uuid::parse_str("22f9c775-cce9-457a-a147-9dafbb801f61").unwrap(); + let key = icarus_envy::environment::get_secret_key().await; + + match icarus_auth::token_stuff::create_service_token(&key, &id) { + Ok((token, _expire)) => { + let payload = serde_json::json!({ + "access_token": token + }); + + match app + .oneshot( + Request::builder() + .method(axum::http::Method::POST) + .uri(callers::endpoints::REFRESH_TOKEN) + .header(axum::http::header::CONTENT_TYPE, "application/json") + .body(Body::from(payload.to_string())) + .unwrap(), + ) + .await + { + Ok(response) => { + let body = axum::body::to_bytes(response.into_body(), usize::MAX) + .await + .unwrap(); + let parsed_body: callers::login::response::service_login::Response = + serde_json::from_slice(&body).unwrap(); + let login_result = &parsed_body.data[0]; + + assert_eq!( + id, login_result.id, + "The Id from the response does not match {id:?} {:?}", + login_result.id + ); + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + + let _ = db_mgr::drop_database(&tm_pool, &db_name).await; + } } diff --git a/src/repo/mod.rs b/src/repo/mod.rs index db62bb4..037ee0f 100644 --- a/src/repo/mod.rs +++ b/src/repo/mod.rs @@ -223,4 +223,28 @@ pub mod service { Err(err) => Err(err), } } + + pub async fn get_passphrase( + pool: &sqlx::PgPool, + id: &uuid::Uuid, + ) -> Result<(uuid::Uuid, String, time::OffsetDateTime), sqlx::Error> { + let result = sqlx::query( + r#" + SELECT * FROM "passphrase" WHERE id = $1; + "#, + ) + .bind(id) + .fetch_one(pool) + .await; + + match result { + Ok(row) => { + let returned_id: uuid::Uuid = row.try_get("id")?; + let passphrase: String = row.try_get("passphrase")?; + let date_created: time::OffsetDateTime = row.try_get("date_created")?; + Ok((returned_id, passphrase, date_created)) + } + Err(err) => Err(err), + } + } } diff --git a/src/token_stuff/mod.rs b/src/token_stuff/mod.rs index 241216a..64e4429 100644 --- a/src/token_stuff/mod.rs +++ b/src/token_stuff/mod.rs @@ -20,33 +20,103 @@ pub fn get_expiration(issued: &time::OffsetDateTime) -> Result Result<(String, i64), josekit::JoseError> { +pub fn create_token( + provided_key: &String, + id: &uuid::Uuid, +) -> Result<(String, i64), josekit::JoseError> { let resource = icarus_models::token::TokenResource { message: String::from(MESSAGE), issuer: String::from(ISSUER), audiences: vec![String::from(AUDIENCE)], + id: *id, }; icarus_models::token::create_token(provided_key, &resource, time::Duration::hours(4)) } -pub fn create_service_token(provided: &String) -> Result<(String, i64), josekit::JoseError> { +pub fn create_service_token( + provided: &String, + id: &uuid::Uuid, +) -> Result<(String, i64), josekit::JoseError> { let resource = icarus_models::token::TokenResource { - message: String::from("Service random"), + message: String::from(SERVICE_SUBJECT), issuer: String::from(ISSUER), audiences: vec![String::from(AUDIENCE)], + id: *id, }; icarus_models::token::create_token(provided, &resource, time::Duration::hours(1)) } +pub fn create_service_refresh_token( + key: &String, + id: &uuid::Uuid, +) -> Result<(String, i64), josekit::JoseError> { + let resource = icarus_models::token::TokenResource { + message: String::from(SERVICE_SUBJECT), + issuer: String::from(ISSUER), + audiences: vec![String::from(AUDIENCE)], + id: *id, + }; + icarus_models::token::create_token(key, &resource, time::Duration::hours(4)) +} + pub fn verify_token(key: &String, token: &String) -> bool { - let ver = Hs256.verifier_from_bytes(key.as_bytes()).unwrap(); - let (payload, _header) = jwt::decode_with_verifier(token, &ver).unwrap(); - match payload.subject() { - Some(_sub) => true, - None => false, + match get_payload(key, token) { + Ok((payload, _header)) => match payload.subject() { + Some(_sub) => true, + None => false, + }, + Err(_err) => false, } } +pub fn extract_id_from_token(key: &String, token: &String) -> Result { + match get_payload(key, token) { + Ok((payload, _header)) => match payload.claim("id") { + Some(id) => match uuid::Uuid::parse_str(id.as_str().unwrap()) { + Ok(extracted) => Ok(extracted), + Err(err) => Err(std::io::Error::other(err.to_string())), + }, + None => Err(std::io::Error::other("No claim found")), + }, + Err(err) => Err(std::io::Error::other(err.to_string())), + } +} + +pub const APP_TOKEN_TYPE: &str = "Icarus_App"; +pub const APP_SUBJECT: &str = "Something random"; +pub const SERVICE_TOKEN_TYPE: &str = "Icarus_Service"; +pub const SERVICE_SUBJECT: &str = "Service random"; + +pub fn get_token_type(key: &String, token: &String) -> Result { + match get_payload(key, token) { + Ok((payload, _header)) => match payload.subject() { + Some(subject) => { + if subject == APP_SUBJECT { + Ok(String::from(APP_TOKEN_TYPE)) + } else if subject == SERVICE_SUBJECT { + Ok(String::from(SERVICE_TOKEN_TYPE)) + } else { + Err(std::io::Error::other(String::from("Invalid subject"))) + } + } + None => Err(std::io::Error::other(String::from("Invalid payload"))), + }, + Err(err) => Err(std::io::Error::other(err.to_string())), + } +} + +pub fn is_token_type_valid(token_type: &String) -> bool { + token_type == SERVICE_TOKEN_TYPE +} + +fn get_payload( + key: &String, + token: &String, +) -> Result<(josekit::jwt::JwtPayload, josekit::jws::JwsHeader), josekit::JoseError> { + let ver = Hs256.verifier_from_bytes(key.as_bytes()).unwrap(); + jwt::decode_with_verifier(token, &ver) +} + #[cfg(test)] mod tests { use super::*; @@ -55,7 +125,8 @@ mod tests { fn test_tokenize() { let rt = tokio::runtime::Runtime::new().unwrap(); let special_key = rt.block_on(icarus_envy::environment::get_secret_key()); - match create_token(&special_key) { + let id = uuid::Uuid::new_v4(); + match create_token(&special_key, &id) { Ok((token, _duration)) => { let result = verify_token(&special_key, &token); assert!(result, "Token not verified");