From 95914653e0b5a9b04dbfdda24a528ee87f851898 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Fri, 14 Jul 2023 15:42:39 -0400 Subject: [PATCH] Rewrite auth endpoint to allow anonymous tokens, and better tokens --- Cargo.lock | 1 + Cargo.toml | 2 +- src/api/auth.rs | 244 ++++++++++++++++++++++++++++++++++------------- src/dto/scope.rs | 17 ++-- src/dto/user.rs | 47 ++++++++- 5 files changed, 235 insertions(+), 76 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0f6d76c..08c5d9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -321,6 +321,7 @@ dependencies = [ "js-sys", "num-integer", "num-traits", + "serde", "time", "wasm-bindgen", "winapi", diff --git a/Cargo.toml b/Cargo.toml index a16fa73..953a221 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ uuid = { version = "1.3.1", features = [ "v4", "fast-rng" ] } sqlx = { version = "0.6.3", features = [ "runtime-tokio-rustls", "sqlite" ] } bytes = "1.4.0" -chrono = "0.4.23" +chrono = { version = "0.4.23", features = [ "serde" ] } tokio = { version = "1.21.2", features = [ "fs", "macros" ] } tokio-util = { version = "0.7.7", features = [ "io" ] } diff --git a/src/api/auth.rs b/src/api/auth.rs index 8396540..e6cab0a 100644 --- a/src/api/auth.rs +++ b/src/api/auth.rs @@ -1,8 +1,17 @@ -use std::{sync::Arc, collections::{HashMap, BTreeMap}, time::SystemTime}; +use std::{ + collections::{BTreeMap, HashMap}, + sync::Arc, + time::SystemTime, +}; -use axum::{extract::{Query, State}, response::{IntoResponse, Response}, http::{StatusCode, header}, Form}; +use axum::{ + extract::{Query, State}, + http::{header, StatusCode}, + response::{IntoResponse, Response}, + Form, +}; use axum_auth::AuthBasic; -use chrono::{DateTime, Utc, Duration}; +use chrono::{DateTime, Days, Duration, Utc}; use serde::{Deserialize, Serialize}; use tracing::{debug, error, info, span, Level}; @@ -12,8 +21,15 @@ use sha2::Sha256; use rand::Rng; -use crate::{dto::{scope::Scope, user::TokenInfo}, app_state::AppState}; -use crate::database::Database; +use crate::{database::Database, dto::scope::Action}; +use crate::{ + app_state::AppState, + dto::{ + scope::{Scope, ScopeType}, + user::{AuthToken, TokenInfo}, + RepositoryVisibility, + }, +}; use crate::auth::auth_challenge_response; @@ -44,38 +60,43 @@ pub struct AuthResponse { } /// In the returned UserToken::user, only the username is specified -fn create_jwt_token(account: &str) -> anyhow::Result { +fn create_jwt_token(account: Option<&str>, scopes: Vec) -> anyhow::Result { let key: Hmac = Hmac::new_from_slice(b"some-secret")?; - + let now = chrono::offset::Utc::now(); - let now_secs = now.timestamp(); - // Construct the claims for the token - let mut claims = BTreeMap::new(); - claims.insert("issuer", "orca-registry__DEV"); - claims.insert("subject", &account); - //claims.insert("audience", auth.service); + // Expire the token in a day + let expiration = now.checked_add_days(Days::new(1)).unwrap(); - let not_before = format!("{}", now_secs); - let issued_at = format!("{}", now_secs); - let expiration = format!("{}", now_secs + 86400); // 1 day - claims.insert("notbefore", ¬_before); - claims.insert("issuedat", &issued_at); - claims.insert("expiration", &expiration); // TODO: 20 seconds expiry for testing - - let issued_at = now; - let expiration = now + Duration::seconds(20); - - // Create a randomized jwtid let mut rng = rand::thread_rng(); let jwtid = format!("{}", rng.gen::()); - claims.insert("jwtid", &jwtid); - let token_str = claims.sign_with_key(&key)?; - Ok(TokenInfo::new(token_str, expiration, issued_at)) + // empty account if they are not authenticated + let account = account.map(|a| a.to_string()).unwrap_or(String::new()); + + // Construct the claims for the token + // TODO: Verify the token! + let token = AuthToken::new( + String::from("orca-registry__DEV"), + account, + String::from("reg"), + expiration, + now.clone(), + now.clone(), + jwtid, + scopes, + ); + + let token_str = token.sign_with_key(&key)?; + Ok(TokenInfo::new(token_str, expiration, now)) } -pub async fn auth_basic_get(basic_auth: Option, state: State>, Query(params): Query>, form: Option>) -> Result { +pub async fn auth_basic_get( + basic_auth: Option, + state: State>, + Query(params): Query>, + form: Option>, +) -> Result { let mut auth = TokenAuthRequest { user: None, password: None, @@ -88,19 +109,32 @@ pub async fn auth_basic_get(basic_auth: Option, state: State { + auth.scope.push(scope); + } + Err(_) => { + return Err(StatusCode::BAD_REQUEST); + } + } + } + // If BasicAuth is provided, set the fields to it if let Some(AuthBasic((username, pass))) = basic_auth { auth.user = Some(username.clone()); auth.password = pass; // I hate having to create this span here multiple times, but its the only - // way I could think of + // way I could think of /* let span = span!(Level::DEBUG, "auth", username = auth.user.clone()); let _enter = span.enter(); debug!("Read user authentication from an AuthBasic"); */ auth_method = "basic-auth"; - } + } // Username and password could be passed in forms // If there was a way to also check if the Method was "POST", this is where // we would do it. @@ -114,14 +148,88 @@ pub async fn auth_basic_get(basic_auth: Option, state: State { + // check repository visibility + let database = &state.database; + match database.get_repository_visibility(&scope.path).await { + Ok(Some(RepositoryVisibility::Public)) => res.push(Ok(true)), + Ok(_) => res.push(Ok(false)), + Err(e) => { + error!( + "Failure to check repository visibility for {}! Err: {}", + scope.path, e + ); + + res.push(Err(StatusCode::INTERNAL_SERVER_ERROR)); + } + } + } + _ => res.push(Ok(false)), + } + } + + // merge the booleans into a single bool, respond with errors if there are any. + let res: Result, StatusCode> = res.into_iter().collect(); + res?.iter().all(|b| *b) + }; + + if is_public_access { + for scope in auth.scope.iter_mut() { + // only retain Action::Pull + scope.actions.retain(|a| *a == Action::Pull); + } + + let token = create_jwt_token(None, auth.scope).map_err(|_| { + error!("Failed to create jwt token!"); + + StatusCode::INTERNAL_SERVER_ERROR + })?; + + let token_str = token.token; + let now_format = format!("{}", token.created_at.format("%+")); + + let auth_response = AuthResponse { + token: token_str.clone(), + expires_in: 86400, // 1 day + issued_at: now_format, + }; + + let json_str = + serde_json::to_string(&auth_response).map_err(|_| StatusCode::BAD_REQUEST)?; + + debug!("Created anonymous token for public scopes!"); + + return Ok(( + StatusCode::OK, + [ + (header::CONTENT_TYPE, "application/json"), + (header::AUTHORIZATION, &format!("Bearer {}", token_str)), + ], + json_str, + ) + .into_response()); + } else { + info!("Auth failure! Auth was not provided in either AuthBasic or Form!"); + + // Maybe BAD_REQUEST should be returned? + return Err(StatusCode::UNAUTHORIZED); + } } // Create logging span for the rest of this request - let span = span!(Level::DEBUG, "auth", username = auth.user.clone(), auth_method); + let span = span!( + Level::DEBUG, + "auth", + username = auth.user.clone(), + auth_method + ); let _enter = span.enter(); debug!("Parsed user auth request"); @@ -131,8 +239,11 @@ pub async fn auth_basic_get(basic_auth: Option, state: State, state: State { - auth.scope.push(scope); - }, - Err(_) => { - return Err(StatusCode::BAD_REQUEST); - } - } - } - // Get offline token and attempt to convert it to a boolean if let Some(offline_token) = params.get("offline_token") { if let Ok(b) = offline_token.parse::() { @@ -175,24 +272,29 @@ pub async fn auth_basic_get(basic_auth: Option, state: State, state: State, state: State, + pub scope_type: ScopeType, + pub path: String, + pub actions: Vec, } impl Scope { diff --git a/src/dto/user.rs b/src/dto/user.rs index 0c2bb6b..92b9735 100644 --- a/src/dto/user.rs +++ b/src/dto/user.rs @@ -4,11 +4,12 @@ use async_trait::async_trait; use axum::{http::{StatusCode, header, HeaderName, HeaderMap, request::Parts}, extract::FromRequestParts}; use bitflags::bitflags; use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; use tracing::debug; use crate::{app_state::AppState, database::Database}; -use super::RepositoryVisibility; +use super::{RepositoryVisibility, scope::Scope}; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum LoginSource { @@ -45,6 +46,50 @@ impl User { } } +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct AuthToken { + #[serde(rename = "iss")] + pub issuer: String, + + #[serde(rename = "sub")] + pub subject: String, + + #[serde(rename = "aud")] + pub audience: String, + + #[serde(rename = "exp")] + #[serde(with = "chrono::serde::ts_seconds")] + pub expiration: DateTime, + + #[serde(rename = "nbf")] + #[serde(with = "chrono::serde::ts_seconds")] + pub not_before: DateTime, + + #[serde(rename = "iat")] + #[serde(with = "chrono::serde::ts_seconds")] + pub issued_at: DateTime, + + #[serde(rename = "jti")] + pub jwt_id: String, + + pub access: Vec, +} + +impl AuthToken { + pub fn new(issuer: String, subject: String, audience: String, expiration: DateTime, not_before: DateTime, issued_at: DateTime, jwt_id: String, access: Vec) -> Self { + Self { + issuer, + subject, + audience, + expiration, + not_before, + issued_at, + jwt_id, + access + } + } +} + #[derive(Clone, Debug, PartialEq)] pub struct TokenInfo { pub token: String,