Rewrite auth endpoint to allow anonymous tokens, and better tokens

This commit is contained in:
SeanOMik 2023-07-14 15:42:39 -04:00
parent 618c04c29b
commit 95914653e0
Signed by: SeanOMik
GPG Key ID: 568F326C7EB33ACB
5 changed files with 235 additions and 76 deletions

1
Cargo.lock generated
View File

@ -321,6 +321,7 @@ dependencies = [
"js-sys", "js-sys",
"num-integer", "num-integer",
"num-traits", "num-traits",
"serde",
"time", "time",
"wasm-bindgen", "wasm-bindgen",
"winapi", "winapi",

View File

@ -14,7 +14,7 @@ uuid = { version = "1.3.1", features = [ "v4", "fast-rng" ] }
sqlx = { version = "0.6.3", features = [ "runtime-tokio-rustls", "sqlite" ] } sqlx = { version = "0.6.3", features = [ "runtime-tokio-rustls", "sqlite" ] }
bytes = "1.4.0" bytes = "1.4.0"
chrono = "0.4.23" chrono = { version = "0.4.23", features = [ "serde" ] }
tokio = { version = "1.21.2", features = [ "fs", "macros" ] } tokio = { version = "1.21.2", features = [ "fs", "macros" ] }
tokio-util = { version = "0.7.7", features = [ "io" ] } tokio-util = { version = "0.7.7", features = [ "io" ] }

View File

@ -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 axum_auth::AuthBasic;
use chrono::{DateTime, Utc, Duration}; use chrono::{DateTime, Days, Duration, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing::{debug, error, info, span, Level}; use tracing::{debug, error, info, span, Level};
@ -12,8 +21,15 @@ use sha2::Sha256;
use rand::Rng; use rand::Rng;
use crate::{dto::{scope::Scope, user::TokenInfo}, app_state::AppState}; use crate::{database::Database, dto::scope::Action};
use crate::database::Database; use crate::{
app_state::AppState,
dto::{
scope::{Scope, ScopeType},
user::{AuthToken, TokenInfo},
RepositoryVisibility,
},
};
use crate::auth::auth_challenge_response; use crate::auth::auth_challenge_response;
@ -44,38 +60,43 @@ pub struct AuthResponse {
} }
/// In the returned UserToken::user, only the username is specified /// In the returned UserToken::user, only the username is specified
fn create_jwt_token(account: &str) -> anyhow::Result<TokenInfo> { fn create_jwt_token(account: Option<&str>, scopes: Vec<Scope>) -> anyhow::Result<TokenInfo> {
let key: Hmac<Sha256> = Hmac::new_from_slice(b"some-secret")?; let key: Hmac<Sha256> = Hmac::new_from_slice(b"some-secret")?;
let now = chrono::offset::Utc::now(); let now = chrono::offset::Utc::now();
let now_secs = now.timestamp();
// Construct the claims for the token // Expire the token in a day
let mut claims = BTreeMap::new(); let expiration = now.checked_add_days(Days::new(1)).unwrap();
claims.insert("issuer", "orca-registry__DEV");
claims.insert("subject", &account);
//claims.insert("audience", auth.service);
let not_before = format!("{}", now_secs);
let issued_at = format!("{}", now_secs);
let expiration = format!("{}", now_secs + 86400); // 1 day
claims.insert("notbefore", &not_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 mut rng = rand::thread_rng();
let jwtid = format!("{}", rng.gen::<u64>()); let jwtid = format!("{}", rng.gen::<u64>());
claims.insert("jwtid", &jwtid);
let token_str = claims.sign_with_key(&key)?; // empty account if they are not authenticated
Ok(TokenInfo::new(token_str, expiration, issued_at)) 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<AuthBasic>, state: State<Arc<AppState>>, Query(params): Query<HashMap<String, String>>, form: Option<Form<AuthForm>>) -> Result<Response, StatusCode> { pub async fn auth_basic_get(
basic_auth: Option<AuthBasic>,
state: State<Arc<AppState>>,
Query(params): Query<HashMap<String, String>>,
form: Option<Form<AuthForm>>,
) -> Result<Response, StatusCode> {
let mut auth = TokenAuthRequest { let mut auth = TokenAuthRequest {
user: None, user: None,
password: None, password: None,
@ -88,6 +109,19 @@ pub async fn auth_basic_get(basic_auth: Option<AuthBasic>, state: State<Arc<AppS
let auth_method; let auth_method;
// Process all the scopes
if let Some(scope) = params.get("scope") {
// TODO: Handle multiple scopes
match Scope::try_from(&scope[..]) {
Ok(scope) => {
auth.scope.push(scope);
}
Err(_) => {
return Err(StatusCode::BAD_REQUEST);
}
}
}
// If BasicAuth is provided, set the fields to it // If BasicAuth is provided, set the fields to it
if let Some(AuthBasic((username, pass))) = basic_auth { if let Some(AuthBasic((username, pass))) = basic_auth {
auth.user = Some(username.clone()); auth.user = Some(username.clone());
@ -113,15 +147,89 @@ pub async fn auth_basic_get(basic_auth: Option<AuthBasic>, state: State<Arc<AppS
debug!("Read user authentication from a Form"); debug!("Read user authentication from a Form");
auth_method = "form"; auth_method = "form";
} else {
// If no auth parameters were specified, check if the repository is public. if it is, respond with a token.
let is_public_access = {
let mut res = vec![];
for scope in auth.scope.iter() {
match scope.scope_type {
ScopeType::Repository => {
// 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<Vec<bool>, 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 { } else {
info!("Auth failure! Auth was not provided in either AuthBasic or Form!"); info!("Auth failure! Auth was not provided in either AuthBasic or Form!");
// Maybe BAD_REQUEST should be returned? // Maybe BAD_REQUEST should be returned?
return Err(StatusCode::UNAUTHORIZED); return Err(StatusCode::UNAUTHORIZED);
} }
}
// Create logging span for the rest of this request // 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(); let _enter = span.enter();
debug!("Parsed user auth request"); debug!("Parsed user auth request");
@ -131,7 +239,10 @@ pub async fn auth_basic_get(basic_auth: Option<AuthBasic>, state: State<Arc<AppS
if let Some(account) = params.get("account") { if let Some(account) = params.get("account") {
if let Some(user) = &auth.user { if let Some(user) = &auth.user {
if account != user { if account != user {
error!("`user` and `account` are not the same!!! (user: {}, account: {})", user, account); error!(
"`user` and `account` are not the same!!! (user: {}, account: {})",
user, account
);
return Err(StatusCode::BAD_REQUEST); return Err(StatusCode::BAD_REQUEST);
} }
@ -145,20 +256,6 @@ pub async fn auth_basic_get(basic_auth: Option<AuthBasic>, state: State<Arc<AppS
auth.service = Some(service.clone()); auth.service = Some(service.clone());
} }
// Process all the scopes
if let Some(scope) = params.get("scope") {
// TODO: Handle multiple scopes
match Scope::try_from(&scope[..]) {
Ok(scope) => {
auth.scope.push(scope);
},
Err(_) => {
return Err(StatusCode::BAD_REQUEST);
}
}
}
// Get offline token and attempt to convert it to a boolean // Get offline token and attempt to convert it to a boolean
if let Some(offline_token) = params.get("offline_token") { if let Some(offline_token) = params.get("offline_token") {
if let Ok(b) = offline_token.parse::<bool>() { if let Ok(b) = offline_token.parse::<bool>() {
@ -175,20 +272,25 @@ pub async fn auth_basic_get(basic_auth: Option<AuthBasic>, state: State<Arc<AppS
if let (Some(account), Some(password)) = (&auth.account, auth.password) { if let (Some(account), Some(password)) = (&auth.account, auth.password) {
// Ensure that the password is correct // Ensure that the password is correct
let mut auth_driver = state.auth_checker.lock().await; let mut auth_driver = state.auth_checker.lock().await;
if !auth_driver.verify_user_login(account.clone(), password).await if !auth_driver
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? { .verify_user_login(account.clone(), password)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
{
debug!("Authentication failed, incorrect password!"); debug!("Authentication failed, incorrect password!");
// TODO: Dont unwrap, find a way to return multiple scopes // TODO: Dont unwrap, find a way to return multiple scopes
return Ok(auth_challenge_response(&state.config, Some(auth.scope.first().unwrap().clone()))); return Ok(auth_challenge_response(
&state.config,
Some(auth.scope.first().unwrap().clone()),
));
} }
drop(auth_driver); drop(auth_driver);
debug!("User password is correct"); debug!("User password is correct");
let now = SystemTime::now(); let now = SystemTime::now();
let token = create_jwt_token(account) let token = create_jwt_token(Some(account), vec![]).map_err(|_| {
.map_err(|_| {
error!("Failed to create jwt token!"); error!("Failed to create jwt token!");
StatusCode::INTERNAL_SERVER_ERROR StatusCode::INTERNAL_SERVER_ERROR
@ -208,11 +310,18 @@ pub async fn auth_basic_get(basic_auth: Option<AuthBasic>, state: State<Arc<AppS
issued_at: now_format, issued_at: now_format,
}; };
let json_str = serde_json::to_string(&auth_response) let json_str =
.map_err(|_| StatusCode::BAD_REQUEST)?; serde_json::to_string(&auth_response).map_err(|_| StatusCode::BAD_REQUEST)?;
let database = &state.database; let database = &state.database;
database.store_user_token(token_str.clone(), account.clone(), token.expiry, token.created_at).await database
.store_user_token(
token_str.clone(),
account.clone(),
token.expiry,
token.created_at,
)
.await
.map_err(|_| { .map_err(|_| {
error!("Failed to store user token in database!"); error!("Failed to store user token in database!");
@ -224,10 +333,11 @@ pub async fn auth_basic_get(basic_auth: Option<AuthBasic>, state: State<Arc<AppS
StatusCode::OK, StatusCode::OK,
[ [
(header::CONTENT_TYPE, "application/json"), (header::CONTENT_TYPE, "application/json"),
( header::AUTHORIZATION, &format!("Bearer {}", token_str) ) (header::AUTHORIZATION, &format!("Bearer {}", token_str)),
], ],
json_str json_str,
).into_response()); )
.into_response());
} }
info!("Auth failure! Not enough information given to create auth token!"); info!("Auth failure! Not enough information given to create auth token!");

View File

@ -1,12 +1,13 @@
use anyhow::anyhow; use anyhow::anyhow;
use serde::{Deserialize, de::Visitor}; use serde::{Deserialize, de::Visitor, Serialize};
use std::fmt; use std::fmt;
#[derive(Default, Debug, Clone)] #[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum ScopeType { pub enum ScopeType {
#[default] #[default]
Unknown, Unknown,
#[serde(rename = "repository")]
Repository, Repository,
} }
@ -19,11 +20,13 @@ impl fmt::Display for ScopeType {
} }
} }
#[derive(Default, Debug, Clone)] #[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum Action { pub enum Action {
#[default] #[default]
None, None,
#[serde(rename = "push")]
Push, Push,
#[serde(rename = "pull")]
Pull, Pull,
} }
@ -37,11 +40,11 @@ impl fmt::Display for Action {
} }
} }
#[derive(Default, Debug, Clone)] #[derive(Default, Debug, Clone, Serialize, PartialEq, Eq)]
pub struct Scope { pub struct Scope {
scope_type: ScopeType, pub scope_type: ScopeType,
path: String, pub path: String,
actions: Vec<Action>, pub actions: Vec<Action>,
} }
impl Scope { impl Scope {

View File

@ -4,11 +4,12 @@ use async_trait::async_trait;
use axum::{http::{StatusCode, header, HeaderName, HeaderMap, request::Parts}, extract::FromRequestParts}; use axum::{http::{StatusCode, header, HeaderName, HeaderMap, request::Parts}, extract::FromRequestParts};
use bitflags::bitflags; use bitflags::bitflags;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use tracing::debug; use tracing::debug;
use crate::{app_state::AppState, database::Database}; use crate::{app_state::AppState, database::Database};
use super::RepositoryVisibility; use super::{RepositoryVisibility, scope::Scope};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum LoginSource { 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<Utc>,
#[serde(rename = "nbf")]
#[serde(with = "chrono::serde::ts_seconds")]
pub not_before: DateTime<Utc>,
#[serde(rename = "iat")]
#[serde(with = "chrono::serde::ts_seconds")]
pub issued_at: DateTime<Utc>,
#[serde(rename = "jti")]
pub jwt_id: String,
pub access: Vec<Scope>,
}
impl AuthToken {
pub fn new(issuer: String, subject: String, audience: String, expiration: DateTime<Utc>, not_before: DateTime<Utc>, issued_at: DateTime<Utc>, jwt_id: String, access: Vec<Scope>) -> Self {
Self {
issuer,
subject,
audience,
expiration,
not_before,
issued_at,
jwt_id,
access
}
}
}
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct TokenInfo { pub struct TokenInfo {
pub token: String, pub token: String,