refactor: structure of an hexagonal architecture
Created a kernel crate to store models and future action implementations. Will be useful to create admin cli.
This commit is contained in:
parent
69af48bb62
commit
3713cc2443
87 changed files with 834 additions and 474 deletions
525
Cargo.lock
generated
525
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
38
Cargo.toml
38
Cargo.toml
|
@ -3,26 +3,26 @@ cargo-features = ["codegen-backend"]
|
|||
[profile.dev]
|
||||
codegen-backend = "cranelift"
|
||||
|
||||
[package]
|
||||
name = "minauthator"
|
||||
description = "Identity provider and OAuth2 server for an small-scale organization."
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
[workspace]
|
||||
members = [
|
||||
"lib/kernel",
|
||||
"lib/utils",
|
||||
"lib/http_server",
|
||||
"lib/admin_cli"
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
[workspace.dependencies]
|
||||
# commons utils
|
||||
anyhow = "1.0"
|
||||
fully_pub = "0.1"
|
||||
argon2 = "0.5"
|
||||
strum = "0.26.3"
|
||||
strum_macros = "0.26"
|
||||
uuid = { version = "1.8", features = ["serde", "v4"] }
|
||||
dotenvy = "0.15.7"
|
||||
base64 = "0.22.1"
|
||||
rand = "0.8.5"
|
||||
rand_core = { version = "0.6.4", features = ["std"] }
|
||||
url = "2.5.3"
|
||||
argh = "0.1" # for CLI
|
||||
|
||||
# CLI
|
||||
argh = "0.1"
|
||||
|
||||
# Async
|
||||
tokio = { version = "1.40.0", features = ["rt-multi-thread"] }
|
||||
|
@ -34,7 +34,6 @@ env_logger = "0.11"
|
|||
# Serialization
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde_urlencoded = "0.7.1"
|
||||
toml = "0.8"
|
||||
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
|
@ -43,21 +42,6 @@ chrono = { version = "0.4", features = ["serde"] }
|
|||
sqlx = { version = "0.7.4", features = ["sqlite", "runtime-tokio", "chrono", "uuid"] }
|
||||
redis = { version = "0.27.3", default-features = false, features = ["acl"] }
|
||||
|
||||
# Web
|
||||
axum = { version = "0.7.7", features = ["json", "multipart"] }
|
||||
axum-extra = { version = "0.9.4", features = ["cookie"] }
|
||||
axum-template = { version = "2.4.0", features = ["minijinja"] }
|
||||
axum_typed_multipart = "0.13.1"
|
||||
minijinja = { version = "2.1", features = ["builtins"] }
|
||||
# to make work the static assets server
|
||||
tower-http = { version = "0.6.1", features = ["fs"] }
|
||||
|
||||
# Auth utils
|
||||
totp-rs = "5.6"
|
||||
minijinja-embed = "2.3.1"
|
||||
axum-macros = "0.4.2"
|
||||
jsonwebtoken = "9.3.0"
|
||||
time = "0.3.36"
|
||||
|
||||
[build-dependencies]
|
||||
minijinja-embed = "2.3.1"
|
||||
|
|
|
@ -3,3 +3,7 @@
|
|||
https://datatracker.ietf.org/doc/html/rfc6749
|
||||
|
||||
https://stackoverflow.com/questions/79118231/how-to-access-the-axum-request-path-in-a-minijinja-template
|
||||
|
||||
## Oauth2 test
|
||||
|
||||
-> authorize
|
||||
|
|
5
justfile
5
justfile
|
@ -1,11 +1,12 @@
|
|||
export RUST_BACKTRACE := "1"
|
||||
export RUST_LOG := "trace"
|
||||
export RUN_ARGS := "run --bin minauthator-server -- --config ./config.toml --database ./tmp/dbs/minauthator.db --static-assets ./assets"
|
||||
|
||||
watch-run:
|
||||
cargo-watch -x 'run -- --config ./config.toml --database ./tmp/dbs/minauthator.db --static-assets ./assets'
|
||||
cargo-watch -x "$RUN_ARGS"
|
||||
|
||||
run:
|
||||
cargo run -- --database ./tmp/dbs/minauthator.db --config ./config.toml --static-assets ./assets
|
||||
cargo $RUN_ARGS
|
||||
|
||||
docker-run:
|
||||
docker run -p 3085:8080 -v ./tmp/docker/config:/etc/minauthator -v ./tmp/docker/db:/var/lib/minauthator minauthator
|
||||
|
|
11
lib/admin_cli/Cargo.toml
Normal file
11
lib/admin_cli/Cargo.toml
Normal file
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "admin_cli"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
fully_pub = { workspace = true }
|
||||
|
||||
[[bin]]
|
||||
name = "minauthator-admin"
|
||||
path = "src/main.rs"
|
6
lib/admin_cli/src/main.rs
Normal file
6
lib/admin_cli/src/main.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
use anyhow::Result;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
println!("Starting minauthator admin CLI");
|
||||
Ok(())
|
||||
}
|
51
lib/http_server/Cargo.toml
Normal file
51
lib/http_server/Cargo.toml
Normal file
|
@ -0,0 +1,51 @@
|
|||
[package]
|
||||
name = "http_server"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
kernel = { path = "../kernel" }
|
||||
utils = { path = "../utils" }
|
||||
|
||||
# common
|
||||
log = { workspace = true }
|
||||
env_logger = { workspace = true }
|
||||
|
||||
strum = { workspace = true }
|
||||
strum_macros = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
fully_pub = { workspace = true }
|
||||
|
||||
tokio = { workspace = true }
|
||||
|
||||
# Web
|
||||
axum = { version = "0.7.7", features = ["json", "multipart"] }
|
||||
axum-extra = { version = "0.9.4", features = ["cookie"] }
|
||||
axum-template = { version = "2.4.0", features = ["minijinja"] }
|
||||
axum_typed_multipart = "0.13.1"
|
||||
minijinja = { version = "2.1", features = ["builtins"] }
|
||||
# to make work the static assets server
|
||||
tower-http = { version = "0.6.1", features = ["fs"] }
|
||||
|
||||
minijinja-embed = "2.3.1"
|
||||
axum-macros = "0.4.2"
|
||||
jsonwebtoken = "9.3.0"
|
||||
time = "0.3.36"
|
||||
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_urlencoded = "0.7.1"
|
||||
chrono = { workspace = true }
|
||||
|
||||
argh = { workspace = true }
|
||||
|
||||
sqlx = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
url = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
minijinja-embed = "2.3.1"
|
||||
|
||||
[[bin]]
|
||||
name = "minauthator-server"
|
||||
path = "src/main.rs"
|
14
lib/http_server/src/controllers/api/index.rs
Normal file
14
lib/http_server/src/controllers/api/index.rs
Normal file
|
@ -0,0 +1,14 @@
|
|||
use axum::{extract::State, response::IntoResponse, Json};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
pub async fn get_index(
|
||||
State(app_state): State<AppState>,
|
||||
) -> impl IntoResponse {
|
||||
Json(json!({
|
||||
"software": "Minauthator",
|
||||
"name": app_state.config.instance.name,
|
||||
"base_uri": app_state.config.instance.base_uri
|
||||
}))
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
pub mod index;
|
||||
pub mod oauth2;
|
||||
pub mod read_user;
|
||||
pub mod openid;
|
|
@ -4,10 +4,9 @@ use fully_pub::fully_pub;
|
|||
use log::error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use kernel::models::authorization::Authorization;
|
||||
use crate::{
|
||||
models::{authorization::Authorization, token_claims::AppUserTokenClaims},
|
||||
server::AppState,
|
||||
services::{app_session::AppClientSession, session::create_token}
|
||||
services::{app_session::AppClientSession, session::create_token}, token_claims::AppUserTokenClaims, AppState
|
||||
};
|
||||
|
||||
const AUTHORIZATION_CODE_TTL_SECONDS: i64 = 120;
|
||||
|
@ -43,7 +42,7 @@ pub async fn get_access_token(
|
|||
)
|
||||
.bind(&form.code)
|
||||
.bind(&app_client_session.client_id)
|
||||
.fetch_one(&app_state.db)
|
||||
.fetch_one(&app_state.db.0)
|
||||
.await;
|
||||
let authorization = match authorizations_res {
|
||||
Ok(val) => val,
|
|
@ -1,9 +1,10 @@
|
|||
use axum::{extract::State, response::IntoResponse, Json};
|
||||
use fully_pub::fully_pub;
|
||||
use kernel::models::authorization::AuthorizationScope;
|
||||
use serde::Serialize;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::{models::authorization::AuthorizationScope, server::AppState};
|
||||
use crate::AppState;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[fully_pub]
|
|
@ -2,7 +2,8 @@ use axum::{extract::State, response::IntoResponse, Extension, Json};
|
|||
use fully_pub::fully_pub;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{models::{token_claims::AppUserTokenClaims, user::User}, server::AppState};
|
||||
use crate::{token_claims::AppUserTokenClaims, AppState};
|
||||
use kernel::models::user::User;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[fully_pub]
|
||||
|
@ -19,10 +20,10 @@ pub async fn read_user_basic(
|
|||
State(app_state): State<AppState>,
|
||||
Extension(token_claims): Extension<AppUserTokenClaims>,
|
||||
) -> impl IntoResponse {
|
||||
// 1. This handler require client user authentification (JWT)
|
||||
// 1. This handler require app user authentification (JWT)
|
||||
let user_res = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
|
||||
.bind(&token_claims.user_id)
|
||||
.fetch_one(&app_state.db)
|
||||
.fetch_one(&app_state.db.0)
|
||||
.await
|
||||
.expect("To get user from claim");
|
||||
let output = ReadUserBasicExtract {
|
0
lib/http_server/src/controllers/ui/admin/users.rs
Normal file
0
lib/http_server/src/controllers/ui/admin/users.rs
Normal file
|
@ -1,10 +1,10 @@
|
|||
use axum::{extract::State, response::IntoResponse, Extension};
|
||||
use minijinja::context;
|
||||
|
||||
use kernel::models::{config::AppVisibility, config::Application};
|
||||
use crate::{
|
||||
models::{config::AppVisibility, config::Application},
|
||||
renderer::TemplateRenderer,
|
||||
server::AppState
|
||||
AppState
|
||||
};
|
||||
|
||||
pub async fn list_apps(
|
|
@ -7,14 +7,15 @@ use serde::{Deserialize, Serialize};
|
|||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
use kernel::{
|
||||
models::{authorization::Authorization, config::AppAuthorizeFlow}
|
||||
};
|
||||
use utils::get_random_alphanumerical;
|
||||
use crate::{
|
||||
models::{authorization::Authorization, config::AppAuthorizeFlow, token_claims::UserTokenClaims},
|
||||
renderer::TemplateRenderer, server::AppState,
|
||||
services::oauth2::{parse_scope, verify_redirect_uri},
|
||||
utils::get_random_alphanumerical
|
||||
renderer::TemplateRenderer, services::oauth2::{parse_scope, verify_redirect_uri}, token_claims::UserTokenClaims, AppState
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[fully_pub]
|
||||
/// query params described in [RFC6749 section 4.1.1](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1)
|
||||
struct AuthorizationParams {
|
||||
|
@ -105,7 +106,7 @@ pub async fn authorize_form(
|
|||
.bind(&token_claims.sub)
|
||||
.bind(&authorization_params.client_id)
|
||||
.bind(sqlx::types::Json(&scopes))
|
||||
.fetch_one(&app_state.db)
|
||||
.fetch_one(&app_state.db.0)
|
||||
.await;
|
||||
|
||||
match authorizations_res {
|
||||
|
@ -119,7 +120,7 @@ pub async fn authorize_form(
|
|||
.bind(existing_authorization.id)
|
||||
.bind(authorization_code.clone())
|
||||
.bind(Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true))
|
||||
.execute(&app_state.db)
|
||||
.execute(&app_state.db.0)
|
||||
.await.unwrap();
|
||||
|
||||
// Authorization already given, just redirect to the app
|
||||
|
@ -219,7 +220,7 @@ pub async fn perform_authorize(
|
|||
.bind(authorization.code)
|
||||
.bind(authorization.last_used_at.map(|x| x.to_rfc3339_opts(SecondsFormat::Millis, true)))
|
||||
.bind(authorization.created_at.to_rfc3339_opts(SecondsFormat::Millis, true))
|
||||
.execute(&app_state.db)
|
||||
.execute(&app_state.db.0)
|
||||
.await;
|
||||
if let Err(err) = res {
|
||||
error!("Failed to save authorization in DB. {}", err);
|
|
@ -1,15 +1,15 @@
|
|||
use axum_extra::extract::{cookie::{Cookie, SameSite}, CookieJar};
|
||||
use chrono::{SecondsFormat, Utc};
|
||||
use kernel::models::user::{User, UserStatus};
|
||||
use log::info;
|
||||
use serde::Deserialize;
|
||||
use axum::{extract::{Query, State}, http::{HeaderMap, HeaderValue, StatusCode}, response::{Html, IntoResponse, Redirect}, Extension, Form};
|
||||
use axum::{extract::{Query, State}, http::StatusCode, response::{IntoResponse, Redirect}, Extension, Form};
|
||||
use fully_pub::fully_pub;
|
||||
use minijinja::context;
|
||||
use time::Duration;
|
||||
use utils::verify_password_hash;
|
||||
|
||||
use crate::{
|
||||
consts::WEB_GUI_JWT_COOKIE_NAME, models::{token_claims::UserTokenClaims, user::{User, UserStatus}}, renderer::TemplateRenderer, server::AppState, services::{password::verify_password_hash, session::create_token}
|
||||
};
|
||||
use crate::{renderer::TemplateRenderer, services::session::create_token, token_claims::UserTokenClaims, AppState, WEB_GUI_JWT_COOKIE_NAME};
|
||||
|
||||
pub async fn login_form(
|
||||
Extension(renderer): Extension<TemplateRenderer>
|
||||
|
@ -47,7 +47,7 @@ pub async fn perform_login(
|
|||
let user_res = sqlx::query_as::<_, User>("SELECT * FROM users WHERE handle = $1 OR email = $2")
|
||||
.bind(&login.login)
|
||||
.bind(&login.login)
|
||||
.fetch_one(&app_state.db)
|
||||
.fetch_one(&app_state.db.0)
|
||||
.await;
|
||||
|
||||
let password_hash = match &user_res {
|
||||
|
@ -87,7 +87,7 @@ pub async fn perform_login(
|
|||
let _result = sqlx::query("UPDATE users SET last_login_at = $2 WHERE id = $1")
|
||||
.bind(user.id.clone())
|
||||
.bind(Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true))
|
||||
.execute(&app_state.db)
|
||||
.execute(&app_state.db.0)
|
||||
.await.unwrap();
|
||||
|
||||
let jwt_max_age = Duration::days(15);
|
|
@ -1,7 +1,7 @@
|
|||
use axum::response::{IntoResponse, Redirect};
|
||||
use axum_extra::extract::CookieJar;
|
||||
|
||||
use crate::consts::WEB_GUI_JWT_COOKIE_NAME;
|
||||
use crate::WEB_GUI_JWT_COOKIE_NAME;
|
||||
|
||||
pub async fn perform_logout(
|
||||
cookies: CookieJar
|
|
@ -5,10 +5,11 @@ use log::error;
|
|||
use minijinja::context;
|
||||
|
||||
use crate::{
|
||||
models::{token_claims::UserTokenClaims, user::User},
|
||||
token_claims::UserTokenClaims,
|
||||
renderer::TemplateRenderer,
|
||||
server::AppState
|
||||
AppState
|
||||
};
|
||||
use kernel::models::user::User;
|
||||
|
||||
pub async fn me_page(
|
||||
State(app_state): State<AppState>,
|
||||
|
@ -17,7 +18,7 @@ pub async fn me_page(
|
|||
) -> impl IntoResponse {
|
||||
let user_res = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
|
||||
.bind(&token_claims.sub)
|
||||
.fetch_one(&app_state.db)
|
||||
.fetch_one(&app_state.db.0)
|
||||
.await
|
||||
.expect("To get user from claim");
|
||||
|
||||
|
@ -38,7 +39,7 @@ pub async fn me_update_details_form(
|
|||
|
||||
let user_res = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
|
||||
.bind(&token_claims.sub)
|
||||
.fetch_one(&app_state.db)
|
||||
.fetch_one(&app_state.db.0)
|
||||
.await
|
||||
.expect("To get user from claim");
|
||||
|
||||
|
@ -79,12 +80,12 @@ pub async fn me_perform_update_details(
|
|||
.bind(details_update.full_name)
|
||||
.bind(details_update.website)
|
||||
.bind(details_update.picture.contents.to_vec())
|
||||
.execute(&app_state.db)
|
||||
.execute(&app_state.db.0)
|
||||
.await;
|
||||
|
||||
let user_res = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
|
||||
.bind(&token_claims.sub)
|
||||
.fetch_one(&app_state.db)
|
||||
.fetch_one(&app_state.db.0)
|
||||
.await
|
||||
.expect("To get user from claim");
|
||||
|
|
@ -7,7 +7,10 @@ use fully_pub::fully_pub;
|
|||
use sqlx::types::Json;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{models::user::{User, UserStatus}, renderer::TemplateRenderer, server::AppState, services::password::get_password_hash};
|
||||
use crate::{renderer::TemplateRenderer, AppState};
|
||||
|
||||
use kernel::models::user::{User, UserStatus};
|
||||
use utils::get_password_hash;
|
||||
|
||||
pub async fn register_form(
|
||||
State(app_state): State<AppState>
|
||||
|
@ -66,7 +69,7 @@ pub async fn perform_register(
|
|||
.bind(user.roles)
|
||||
.bind(user.password_hash)
|
||||
.bind(user.created_at.to_rfc3339_opts(SecondsFormat::Millis, true))
|
||||
.execute(&app_state.db)
|
||||
.execute(&app_state.db.0)
|
||||
.await;
|
||||
match res {
|
||||
Err(err) => {
|
||||
|
@ -93,7 +96,7 @@ pub async fn perform_register(
|
|||
StatusCode::OK,
|
||||
"pages/register",
|
||||
context!(
|
||||
success => true
|
||||
success => true
|
||||
)
|
||||
)
|
||||
}
|
|
@ -4,7 +4,8 @@ use log::error;
|
|||
use minijinja::context;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{models::{authorization::Authorization, token_claims::UserTokenClaims}, renderer::TemplateRenderer, server::AppState};
|
||||
use kernel::models::authorization::Authorization;
|
||||
use crate::{renderer::TemplateRenderer, token_claims::UserTokenClaims, AppState};
|
||||
|
||||
pub async fn get_authorizations(
|
||||
State(app_state): State<AppState>,
|
||||
|
@ -13,7 +14,7 @@ pub async fn get_authorizations(
|
|||
) -> impl IntoResponse {
|
||||
let user_authorizations = sqlx::query_as::<_, Authorization>("SELECT * FROM authorizations WHERE user_id = $1")
|
||||
.bind(&token_claims.sub)
|
||||
.fetch_all(&app_state.db)
|
||||
.fetch_all(&app_state.db.0)
|
||||
.await
|
||||
.expect("To get user authorization with user_id from claim");
|
||||
renderer.render(
|
||||
|
@ -37,7 +38,7 @@ pub async fn revoke_authorization(
|
|||
) -> impl IntoResponse {
|
||||
let delete_res = sqlx::query("DELETE FROM authorizations WHERE id = $1")
|
||||
.bind(&form.authorization_id)
|
||||
.execute(&app_state.db)
|
||||
.execute(&app_state.db.0)
|
||||
.await;
|
||||
match delete_res {
|
||||
Ok(_) => {},
|
|
@ -1,24 +1,22 @@
|
|||
use base64::{prelude::BASE64_STANDARD, Engine};
|
||||
pub mod controllers;
|
||||
pub mod router;
|
||||
pub mod services;
|
||||
pub mod middlewares;
|
||||
pub mod renderer;
|
||||
pub mod token_claims;
|
||||
|
||||
use fully_pub::fully_pub;
|
||||
use anyhow::{Result, Context};
|
||||
use kernel::{context::AppSecrets, models::config::Config, repositories::storage::Storage};
|
||||
use log::info;
|
||||
use minijinja::{context, Environment};
|
||||
use sqlx::{Pool, Sqlite};
|
||||
use crate::{models::config::{AppSecrets, Config}, router::build_router};
|
||||
use minijinja::Environment;
|
||||
|
||||
fn build_templating_env(config: &Config) -> Environment<'static> {
|
||||
let mut env = Environment::new();
|
||||
use crate::{
|
||||
router::build_router,
|
||||
renderer::build_templating_env
|
||||
};
|
||||
|
||||
minijinja_embed::load_templates!(&mut env);
|
||||
|
||||
env.add_global("gl", context! {
|
||||
instance => config.instance
|
||||
});
|
||||
env.add_function("encode_b64str", |bin_val: Vec<u8>| {
|
||||
BASE64_STANDARD.encode(bin_val)
|
||||
});
|
||||
env
|
||||
}
|
||||
pub const WEB_GUI_JWT_COOKIE_NAME: &str = "minauthator_jwt";
|
||||
|
||||
#[derive(Debug)]
|
||||
#[fully_pub]
|
||||
|
@ -34,7 +32,7 @@ pub struct ServerConfig {
|
|||
pub struct AppState {
|
||||
secrets: AppSecrets,
|
||||
config: Config,
|
||||
db: Pool<Sqlite>,
|
||||
db: Storage,
|
||||
templating_env: Environment<'static>
|
||||
}
|
||||
|
||||
|
@ -42,7 +40,7 @@ pub async fn start_http_server(
|
|||
server_config: ServerConfig,
|
||||
config: Config,
|
||||
secrets: AppSecrets,
|
||||
db_pool: Pool<Sqlite>
|
||||
db_pool: Storage
|
||||
) -> Result<()> {
|
||||
// build state
|
||||
let state = AppState {
|
|
@ -1,9 +1,9 @@
|
|||
use argh::FromArgs;
|
||||
use anyhow::{Context, Result};
|
||||
use http_server::{start_http_server, ServerConfig};
|
||||
use kernel::{consts::DEFAULT_ASSETS_PATH, context::{get_kernel_context, StartKernelConfig}};
|
||||
use log::info;
|
||||
|
||||
use crate::{get_app_context, server::{start_http_server, ServerConfig}, DEFAULT_ASSETS_PATH};
|
||||
|
||||
#[derive(Debug, FromArgs)]
|
||||
/// Minauthator daemon
|
||||
struct ServerCliFlags {
|
||||
|
@ -27,14 +27,15 @@ struct ServerCliFlags {
|
|||
listen_port: u32
|
||||
}
|
||||
|
||||
/// handle CLI arguments to start process daemon
|
||||
pub async fn start_server_cli() -> Result<()> {
|
||||
/// handle CLI arguments to start HTTP server daemon
|
||||
#[tokio::main]
|
||||
pub async fn main() -> Result<()> {
|
||||
info!("Starting minauth");
|
||||
let flags: ServerCliFlags = argh::from_env();
|
||||
let (config, secrets, db_pool) = get_app_context(crate::StartAppConfig {
|
||||
let (config, secrets, db_pool) = get_kernel_context(StartKernelConfig {
|
||||
config_path: flags.config,
|
||||
database_path: flags.database
|
||||
}).await.context("Getting app context")?;
|
||||
}).await.context("Getting kernel context")?;
|
||||
start_http_server(
|
||||
ServerConfig {
|
||||
assets_path: flags.static_assets.unwrap_or(DEFAULT_ASSETS_PATH.to_string()),
|
|
@ -5,9 +5,12 @@ use axum::{
|
|||
response::{Html, IntoResponse, Response},
|
||||
Extension
|
||||
};
|
||||
use utils::parse_basic_auth;
|
||||
|
||||
use crate::{
|
||||
models::token_claims::AppUserTokenClaims, server::AppState, services::{app_session::AppClientSession, session::verify_token}, utils::parse_basic_auth
|
||||
services::{app_session::AppClientSession, session::verify_token},
|
||||
token_claims::AppUserTokenClaims,
|
||||
AppState
|
||||
};
|
||||
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
use axum::{extract::{Request, State}, http::StatusCode, middleware::Next, response::Response, Extension};
|
||||
|
||||
use crate::{models::token_claims::UserTokenClaims, renderer::TemplateRenderer, server::AppState};
|
||||
use crate::{renderer::TemplateRenderer, token_claims::UserTokenClaims, AppState};
|
||||
|
||||
pub async fn renderer_middleware(
|
||||
State(app_state): State<AppState>,
|
|
@ -7,7 +7,9 @@ use axum::{
|
|||
use axum_extra::extract::CookieJar;
|
||||
|
||||
use crate::{
|
||||
consts::WEB_GUI_JWT_COOKIE_NAME, models::token_claims::UserTokenClaims, server::AppState, services::session::verify_token
|
||||
services::session::verify_token,
|
||||
token_claims::UserTokenClaims,
|
||||
AppState, WEB_GUI_JWT_COOKIE_NAME
|
||||
};
|
||||
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
use axum::{http::StatusCode, response::{Html, IntoResponse}};
|
||||
use fully_pub::fully_pub;
|
||||
use kernel::models::config::Config;
|
||||
use log::error;
|
||||
use minijinja::{context, Environment, Value};
|
||||
use utils::encode_base64_picture;
|
||||
|
||||
use crate::models::token_claims::UserTokenClaims;
|
||||
use crate::token_claims::UserTokenClaims;
|
||||
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -43,3 +45,14 @@ impl TemplateRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn build_templating_env(config: &Config) -> Environment<'static> {
|
||||
let mut env = Environment::new();
|
||||
|
||||
minijinja_embed::load_templates!(&mut env);
|
||||
|
||||
env.add_global("gl", context! {
|
||||
instance => config.instance
|
||||
});
|
||||
env.add_function("inline_picture", encode_base64_picture);
|
||||
env
|
||||
}
|
|
@ -9,7 +9,7 @@ use crate::{
|
|||
app_auth,
|
||||
renderer::renderer_middleware
|
||||
},
|
||||
server::{AppState, ServerConfig}
|
||||
AppState, ServerConfig
|
||||
};
|
||||
|
||||
pub fn build_router(server_config: &ServerConfig, app_state: AppState) -> Router<AppState> {
|
||||
|
@ -43,7 +43,8 @@ pub fn build_router(server_config: &ServerConfig, app_state: AppState) -> Router
|
|||
|
||||
let api_user_routes = Router::new()
|
||||
.route("/api/user", get(api::read_user::read_user_basic))
|
||||
.layer(middleware::from_fn_with_state(app_state.clone(), app_auth::enforce_jwt_auth_middleware));
|
||||
.layer(middleware::from_fn_with_state(app_state.clone(), app_auth::enforce_jwt_auth_middleware))
|
||||
.route("/api", get(api::index::get_index));
|
||||
|
||||
let well_known_routes = Router::new()
|
||||
.route("/.well-known/openid-configuration", get(api::openid::well_known::get_well_known_openid_configuration));
|
|
@ -1,4 +1,3 @@
|
|||
pub mod password;
|
||||
pub mod session;
|
||||
pub mod oauth2;
|
||||
pub mod app_session;
|
|
@ -1,7 +1,7 @@
|
|||
use std::str::FromStr;
|
||||
use anyhow::{Result, Context};
|
||||
|
||||
use crate::models::{authorization::AuthorizationScope, config::Application};
|
||||
use kernel::models::{authorization::AuthorizationScope, config::Application};
|
||||
|
||||
pub fn verify_redirect_uri(app: &Application, input_redirect_uri: &str) -> bool {
|
||||
app.allowed_redirect_uris
|
|
@ -1,8 +1,7 @@
|
|||
use anyhow::Result;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use jsonwebtoken::{encode, decode, Header, Algorithm, Validation, EncodingKey, DecodingKey};
|
||||
|
||||
use crate::models::config::AppSecrets;
|
||||
use kernel::context::AppSecrets;
|
||||
|
||||
|
||||
pub fn create_token<T: Serialize>(secrets: &AppSecrets, claims: T) -> String {
|
|
@ -1,10 +1,9 @@
|
|||
use fully_pub::fully_pub;
|
||||
use jsonwebtoken::get_current_timestamp;
|
||||
use kernel::models::authorization::AuthorizationScope;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::Duration;
|
||||
|
||||
use super::authorization::AuthorizationScope;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[fully_pub]
|
||||
struct UserTokenClaims {
|
22
lib/kernel/Cargo.toml
Normal file
22
lib/kernel/Cargo.toml
Normal file
|
@ -0,0 +1,22 @@
|
|||
[package]
|
||||
name = "kernel"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
utils = { path = "../utils" }
|
||||
|
||||
log = { workspace = true }
|
||||
env_logger = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
fully_pub = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
strum_macros = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
sqlx = { workspace = true }
|
||||
dotenvy = { workspace = true }
|
||||
|
||||
uuid = { workspace = true }
|
||||
url = { workspace = true }
|
0
lib/kernel/src/actions/mod.rs
Normal file
0
lib/kernel/src/actions/mod.rs
Normal file
0
lib/kernel/src/actions/user.rs
Normal file
0
lib/kernel/src/actions/user.rs
Normal file
4
lib/kernel/src/consts.rs
Normal file
4
lib/kernel/src/consts.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
pub const DEFAULT_DB_PATH: &str = "/var/lib/minauthator/minauthator.db";
|
||||
pub const DEFAULT_ASSETS_PATH: &str = "/usr/local/lib/minauthator/assets";
|
||||
pub const DEFAULT_CONFIG_PATH: &str = "/etc/minauthator/config.yaml";
|
||||
|
51
lib/kernel/src/context.rs
Normal file
51
lib/kernel/src/context.rs
Normal file
|
@ -0,0 +1,51 @@
|
|||
use std::{env, fs};
|
||||
use anyhow::{Result, Context, anyhow};
|
||||
use fully_pub::fully_pub;
|
||||
|
||||
use log::info;
|
||||
use sqlx::{Pool, Sqlite};
|
||||
use crate::{
|
||||
consts::{DEFAULT_CONFIG_PATH, DEFAULT_DB_PATH}, database::prepare_database, models::config::Config, repositories::storage::Storage
|
||||
};
|
||||
|
||||
/// get server config
|
||||
fn get_config(path: String) -> Result<Config> {
|
||||
let inp_def_yaml = fs::read_to_string(path)
|
||||
.expect("Should have been able to read the the config file");
|
||||
|
||||
toml::from_str(&inp_def_yaml)
|
||||
.map_err(|e| anyhow!("Failed to parse config, {:?}", e))
|
||||
}
|
||||
|
||||
#[fully_pub]
|
||||
struct StartKernelConfig {
|
||||
config_path: Option<String>,
|
||||
database_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[fully_pub]
|
||||
struct AppSecrets {
|
||||
jwt_secret: String
|
||||
}
|
||||
|
||||
pub async fn get_kernel_context(start_config: StartKernelConfig) -> Result<(Config, AppSecrets, Storage)> {
|
||||
env_logger::init();
|
||||
let _ = dotenvy::dotenv();
|
||||
|
||||
let database_path = &start_config.database_path.unwrap_or(DEFAULT_DB_PATH.to_string());
|
||||
info!("Using database file at {}", database_path);
|
||||
let storage = prepare_database(database_path).await.context("Could not prepare db.")?;
|
||||
|
||||
let config_path = start_config.config_path.unwrap_or(DEFAULT_CONFIG_PATH.to_string());
|
||||
info!("Using config file at {}", &config_path);
|
||||
let config: Config = get_config(config_path)
|
||||
.expect("Cannot get config.");
|
||||
|
||||
dotenvy::dotenv().context("loading .env")?;
|
||||
let secrets = AppSecrets {
|
||||
jwt_secret: env::var("APP_JWT_SECRET").context("Expecting APP_JWT_SECRET env var.")?
|
||||
};
|
||||
|
||||
Ok((config, secrets, storage))
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
use anyhow::{Context, Result};
|
||||
use sqlx::{sqlite::{SqliteConnectOptions, SqlitePoolOptions}, Pool, Sqlite, ConnectOptions};
|
||||
use sqlx::{sqlite::{SqliteConnectOptions, SqlitePoolOptions}, ConnectOptions};
|
||||
use std::str::FromStr;
|
||||
|
||||
pub async fn prepare_database(sqlite_db_path: &str) -> Result<Pool<Sqlite>> {
|
||||
use crate::repositories::storage::Storage;
|
||||
|
||||
pub async fn prepare_database(sqlite_db_path: &str) -> Result<Storage> {
|
||||
let conn_str = format!("sqlite:{}", sqlite_db_path);
|
||||
|
||||
let pool = SqlitePoolOptions::new()
|
||||
|
@ -14,6 +16,6 @@ pub async fn prepare_database(sqlite_db_path: &str) -> Result<Pool<Sqlite>> {
|
|||
.await
|
||||
.context("could not connect to database_url")?;
|
||||
|
||||
Ok(pool)
|
||||
Ok(Storage(pool))
|
||||
}
|
||||
|
7
lib/kernel/src/lib.rs
Normal file
7
lib/kernel/src/lib.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
pub mod models;
|
||||
pub mod database;
|
||||
pub mod consts;
|
||||
pub mod context;
|
||||
pub mod actions;
|
||||
pub mod repositories;
|
||||
|
|
@ -69,7 +69,6 @@ struct Config {
|
|||
roles: Vec<Role>
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[fully_pub]
|
||||
struct AppSecrets {
|
|
@ -1,4 +1,3 @@
|
|||
pub mod config;
|
||||
pub mod user;
|
||||
pub mod authorization;
|
||||
pub mod token_claims;
|
2
lib/kernel/src/repositories/mod.rs
Normal file
2
lib/kernel/src/repositories/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod storage;
|
||||
pub mod users;
|
7
lib/kernel/src/repositories/storage.rs
Normal file
7
lib/kernel/src/repositories/storage.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
use fully_pub::fully_pub;
|
||||
use sqlx::{Pool, Sqlite};
|
||||
|
||||
/// storage interface
|
||||
#[fully_pub]
|
||||
#[derive(Clone, Debug)]
|
||||
struct Storage(Pool<Sqlite>);
|
14
lib/kernel/src/repositories/users.rs
Normal file
14
lib/kernel/src/repositories/users.rs
Normal file
|
@ -0,0 +1,14 @@
|
|||
// user repositories
|
||||
|
||||
use crate::models::user::User;
|
||||
|
||||
use super::storage::Storage;
|
||||
use anyhow::{Result, Context};
|
||||
|
||||
async fn get_user_by_id(storage: &Storage, user_id: &str) -> Result<User> {
|
||||
sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
|
||||
.bind(user_id)
|
||||
.fetch_one(&storage.0)
|
||||
.await
|
||||
.context("To get user from claim")
|
||||
}
|
10
lib/utils/Cargo.toml
Normal file
10
lib/utils/Cargo.toml
Normal file
|
@ -0,0 +1,10 @@
|
|||
[package]
|
||||
name = "utils"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
argon2 = "0.5"
|
||||
base64 = "0.22"
|
||||
rand = "0.8.5"
|
||||
rand_core = { version = "0.6.4", features = ["std"] }
|
|
@ -61,3 +61,7 @@ pub fn parse_basic_auth(header_value: &str) -> Result<(String, String)> {
|
|||
))
|
||||
}
|
||||
|
||||
pub fn encode_base64_picture(picture_bytes: Vec<u8>) -> String {
|
||||
let encoded = BASE64_STANDARD.encode(picture_bytes);
|
||||
return format!("data:image/*;base64,{}", encoded);
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
pub const WEB_GUI_JWT_COOKIE_NAME: &str = "minauthator_jwt";
|
62
src/main.rs
62
src/main.rs
|
@ -1,62 +0,0 @@
|
|||
pub mod models;
|
||||
pub mod controllers;
|
||||
pub mod router;
|
||||
pub mod server;
|
||||
pub mod database;
|
||||
pub mod cli;
|
||||
pub mod utils;
|
||||
pub mod services;
|
||||
pub mod middlewares;
|
||||
pub mod renderer;
|
||||
pub mod consts;
|
||||
|
||||
use std::{env, fs};
|
||||
use anyhow::{Result, Context, anyhow};
|
||||
|
||||
use database::prepare_database;
|
||||
use log::info;
|
||||
use sqlx::{Pool, Sqlite};
|
||||
use models::config::{AppSecrets, Config};
|
||||
|
||||
pub const DEFAULT_DB_PATH: &str = "/var/lib/minauthator/minauthator.db";
|
||||
pub const DEFAULT_ASSETS_PATH: &str = "/usr/local/lib/minauthator/assets";
|
||||
pub const DEFAULT_CONFIG_PATH: &str = "/etc/minauthator/config.yaml";
|
||||
|
||||
fn get_config(path: String) -> Result<Config> {
|
||||
let inp_def_yaml = fs::read_to_string(path)
|
||||
.expect("Should have been able to read the the config file");
|
||||
|
||||
toml::from_str(&inp_def_yaml)
|
||||
.map_err(|e| anyhow!("Failed to parse config, {:?}", e))
|
||||
}
|
||||
|
||||
struct StartAppConfig {
|
||||
config_path: Option<String>,
|
||||
database_path: Option<String>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
cli::start_server_cli().await
|
||||
}
|
||||
|
||||
async fn get_app_context(start_app_config: StartAppConfig) -> Result<(Config, AppSecrets, Pool<Sqlite>)> {
|
||||
env_logger::init();
|
||||
let _ = dotenvy::dotenv();
|
||||
|
||||
let database_path = &start_app_config.database_path.unwrap_or(DEFAULT_DB_PATH.to_string());
|
||||
info!("Using database file at {}", database_path);
|
||||
let pool = prepare_database(database_path).await.context("Could not prepare db.")?;
|
||||
|
||||
let config_path = start_app_config.config_path.unwrap_or(DEFAULT_CONFIG_PATH.to_string());
|
||||
info!("Using config file at {}", &config_path);
|
||||
let config: Config = get_config(config_path)
|
||||
.expect("Cannot get config.");
|
||||
|
||||
dotenvy::dotenv().context("loading .env")?;
|
||||
let secrets = AppSecrets {
|
||||
jwt_secret: env::var("APP_JWT_SECRET").context("Expecting APP_JWT_SECRET env var.")?
|
||||
};
|
||||
|
||||
Ok((config, secrets, pool))
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use argon2::{
|
||||
password_hash::{
|
||||
rand_core::OsRng,
|
||||
PasswordHash, PasswordHasher, PasswordVerifier, SaltString
|
||||
},
|
||||
Argon2
|
||||
};
|
||||
|
||||
pub fn get_password_hash(password: String) -> Result<(String, String)> {
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
|
||||
// Argon2 with default params (Argon2id v19)
|
||||
let argon2 = Argon2::default();
|
||||
|
||||
// Hash password to PHC string ($argon2id$v=19$...)
|
||||
match argon2.hash_password(password.as_bytes(), &salt) {
|
||||
Ok(val) => Ok((salt.to_string(), val.to_string())),
|
||||
Err(_) => Err(anyhow!("Failed to process password."))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_password_hash(password_hash: String, password: String) -> Result<()> {
|
||||
let parsed_hash = match PasswordHash::new(&password_hash) {
|
||||
Ok(val) => val,
|
||||
Err(_) => {
|
||||
return Err(anyhow!("Failed to parse password hash"));
|
||||
}
|
||||
};
|
||||
match Argon2::default().verify_password(password.as_bytes(), &parsed_hash) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(_) => Err(anyhow!("Failed to verify password."))
|
||||
}
|
||||
}
|
||||
|
39
tests/hurl_integration/run_scenario.sh
Executable file
39
tests/hurl_integration/run_scenario.sh
Executable file
|
@ -0,0 +1,39 @@
|
|||
#!/usr/bin/sh
|
||||
|
||||
set -eou pipefail
|
||||
|
||||
scenario_name="$1"
|
||||
project_root="$(dirname $(cargo locate-project | jq -r .root))"
|
||||
scenario_dir="$project_root/tests/hurl_integration/$1"
|
||||
scenario_tmp_dir_path="$project_root/tmp/tests/$scenario_name"
|
||||
database_path="$project_root/tmp/tests/$scenario_name/minauthator.db"
|
||||
|
||||
echo "Starting scenario $scenario_name."
|
||||
mkdir -p $scenario_tmp_dir_path
|
||||
if [ -f $database_path ]; then
|
||||
rm $database_path
|
||||
fi
|
||||
sqlite3 $database_path < $project_root/migrations/all.sql
|
||||
|
||||
export DB_PATH=$database_path
|
||||
if [ -f $scenario_dir/init_db.sh ]; then
|
||||
$scenario_dir/init_db.sh
|
||||
fi
|
||||
|
||||
pkill -f $project_root/target/debug/minauthator-server &
|
||||
sleep 0.1
|
||||
$project_root/target/debug/minauthator-server \
|
||||
--config "$scenario_dir/config.toml" \
|
||||
--database $database_path \
|
||||
--listen-host "127.0.0.1" \
|
||||
--listen-port "8086" \
|
||||
--static-assets "$project_root/assets" &
|
||||
|
||||
server_pid=$!
|
||||
sleep 0.2
|
||||
hurl \
|
||||
--variable base_url="http://localhost:8086" \
|
||||
--test --error-format long \
|
||||
$scenario_dir/main.hurl
|
||||
kill $server_pid
|
||||
echo "End of scenario."
|
56
tests/hurl_integration/scenario_1/config.toml
Normal file
56
tests/hurl_integration/scenario_1/config.toml
Normal file
|
@ -0,0 +1,56 @@
|
|||
[instance]
|
||||
base_uri = "http://localhost:8086"
|
||||
name = "Example org"
|
||||
logo_uri = "https://example.org/logo.png"
|
||||
|
||||
[[applications]]
|
||||
slug = "demo_app"
|
||||
name = "Demo app"
|
||||
description = "A super application where you can do everything you want."
|
||||
client_id = "00000001-0000-0000-0000-000000000001"
|
||||
client_secret = "dummy_client_secret"
|
||||
login_uri = "https://localhost:9876"
|
||||
allowed_redirect_uris = [
|
||||
"http://localhost:9090/callback",
|
||||
"http://localhost:9876/callback"
|
||||
]
|
||||
visibility = "Internal"
|
||||
authorize_flow = "Implicit"
|
||||
|
||||
[[applications]]
|
||||
slug = "wiki"
|
||||
name = "Wiki app"
|
||||
description = "The knowledge base of the exemple org."
|
||||
client_id = "f9de1885-448d-44bb-8c48-7e985486a8c6"
|
||||
client_secret = "49c6c16a-0a8a-4981-a60d-5cb96582cc1a"
|
||||
login_uri = "https://wiki.example.org/login"
|
||||
allowed_redirect_uris = [
|
||||
"https://wiki.example.org/oauth2/callback"
|
||||
]
|
||||
visibility = "Public"
|
||||
authorize_flow = "Implicit"
|
||||
|
||||
[[applications]]
|
||||
slug = "private_app"
|
||||
name = "Demo app"
|
||||
description = "Private app you should never discover"
|
||||
client_id = "c8a08783-2342-4ce3-a3cb-9dc89b6bdf"
|
||||
client_secret = "this_is_the_secret"
|
||||
login_uri = "https://private-app.org"
|
||||
allowed_redirect_uris = [
|
||||
"http://localhost:9091/authorize",
|
||||
]
|
||||
visibility = "Private"
|
||||
authorize_flow = "Implicit"
|
||||
|
||||
[[roles]]
|
||||
slug = "basic"
|
||||
name = "Basic"
|
||||
description = "Basic user"
|
||||
default = true
|
||||
|
||||
[[roles]]
|
||||
slug = "admin"
|
||||
name = "Administrator"
|
||||
description = "Full power on organization instance"
|
||||
|
9
tests/hurl_integration/scenario_1/init_db.sh
Executable file
9
tests/hurl_integration/scenario_1/init_db.sh
Executable file
|
@ -0,0 +1,9 @@
|
|||
password_hash="$(echo -n "root" | argon2 salt_06cGGWYDJCZ -e)"
|
||||
echo $password_hash
|
||||
SQL=$(cat <<EOF
|
||||
INSERT INTO users
|
||||
(id, handle, email, roles, status, password_hash, created_at)
|
||||
VALUES
|
||||
('$(uuid)', 'root', 'root@example.org', '[]', 'Active', '$password_hash', '2024-11-30T00:00:00Z');
|
||||
EOF)
|
||||
echo $SQL | sqlite3 $DB_PATH
|
BIN
tests/hurl_integration/scenario_1/john_doe_profile_pic.jpg
Normal file
BIN
tests/hurl_integration/scenario_1/john_doe_profile_pic.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
88
tests/hurl_integration/scenario_1/main.hurl
Normal file
88
tests/hurl_integration/scenario_1/main.hurl
Normal file
|
@ -0,0 +1,88 @@
|
|||
GET {{ base_url }}/api
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
jsonpath "$.software" == "Minauthator"
|
||||
|
||||
POST {{ base_url }}/login
|
||||
[FormParams]
|
||||
login: root
|
||||
password: root
|
||||
HTTP 303
|
||||
[Captures]
|
||||
user_jwt: cookie "minauthator_jwt"
|
||||
[Asserts]
|
||||
cookie "minauthator_jwt" exists
|
||||
cookie "minauthator_jwt[Value]" contains "eyJ0"
|
||||
cookie "minauthator_jwt[SameSite]" == "Lax"
|
||||
|
||||
GET {{ base_url }}/me
|
||||
HTTP 200
|
||||
Content-Type: text/html; charset=utf-8
|
||||
[Asserts]
|
||||
xpath "string(///h1)" == "Welcome root!"
|
||||
|
||||
POST {{ base_url }}/me/details-form
|
||||
[MultipartFormData]
|
||||
handle: root
|
||||
email: root@johndoe.net
|
||||
full_name: John Doe
|
||||
website: https://johndoe.net
|
||||
picture: file,john_doe_profile_pic.jpg; image/jpeg
|
||||
HTTP 200
|
||||
|
||||
GET {{ base_url }}/me/authorizations
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
xpath "string(///h1)" == "Your authorizations"
|
||||
xpath "string(///i)" == "You didn't authorized or accessed any applications for now."
|
||||
|
||||
# OAuth2 implicit flow (pre-granted app)
|
||||
GET {{ base_url }}/authorize
|
||||
[QueryStringParams]
|
||||
client_id: 00000001-0000-0000-0000-000000000001
|
||||
response_type: code
|
||||
redirect_uri: http://localhost:9090/callback
|
||||
state: Afk4kf6pbZkms78jM
|
||||
scope: user_read_basic
|
||||
HTTP 302
|
||||
[Asserts]
|
||||
header "Location" contains "http://localhost:9090/callback?code="
|
||||
[Captures]
|
||||
authorization_code: header "Location" regex "\\?code=(.*)&"
|
||||
|
||||
# OAuth2 get access token
|
||||
POST {{ base_url }}/api/token
|
||||
[BasicAuth]
|
||||
00000001-0000-0000-0000-000000000001: dummy_client_secret
|
||||
[FormParams]
|
||||
code: {{ authorization_code }}
|
||||
scope: user_read_basic
|
||||
redirect_uri: http://localhost:9090/callback
|
||||
grant_type: authorization_code
|
||||
HTTP 200
|
||||
Content-Type: application/json
|
||||
[Asserts]
|
||||
jsonpath "$.access_token" exists
|
||||
jsonpath "$.access_token" matches "eyJ[[:alpha:]0-9].[[:alpha:]0-9].[[:alpha:]0-9]"
|
||||
[Captures]
|
||||
access_token: jsonpath "$.access_token"
|
||||
|
||||
# Get basic user info
|
||||
GET {{ base_url }}/api/user
|
||||
Authorization: JWT {{ access_token }}
|
||||
HTTP 200
|
||||
Content-Type: application/json
|
||||
[Asserts]
|
||||
jsonpath "$.handle" == "root"
|
||||
jsonpath "$.email" == "root@johndoe.net"
|
||||
|
||||
GET {{ base_url }}/me/authorizations
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
xpath "string(///h1)" == "Your authorizations"
|
||||
xpath "string(///main/ul/li)" contains "UserReadBasic"
|
||||
|
||||
GET {{ base_url }}/logout
|
||||
HTTP 303
|
||||
[Asserts]
|
||||
cookie "minauthator_jwt" == ""
|
1
tests/manual/.gitignore
vendored
Normal file
1
tests/manual/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
tmp
|
7
tests/manual/access_token_request.sh
Executable file
7
tests/manual/access_token_request.sh
Executable file
|
@ -0,0 +1,7 @@
|
|||
#!/usr/bin/sh
|
||||
|
||||
curl -v http://localhost:8085/api/token \
|
||||
-u "a1785786-8be1-443c-9a6f-35feed703609":"49c6c16a-0a8a-4981-a60d-5cb96582cc1a" \
|
||||
-d grant_type="authorization_code" \
|
||||
-d code="$(cat tmp/authorize_code.txt)" \
|
||||
-d redirect_uri="http://localhost:9090/authorize" > tmp/access_token.json
|
8
tests/manual/all.sh
Executable file
8
tests/manual/all.sh
Executable file
|
@ -0,0 +1,8 @@
|
|||
#!/usr/bin/sh
|
||||
|
||||
set -xeuo pipefail
|
||||
|
||||
./login.sh
|
||||
./authorize.sh
|
||||
./access_token_request.sh
|
||||
./get_user_info.sh
|
15
tests/manual/authorize.sh
Executable file
15
tests/manual/authorize.sh
Executable file
|
@ -0,0 +1,15 @@
|
|||
#!/usr/bin/sh
|
||||
|
||||
curl -v http://localhost:8085/authorize \
|
||||
-G \
|
||||
-D "tmp/headers.txt" \
|
||||
--cookie "tmp/.curl-cookies" \
|
||||
-d client_id="a1785786-8be1-443c-9a6f-35feed703609" \
|
||||
-d response_type="code" \
|
||||
-d redirect_uri="http://localhost:9090/callback" \
|
||||
-d scope="user_read_basic" \
|
||||
-d state="qxYAfk4kf6pbZkms78jM"
|
||||
|
||||
code="$(cat tmp/headers.txt | grep -i "location" | awk -F ": " '{print $2}' | trurl -f - -g "{query:code}")"
|
||||
|
||||
echo "$code" > tmp/authorize_code.txt
|
5
tests/manual/create_test_user.sql
Normal file
5
tests/manual/create_test_user.sql
Normal file
|
@ -0,0 +1,5 @@
|
|||
INSERT INTO users
|
||||
(id, handle, email, roles, status, password_hash, created_at)
|
||||
VALUES
|
||||
('30c134a7-d541-4ec7-9310-9c8e298077db', 'test', 'test@example.org', '[]', 'Active', '$argon2i$v=19$m=4096,t=3,p=1$V2laYjAwTlFHOUpiekRlVzRQUU0$33h8XwAWM3pKQM7Ksler0l7rMJfseTuWPJKrdX/cGyc', '2024-11-30T00:00:00Z');
|
||||
|
5
tests/manual/get_user_info.sh
Executable file
5
tests/manual/get_user_info.sh
Executable file
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/sh
|
||||
|
||||
curl -v http://localhost:8085/api/user \
|
||||
-u "a1785786-8be1-443c-9a6f-35feed703609":"49c6c16a-0a8a-4981-a60d-5cb96582cc1a" \
|
||||
-H "Authorization: JWT $(jq -r .access_token tmp/access_token.json)"
|
6
tests/manual/login.sh
Executable file
6
tests/manual/login.sh
Executable file
|
@ -0,0 +1,6 @@
|
|||
#!/usr/bin/sh
|
||||
|
||||
curl -v http://localhost:8085/login \
|
||||
--cookie-jar "tmp/.curl-cookies" \
|
||||
-d login="test" \
|
||||
-d password="test"
|
10
tests/manual/oauth2c.sh
Executable file
10
tests/manual/oauth2c.sh
Executable file
|
@ -0,0 +1,10 @@
|
|||
#!/usr/bin/bash
|
||||
|
||||
oauth2c http://localhost:8085 \
|
||||
--client-id "a1785786-8be1-443c-9a6f-35feed703609" \
|
||||
--client-secret "49c6c16a-0a8a-4981-a60d-5cb96582cc1a" \
|
||||
--response-types code \
|
||||
--response-mode query \
|
||||
--grant-type authorization_code \
|
||||
--auth-method client_secret_basic \
|
||||
--scopes "user_read_basic"
|
6
tests/manual/register.sh
Executable file
6
tests/manual/register.sh
Executable file
|
@ -0,0 +1,6 @@
|
|||
#!/usr/bin/sh
|
||||
|
||||
curl -v http://localhost:8085/register \
|
||||
-d email="test@example.org" \
|
||||
-d handle="test" \
|
||||
-d password="test"
|
Loading…
Reference in a new issue