Rewrite auth endpoint to allow anonymous tokens, and better tokens
This commit is contained in:
parent
618c04c29b
commit
95914653e0
|
@ -321,6 +321,7 @@ dependencies = [
|
|||
"js-sys",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"time",
|
||||
"wasm-bindgen",
|
||||
"winapi",
|
||||
|
|
|
@ -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" ] }
|
||||
|
||||
|
|
244
src/api/auth.rs
244
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<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 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::<u64>());
|
||||
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<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 {
|
||||
user: None,
|
||||
password: None,
|
||||
|
@ -88,19 +109,32 @@ pub async fn auth_basic_get(basic_auth: Option<AuthBasic>, state: State<Arc<AppS
|
|||
|
||||
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 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<AuthBasic>, state: State<Arc<AppS
|
|||
|
||||
auth_method = "form";
|
||||
} else {
|
||||
info!("Auth failure! Auth was not provided in either AuthBasic or Form!");
|
||||
// If no auth parameters were specified, check if the repository is public. if it is, respond with a token.
|
||||
|
||||
// Maybe BAD_REQUEST should be returned?
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
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 {
|
||||
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<AuthBasic>, state: State<Arc<AppS
|
|||
if let Some(account) = params.get("account") {
|
||||
if let Some(user) = &auth.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);
|
||||
}
|
||||
}
|
||||
|
@ -145,20 +256,6 @@ pub async fn auth_basic_get(basic_auth: Option<AuthBasic>, state: State<Arc<AppS
|
|||
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
|
||||
if let Some(offline_token) = params.get("offline_token") {
|
||||
if let Ok(b) = offline_token.parse::<bool>() {
|
||||
|
@ -175,24 +272,29 @@ pub async fn auth_basic_get(basic_auth: Option<AuthBasic>, state: State<Arc<AppS
|
|||
if let (Some(account), Some(password)) = (&auth.account, auth.password) {
|
||||
// Ensure that the password is correct
|
||||
let mut auth_driver = state.auth_checker.lock().await;
|
||||
if !auth_driver.verify_user_login(account.clone(), password).await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? {
|
||||
if !auth_driver
|
||||
.verify_user_login(account.clone(), password)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||
{
|
||||
debug!("Authentication failed, incorrect password!");
|
||||
|
||||
|
||||
// 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);
|
||||
|
||||
debug!("User password is correct");
|
||||
|
||||
let now = SystemTime::now();
|
||||
let token = create_jwt_token(account)
|
||||
.map_err(|_| {
|
||||
error!("Failed to create jwt token!");
|
||||
let token = create_jwt_token(Some(account), vec![]).map_err(|_| {
|
||||
error!("Failed to create jwt token!");
|
||||
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
let token_str = token.token;
|
||||
|
||||
debug!("Created jwt token");
|
||||
|
@ -208,11 +310,18 @@ pub async fn auth_basic_get(basic_auth: Option<AuthBasic>, state: State<Arc<AppS
|
|||
issued_at: now_format,
|
||||
};
|
||||
|
||||
let json_str = serde_json::to_string(&auth_response)
|
||||
.map_err(|_| StatusCode::BAD_REQUEST)?;
|
||||
let json_str =
|
||||
serde_json::to_string(&auth_response).map_err(|_| StatusCode::BAD_REQUEST)?;
|
||||
|
||||
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(|_| {
|
||||
error!("Failed to store user token in database!");
|
||||
|
||||
|
@ -223,14 +332,15 @@ pub async fn auth_basic_get(basic_auth: Option<AuthBasic>, state: State<Arc<AppS
|
|||
return Ok((
|
||||
StatusCode::OK,
|
||||
[
|
||||
( header::CONTENT_TYPE, "application/json" ),
|
||||
( header::AUTHORIZATION, &format!("Bearer {}", token_str) )
|
||||
(header::CONTENT_TYPE, "application/json"),
|
||||
(header::AUTHORIZATION, &format!("Bearer {}", token_str)),
|
||||
],
|
||||
json_str
|
||||
).into_response());
|
||||
json_str,
|
||||
)
|
||||
.into_response());
|
||||
}
|
||||
|
||||
info!("Auth failure! Not enough information given to create auth token!");
|
||||
// If we didn't get fields required to make a token, then the client did something bad
|
||||
Err(StatusCode::UNAUTHORIZED)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
use anyhow::anyhow;
|
||||
use serde::{Deserialize, de::Visitor};
|
||||
use serde::{Deserialize, de::Visitor, Serialize};
|
||||
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum ScopeType {
|
||||
#[default]
|
||||
Unknown,
|
||||
#[serde(rename = "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 {
|
||||
#[default]
|
||||
None,
|
||||
#[serde(rename = "push")]
|
||||
Push,
|
||||
#[serde(rename = "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 {
|
||||
scope_type: ScopeType,
|
||||
path: String,
|
||||
actions: Vec<Action>,
|
||||
pub scope_type: ScopeType,
|
||||
pub path: String,
|
||||
pub actions: Vec<Action>,
|
||||
}
|
||||
|
||||
impl Scope {
|
||||
|
|
|
@ -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<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)]
|
||||
pub struct TokenInfo {
|
||||
pub token: String,
|
||||
|
|
Loading…
Reference in New Issue