Compare commits
8 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 941f4846c7 | |||
| 77104472df | |||
| 283c4ebad2 | |||
| fd0fc47ccf | |||
| 61d4cc3978 | |||
| 073a5ac512 | |||
| 905c57000a | |||
| 18b33c00a7 |
12 changed files with 1195 additions and 464 deletions
1556
Cargo.lock
generated
1556
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -50,3 +50,4 @@ redis = { version = "0.27.3", default-features = false, features = ["acl"] }
|
|||
# Auth utils
|
||||
totp-rs = "5.6"
|
||||
|
||||
shadow-rs = { version = "1.2.0", features = ["metadata", "std"] }
|
||||
|
|
|
|||
8
TODO.md
8
TODO.md
|
|
@ -62,3 +62,11 @@
|
|||
- [ ] implement docker secrets. https://docs.docker.com/engine/swarm/secrets/
|
||||
|
||||
- [ ] Find a minimal OpenID client implementation like Listmonk but a little bit more mature
|
||||
|
||||
- [x] Add `iat` field to id_token claims. Even though the iat field is not required in the spec,
|
||||
most OIDC client require its use for security reason, to not accept a token before a certain date.
|
||||
It make it clear that the token must not be retro-active.
|
||||
|
||||
- [ ] Add `picture` claim in `id_token`
|
||||
|
||||
> URL of the End-User's profile picture. This URL MUST refer to an image file (for example, a PNG, JPEG, or GIF image file), rather than to a Web page containing an image. Note that this URL SHOULD specifically reference a profile photo of the End-User suitable for displaying when describing the End-User, rather than an arbitrary photo taken by the End-User.
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ kernel = { path = "../kernel" }
|
|||
utils = { path = "../utils" }
|
||||
|
||||
# common
|
||||
shadow-rs = { workspace = true }
|
||||
log = { workspace = true }
|
||||
env_logger = { workspace = true }
|
||||
|
||||
|
|
@ -19,23 +20,8 @@ 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 }
|
||||
|
|
@ -44,6 +30,22 @@ sqlx = { workspace = true }
|
|||
uuid = { workspace = true }
|
||||
url = { workspace = true }
|
||||
|
||||
# Web
|
||||
axum = { version = "0.8.4", features = ["json", "multipart"] }
|
||||
axum-extra = { version = "0.10.0", features = ["cookie"] }
|
||||
axum-template = { version = "2.4.0", features = ["minijinja"] }
|
||||
axum_typed_multipart = "0.16.2"
|
||||
# to make work the static assets server
|
||||
tower-http = { version = "0.6.1", features = ["fs", "trace"] }
|
||||
# template engine
|
||||
minijinja = { version = "2.1", features = ["builtins"] }
|
||||
minijinja-embed = "2.3.1"
|
||||
|
||||
axum-macros = "0.4.2"
|
||||
jsonwebtoken = "9.3.0"
|
||||
time = "0.3.36"
|
||||
|
||||
serde_urlencoded = "0.7.1"
|
||||
# To work with key pair
|
||||
pem = "3.0.4"
|
||||
|
||||
|
|
@ -53,6 +55,7 @@ branch = "master"
|
|||
features = ["simple_asn1", "pem"]
|
||||
|
||||
[build-dependencies]
|
||||
shadow-rs = { workspace = true }
|
||||
minijinja-embed = "2.3.1"
|
||||
|
||||
[[bin]]
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
use shadow_rs::ShadowBuilder;
|
||||
|
||||
fn main() {
|
||||
ShadowBuilder::builder().build().unwrap();
|
||||
minijinja_embed::embed_templates!("src/templates");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
use axum::{extract::State, response::IntoResponse, Json};
|
||||
use serde_json::json;
|
||||
use shadow_rs::shadow;
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
shadow!(build);
|
||||
|
||||
pub async fn get_index(
|
||||
State(app_state): State<AppState>,
|
||||
) -> impl IntoResponse {
|
||||
|
|
@ -12,3 +15,11 @@ pub async fn get_index(
|
|||
"base_uri": app_state.config.instance.base_uri
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn get_app_version() -> impl IntoResponse {
|
||||
Json(json!({
|
||||
"tag": shadow_rs::tag(),
|
||||
"branch": shadow_rs::branch(),
|
||||
"commit_hash": build::COMMIT_HASH
|
||||
}))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use axum::{extract::State, http::StatusCode, response::{Html, IntoResponse}, Extension, Form, Json};
|
||||
use chrono::{Duration, Utc};
|
||||
use fully_pub::fully_pub;
|
||||
use log::error;
|
||||
use log::{debug, error};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use kernel::{models::authorization::Authorization, repositories::users::get_user_by_id};
|
||||
|
|
@ -11,7 +11,7 @@ use crate::{
|
|||
|
||||
const AUTHORIZATION_CODE_TTL_SECONDS: i64 = 120;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[fully_pub]
|
||||
struct AccessTokenRequestParams {
|
||||
grant_type: String,
|
||||
|
|
@ -48,6 +48,7 @@ pub async fn get_access_token(
|
|||
let authorization = match authorizations_res {
|
||||
Ok(val) => val,
|
||||
Err(sqlx::Error::RowNotFound) => {
|
||||
error!("Received invalid authorization_code.");
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json("Invalid authorization_code.")
|
||||
|
|
@ -68,12 +69,15 @@ pub async fn get_access_token(
|
|||
Utc::now().signed_duration_since(ts) < Duration::seconds(AUTHORIZATION_CODE_TTL_SECONDS)
|
||||
});
|
||||
if !is_code_valid {
|
||||
debug!("Received expired authorization code");
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json("Authorization code has expired.")
|
||||
).into_response();
|
||||
}
|
||||
|
||||
debug!("Generating access_token and id_token.");
|
||||
|
||||
// 2.3. Fetch user resource owner
|
||||
let user = get_user_by_id(&app_state.db, &authorization.user_id)
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use jsonwebkey_convert_repaired::RSAPublicKey;
|
||||
use jsonwebkey_convert_repaired::der::FromPem;
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ struct WellKnownOpenIdConfiguration {
|
|||
userinfo_endpoint: String,
|
||||
scopes_supported: Vec<String>,
|
||||
response_types_supported: Vec<String>,
|
||||
subject_types_supported: Vec<String>,
|
||||
token_endpoint_auth_methods_supported: Vec<String>,
|
||||
id_token_signing_alg_values_supported: Vec<String>,
|
||||
jwks_uri: String
|
||||
|
|
@ -33,6 +34,7 @@ pub async fn get_well_known_openid_configuration(
|
|||
userinfo_endpoint: format!("{}/api/user", base_url),
|
||||
scopes_supported: AuthorizationScope::iter().map(|v| v.to_string()).collect(),
|
||||
response_types_supported: vec!["code".into()],
|
||||
subject_types_supported: vec!["public".into(), "pairwise".into()],
|
||||
token_endpoint_auth_methods_supported: vec!["client_secret_basic".into()],
|
||||
id_token_signing_alg_values_supported: vec!["RS256".into()],
|
||||
jwks_uri: format!("{}/.well-known/jwks", base_url)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ pub async fn get_user_asset(
|
|||
Err(_) => {
|
||||
return (
|
||||
StatusCode::NOT_FOUND,
|
||||
Html("Could not find user asset")
|
||||
Html("Could not find user asset.")
|
||||
).into_response();
|
||||
},
|
||||
Ok(ua) => ua
|
||||
|
|
@ -22,6 +22,6 @@ pub async fn get_user_asset(
|
|||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_str(&user_asset.mime_type).expect("Constructing header value.")
|
||||
);
|
||||
|
||||
|
||||
(hm, user_asset.content).into_response()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,11 +46,12 @@ 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))
|
||||
.route("/api", get(api::index::get_index))
|
||||
.route("/api/user-assets/:asset_id", get(api::public_assets::get_user_asset));
|
||||
.layer(middleware::from_fn_with_state(app_state.clone(), app_auth::enforce_jwt_auth_middleware));
|
||||
|
||||
let well_known_routes = Router::new()
|
||||
let public_api_routes = Router::new()
|
||||
.route("/api", get(api::index::get_index))
|
||||
.route("/api/user-assets/{asset_id}", get(api::public_assets::get_user_asset))
|
||||
.route("/.app-version", get(api::index::get_app_version))
|
||||
.route("/.well-known/openid-configuration", get(api::openid::well_known::get_well_known_openid_configuration))
|
||||
.route("/.well-known/jwks", get(api::openid::keys::get_signing_public_keys));
|
||||
|
||||
|
|
@ -59,7 +60,7 @@ pub fn build_router(server_config: &ServerConfig, app_state: AppState) -> Router
|
|||
.merge(user_routes)
|
||||
.merge(api_app_routes)
|
||||
.merge(api_user_routes)
|
||||
.merge(well_known_routes)
|
||||
.merge(public_api_routes)
|
||||
.nest_service(
|
||||
"/assets",
|
||||
ServeDir::new(server_config.assets_path.clone())
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
use std::ascii::AsciiExt;
|
||||
|
||||
use axum::body::HttpBody;
|
||||
use fully_pub::fully_pub;
|
||||
use jsonwebtoken::get_current_timestamp;
|
||||
use kernel::models::{authorization::AuthorizationScope, config::Config, user::User};
|
||||
|
|
@ -64,7 +67,9 @@ impl OAuth2AccessTokenClaims {
|
|||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[fully_pub]
|
||||
struct OIDCIdTokenClaims {
|
||||
/// Token expiration
|
||||
/// Issued at date
|
||||
iat: u64,
|
||||
/// Token expiration date
|
||||
exp: u64,
|
||||
/// Token issuer (URI to the issuer)
|
||||
iss: String,
|
||||
|
|
@ -72,10 +77,15 @@ struct OIDCIdTokenClaims {
|
|||
aud: String,
|
||||
/// End-user id assigned by the issuer (user_id)
|
||||
sub: String,
|
||||
/// additional claims
|
||||
name: Option<String>,
|
||||
/// Displayable name
|
||||
name: String,
|
||||
email: Option<String>,
|
||||
/// handle of user
|
||||
preferred_username: Option<String>,
|
||||
/// Public URL to the user asset id (for now)
|
||||
/// In the future, we should create a unique link per authorization
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
picture: Option<String>,
|
||||
roles: Vec<String>,
|
||||
nonce: Option<String>
|
||||
}
|
||||
|
|
@ -91,9 +101,11 @@ impl OIDCIdTokenClaims {
|
|||
iss: config.instance.base_uri.clone(),
|
||||
aud: client_id.into(),
|
||||
sub: user.id,
|
||||
iat: get_current_timestamp(),
|
||||
exp: get_current_timestamp() + 86_000,
|
||||
email: user.email,
|
||||
name: user.full_name,
|
||||
name: user.full_name.unwrap_or(user.handle.clone()),
|
||||
picture: user.avatar_asset_id.map(|asset_id| format!("{}/api/user-assets/{}", &config.instance.base_uri, asset_id)),
|
||||
preferred_username: Some(user.handle),
|
||||
roles: user.roles.0,
|
||||
nonce
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue