feat(oauth2): get access token route and read basic user info
This commit is contained in:
parent
ecf1da2978
commit
f990708052
32 changed files with 465 additions and 67 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -1407,6 +1407,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
|
"strum",
|
||||||
"strum_macros",
|
"strum_macros",
|
||||||
"tokio",
|
"tokio",
|
||||||
"toml",
|
"toml",
|
||||||
|
@ -2307,6 +2308,12 @@ version = "0.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum"
|
||||||
|
version = "0.26.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strum_macros"
|
name = "strum_macros"
|
||||||
version = "0.26.4"
|
version = "0.26.4"
|
||||||
|
|
|
@ -57,6 +57,7 @@ base64 = "0.22.1"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
rand_core = { version = "0.6.4", features = ["std"] }
|
rand_core = { version = "0.6.4", features = ["std"] }
|
||||||
url = "2.5.3"
|
url = "2.5.3"
|
||||||
|
strum = "0.26.3"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
minijinja-embed = "2.3.1"
|
minijinja-embed = "2.3.1"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Minauthator
|
# Minauthator
|
||||||
|
|
||||||
Auth provider supporting [OAuth2](https://datatracker.ietf.org/doc/html/rfc6749)
|
Auth provider supporting [OAuth2](https://datatracker.ietf.org/doc/html/rfc6749).
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
|
11
TODO.md
11
TODO.md
|
@ -3,11 +3,10 @@
|
||||||
- [x] Login form
|
- [x] Login form
|
||||||
- [x] Register form
|
- [x] Register form
|
||||||
- [x] Generate JWT
|
- [x] Generate JWT
|
||||||
- Redirect to login form if unauthenticated
|
- [ ] Redirect to login form if unauthenticated
|
||||||
- Authorize form
|
- [x] Authorize form
|
||||||
- Select by app client id
|
- [x] Verify authorize
|
||||||
- Verify authorize
|
- [x] Upload picture
|
||||||
- Select by app client secret
|
- [x] Get access token
|
||||||
- Upload picture
|
|
||||||
|
|
||||||
- [ ] Support error responses by https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1
|
- [ ] Support error responses by https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
# Netscape HTTP Cookie File
|
|
||||||
# https://curl.se/docs/http-cookies.html
|
|
||||||
# This file was generated by libcurl! Edit at your own risk.
|
|
||||||
|
|
||||||
localhost FALSE / FALSE 1731769020 minauth_jwt eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJiZDQzM2Y1MS1kOWViLTRlNjAtODM1OC03NTMyYzJjMGY0NmIiLCJleHAiOjE3MzEyNTA2MjB9.KOPkOd-c-UlBZ4JwWT8XeZKCbgNzSM0Hu2udTzb-rIY
|
|
1
http_integration_tests/.gitignore
vendored
Normal file
1
http_integration_tests/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
tmp
|
7
http_integration_tests/access_token_request.sh
Executable file
7
http_integration_tests/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
|
|
@ -2,9 +2,14 @@
|
||||||
|
|
||||||
curl -v http://localhost:8085/authorize \
|
curl -v http://localhost:8085/authorize \
|
||||||
-G \
|
-G \
|
||||||
--cookie ".curl-cookies" \
|
-D "tmp/headers.txt" \
|
||||||
|
--cookie "tmp/.curl-cookies" \
|
||||||
-d client_id="a1785786-8be1-443c-9a6f-35feed703609" \
|
-d client_id="a1785786-8be1-443c-9a6f-35feed703609" \
|
||||||
-d response_type="code" \
|
-d response_type="code" \
|
||||||
-d redirect_uri="http://localhost:9090/authorize" \
|
-d redirect_uri="http://localhost:9090/authorize" \
|
||||||
-d scope="read_basics" \
|
-d scope="read_basics" \
|
||||||
-d state="qxYAfk4kf6pbZkms78jM"
|
-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
http_integration_tests/get_user_info.sh
Executable file
5
http_integration_tests/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)"
|
|
@ -1,6 +1,6 @@
|
||||||
#!/usr/bin/sh
|
#!/usr/bin/sh
|
||||||
|
|
||||||
curl -v http://localhost:8085/login \
|
curl -v http://localhost:8085/login \
|
||||||
--cookie-jar ".curl-cookies" \
|
--cookie-jar "tmp/.curl-cookies" \
|
||||||
-d login="test" \
|
-d login="test" \
|
||||||
-d password="test"
|
-d password="test"
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
pub mod verify_authorization;
|
pub mod oauth2;
|
||||||
|
pub mod read_user;
|
||||||
|
|
91
src/controllers/api/oauth2/access_token.rs
Normal file
91
src/controllers/api/oauth2/access_token.rs
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
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 serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
models::{authorization::Authorization, token_claims::AppUserTokenClaims},
|
||||||
|
server::AppState,
|
||||||
|
services::{app_session::AppClientSession, session::create_token}
|
||||||
|
};
|
||||||
|
|
||||||
|
const AUTHORIZATION_CODE_TTL_SECONDS: i64 = 120;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[fully_pub]
|
||||||
|
struct AccessTokenRequestParams {
|
||||||
|
grant_type: String,
|
||||||
|
code: String,
|
||||||
|
redirect_uri: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[fully_pub]
|
||||||
|
struct AccessTokenResponse {
|
||||||
|
access_token: String,
|
||||||
|
token_type: String,
|
||||||
|
expires_in: u64
|
||||||
|
}
|
||||||
|
|
||||||
|
// implement Client -> Auth Server request for RFC6749 Authorization Code Grant
|
||||||
|
pub async fn get_access_token(
|
||||||
|
State(app_state): State<AppState>,
|
||||||
|
Extension(app_client_session): Extension<AppClientSession>,
|
||||||
|
Form(form): Form<AccessTokenRequestParams>
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
// 1. This handler require client authentification
|
||||||
|
// login the client with client_id and client_secret
|
||||||
|
|
||||||
|
// 2. Get authorization from DB and validate code
|
||||||
|
let authorizations_res = sqlx::query_as::<_, Authorization>(
|
||||||
|
"SELECT * FROM authorizations WHERE code = $1 AND client_id = $2"
|
||||||
|
)
|
||||||
|
.bind(&form.code)
|
||||||
|
.bind(&app_client_session.client_id)
|
||||||
|
.fetch_one(&app_state.db)
|
||||||
|
.await;
|
||||||
|
let authorization = match authorizations_res {
|
||||||
|
Ok(val) => val,
|
||||||
|
Err(sqlx::Error::RowNotFound) => {
|
||||||
|
return (
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
Json("Invalid authorization_code.")
|
||||||
|
).into_response();
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
error!("Could not fetch authorization. {}", err);
|
||||||
|
return (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Html("Internal server error")
|
||||||
|
).into_response();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// 2.2. Validate that the authorization code is not expired
|
||||||
|
let is_code_valid = authorization.last_used_at
|
||||||
|
.map_or(false, |ts| {
|
||||||
|
Utc::now().signed_duration_since(ts) < Duration::seconds(AUTHORIZATION_CODE_TTL_SECONDS)
|
||||||
|
});
|
||||||
|
if !is_code_valid {
|
||||||
|
return (
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
Json("Authorization code has expired.")
|
||||||
|
).into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Generate JWT for oauth2 client user session
|
||||||
|
let jwt = create_token(
|
||||||
|
&app_state.secrets,
|
||||||
|
AppUserTokenClaims::from_client_user_id(
|
||||||
|
&app_client_session.client_id,
|
||||||
|
&authorization.user_id
|
||||||
|
)
|
||||||
|
);
|
||||||
|
// 4. return JWT
|
||||||
|
let access_token_res = AccessTokenResponse {
|
||||||
|
access_token: jwt,
|
||||||
|
token_type: "jwt".to_string(),
|
||||||
|
expires_in: 3600
|
||||||
|
};
|
||||||
|
Json(access_token_res).into_response()
|
||||||
|
}
|
1
src/controllers/api/oauth2/mod.rs
Normal file
1
src/controllers/api/oauth2/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod access_token;
|
37
src/controllers/api/read_user.rs
Normal file
37
src/controllers/api/read_user.rs
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
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};
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[fully_pub]
|
||||||
|
struct ReadUserBasicExtract {
|
||||||
|
id: String,
|
||||||
|
handle: String,
|
||||||
|
full_name: Option<String>,
|
||||||
|
email: Option<String>,
|
||||||
|
website: Option<String>,
|
||||||
|
roles: Vec<String>
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
let user_res = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
|
||||||
|
.bind(&token_claims.user_id)
|
||||||
|
.fetch_one(&app_state.db)
|
||||||
|
.await
|
||||||
|
.expect("To get user from claim");
|
||||||
|
let output = ReadUserBasicExtract {
|
||||||
|
id: user_res.id,
|
||||||
|
handle: user_res.handle,
|
||||||
|
full_name: user_res.full_name,
|
||||||
|
email: user_res.email,
|
||||||
|
website: user_res.website,
|
||||||
|
roles: user_res.roles.to_vec()
|
||||||
|
};
|
||||||
|
Json(output).into_response()
|
||||||
|
}
|
|
@ -7,11 +7,16 @@ use serde::{Deserialize, Serialize};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{models::authorization::Authorization, renderer::TemplateRenderer, server::AppState, services::{oauth2::verify_redirect_uri, session::TokenClaims}, utils::get_random_alphanumerical};
|
use crate::{
|
||||||
|
models::{authorization::Authorization, token_claims::UserTokenClaims},
|
||||||
|
renderer::TemplateRenderer, server::AppState,
|
||||||
|
services::oauth2::{parse_scope, verify_redirect_uri},
|
||||||
|
utils::get_random_alphanumerical
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
#[fully_pub]
|
#[fully_pub]
|
||||||
/// query params described in [RFC6759 section 4.1.1](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1)
|
/// query params described in [RFC6749 section 4.1.1](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1)
|
||||||
struct AuthorizationParams {
|
struct AuthorizationParams {
|
||||||
response_type: String,
|
response_type: String,
|
||||||
client_id: String,
|
client_id: String,
|
||||||
|
@ -45,7 +50,7 @@ fn redirect_to_client(
|
||||||
/// https://datatracker.ietf.org/doc/html/rfc6749#section-3.1
|
/// https://datatracker.ietf.org/doc/html/rfc6749#section-3.1
|
||||||
pub async fn authorize_form(
|
pub async fn authorize_form(
|
||||||
State(app_state): State<AppState>,
|
State(app_state): State<AppState>,
|
||||||
Extension(token_claims): Extension<TokenClaims>,
|
Extension(token_claims): Extension<UserTokenClaims>,
|
||||||
Extension(renderer): Extension<TemplateRenderer>,
|
Extension(renderer): Extension<TemplateRenderer>,
|
||||||
query_params: Query<AuthorizationParams>
|
query_params: Query<AuthorizationParams>
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
|
@ -94,18 +99,21 @@ pub async fn authorize_form(
|
||||||
|
|
||||||
match authorizations_res {
|
match authorizations_res {
|
||||||
Ok(existing_authorization) => {
|
Ok(existing_authorization) => {
|
||||||
info!("Reusing existing authorization {}", &existing_authorization.id);
|
info!("Reusing existing authorization: {}", &existing_authorization.id);
|
||||||
|
|
||||||
|
// Create new auth code
|
||||||
|
let authorization_code = get_random_alphanumerical(32);
|
||||||
// Update last used timestamp for this authorization
|
// Update last used timestamp for this authorization
|
||||||
let _result = sqlx::query("UPDATE authorizations SET last_used_at = $2 WHERE id = $1")
|
let _result = sqlx::query("UPDATE authorizations SET code = $2, last_used_at = $3 WHERE id = $1")
|
||||||
.bind(existing_authorization.id)
|
.bind(existing_authorization.id)
|
||||||
|
.bind(authorization_code.clone())
|
||||||
.bind(Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true))
|
.bind(Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true))
|
||||||
.execute(&app_state.db)
|
.execute(&app_state.db)
|
||||||
.await.unwrap();
|
.await.unwrap();
|
||||||
|
|
||||||
// Authorization already given, just redirect to the app
|
// Authorization already given, just redirect to the app
|
||||||
return redirect_to_client(
|
return redirect_to_client(
|
||||||
&existing_authorization.code,
|
&authorization_code,
|
||||||
&authorization_params
|
&authorization_params
|
||||||
).into_response()
|
).into_response()
|
||||||
},
|
},
|
||||||
|
@ -137,7 +145,7 @@ pub async fn authorize_form(
|
||||||
|
|
||||||
pub async fn perform_authorize(
|
pub async fn perform_authorize(
|
||||||
State(app_state): State<AppState>,
|
State(app_state): State<AppState>,
|
||||||
Extension(token_claims): Extension<TokenClaims>,
|
Extension(token_claims): Extension<UserTokenClaims>,
|
||||||
Form(authorize_form): Form<AuthorizationParams>
|
Form(authorize_form): Form<AuthorizationParams>
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
// 1. Get the app details
|
// 1. Get the app details
|
||||||
|
@ -152,7 +160,16 @@ pub async fn perform_authorize(
|
||||||
).into_response();
|
).into_response();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// parse scope again
|
||||||
|
let scopes = match parse_scope(&authorize_form.scope) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(err) => {
|
||||||
|
return (
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
Html(format!("Invalid scope: {}", err))
|
||||||
|
).into_response();
|
||||||
|
}
|
||||||
|
};
|
||||||
// 2. Create an authorizaton code
|
// 2. Create an authorizaton code
|
||||||
let authorization_code = get_random_alphanumerical(32);
|
let authorization_code = get_random_alphanumerical(32);
|
||||||
|
|
||||||
|
@ -160,12 +177,12 @@ pub async fn perform_authorize(
|
||||||
id: Uuid::new_v4().to_string(),
|
id: Uuid::new_v4().to_string(),
|
||||||
user_id: token_claims.sub,
|
user_id: token_claims.sub,
|
||||||
client_id: app.client_id.clone(),
|
client_id: app.client_id.clone(),
|
||||||
scopes: sqlx::types::Json(Vec::new()),
|
scopes: sqlx::types::Json(scopes),
|
||||||
code: authorization_code.clone(),
|
code: authorization_code.clone(),
|
||||||
last_used_at: Some(Utc::now()),
|
last_used_at: Some(Utc::now()),
|
||||||
created_at: Utc::now(),
|
created_at: Utc::now(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 3. Save authorization in DB with state
|
// 3. Save authorization in DB with state
|
||||||
let res = sqlx::query("
|
let res = sqlx::query("
|
||||||
INSERT INTO authorizations
|
INSERT INTO authorizations
|
||||||
|
|
|
@ -6,7 +6,7 @@ use fully_pub::fully_pub;
|
||||||
use minijinja::context;
|
use minijinja::context;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
models::user::{User, UserStatus}, renderer::TemplateRenderer, server::AppState, services::{password::verify_password_hash, session::create_token}
|
models::{token_claims::UserTokenClaims, user::{User, UserStatus}}, renderer::TemplateRenderer, server::AppState, services::{password::verify_password_hash, session::create_token}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn login_form(
|
pub async fn login_form(
|
||||||
|
@ -80,7 +80,7 @@ pub async fn perform_login(
|
||||||
.execute(&app_state.db)
|
.execute(&app_state.db)
|
||||||
.await.unwrap();
|
.await.unwrap();
|
||||||
|
|
||||||
let jwt = create_token(&app_state.secrets, &user);
|
let jwt = create_token(&app_state.secrets, UserTokenClaims::from_user_id(&user.id));
|
||||||
|
|
||||||
// TODO: handle keep_session boolean from form and specify cookie max age only if this setting
|
// TODO: handle keep_session boolean from form and specify cookie max age only if this setting
|
||||||
// is true
|
// is true
|
||||||
|
|
|
@ -1,14 +1,18 @@
|
||||||
use axum::{body::Bytes, extract::State, http::StatusCode, response::{Html, IntoResponse}, Extension};
|
use axum::{body::Bytes, extract::State, response::IntoResponse, Extension};
|
||||||
use axum_typed_multipart::{FieldData, TryFromMultipart, TypedMultipart};
|
use axum_typed_multipart::{FieldData, TryFromMultipart, TypedMultipart};
|
||||||
use fully_pub::fully_pub;
|
use fully_pub::fully_pub;
|
||||||
use minijinja::context;
|
use minijinja::context;
|
||||||
|
|
||||||
use crate::{models::user::User, renderer::TemplateRenderer, server::AppState, services::session::TokenClaims};
|
use crate::{
|
||||||
|
models::{token_claims::UserTokenClaims, user::User},
|
||||||
|
renderer::TemplateRenderer,
|
||||||
|
server::AppState
|
||||||
|
};
|
||||||
|
|
||||||
pub async fn me_page(
|
pub async fn me_page(
|
||||||
State(app_state): State<AppState>,
|
State(app_state): State<AppState>,
|
||||||
Extension(renderer): Extension<TemplateRenderer>,
|
Extension(renderer): Extension<TemplateRenderer>,
|
||||||
Extension(token_claims): Extension<TokenClaims>
|
Extension(token_claims): Extension<UserTokenClaims>
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
|
|
||||||
let user_res = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
|
let user_res = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
|
||||||
|
@ -29,7 +33,7 @@ pub async fn me_page(
|
||||||
pub async fn me_update_details_form(
|
pub async fn me_update_details_form(
|
||||||
State(app_state): State<AppState>,
|
State(app_state): State<AppState>,
|
||||||
Extension(renderer): Extension<TemplateRenderer>,
|
Extension(renderer): Extension<TemplateRenderer>,
|
||||||
Extension(token_claims): Extension<TokenClaims>
|
Extension(token_claims): Extension<UserTokenClaims>
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
|
|
||||||
let user_res = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
|
let user_res = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
|
||||||
|
@ -63,7 +67,7 @@ struct UserDetailsUpdateForm {
|
||||||
pub async fn me_perform_update_details(
|
pub async fn me_perform_update_details(
|
||||||
State(app_state): State<AppState>,
|
State(app_state): State<AppState>,
|
||||||
Extension(renderer): Extension<TemplateRenderer>,
|
Extension(renderer): Extension<TemplateRenderer>,
|
||||||
Extension(token_claims): Extension<TokenClaims>,
|
Extension(token_claims): Extension<UserTokenClaims>,
|
||||||
TypedMultipart(details_update): TypedMultipart<UserDetailsUpdateForm>
|
TypedMultipart(details_update): TypedMultipart<UserDetailsUpdateForm>
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let template_path = "pages/me/details-form";
|
let template_path = "pages/me/details-form";
|
||||||
|
|
112
src/middlewares/app_auth.rs
Normal file
112
src/middlewares/app_auth.rs
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
use axum::{
|
||||||
|
extract::{Request, State},
|
||||||
|
http::{HeaderMap, StatusCode},
|
||||||
|
middleware::Next,
|
||||||
|
response::{Html, IntoResponse, Response},
|
||||||
|
Extension
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
models::token_claims::AppUserTokenClaims, server::AppState, services::{app_session::AppClientSession, session::verify_token}, utils::parse_basic_auth
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/// add optional auth to the extension data
|
||||||
|
pub async fn basic_auth_middleware(
|
||||||
|
State(app_state): State<AppState>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
mut req: Request,
|
||||||
|
next: Next,
|
||||||
|
) -> Result<Response, impl IntoResponse> {
|
||||||
|
let authorization_val = match headers.get("Authorization") {
|
||||||
|
Some(header_val) => header_val.to_str().expect("Header val to be string"),
|
||||||
|
None => {
|
||||||
|
// no auth found, auth may be optional
|
||||||
|
return Ok(next.run(req).await)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// check with config
|
||||||
|
let (login, password) = match parse_basic_auth(authorization_val) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_e) => {
|
||||||
|
return Err(
|
||||||
|
(StatusCode::UNAUTHORIZED, Html("Unauthorized: invalid http basic header."))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let app = match app_state.config.applications
|
||||||
|
.iter()
|
||||||
|
.find(|a| a.client_id == login)
|
||||||
|
{
|
||||||
|
Some(app) => app,
|
||||||
|
None => {
|
||||||
|
return Err((
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
Html("Unauthorized: Invalid username or password.")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if app.client_secret != password {
|
||||||
|
return Err((
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
Html("Unauthorized: Invalid username or password.")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
req.extensions_mut().insert(AppClientSession {
|
||||||
|
client_id: login
|
||||||
|
});
|
||||||
|
Ok(next.run(req).await)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// require auth
|
||||||
|
pub async fn enforce_basic_auth_middleware(
|
||||||
|
app_client_session_ext: Option<Extension<AppClientSession>>,
|
||||||
|
req: Request,
|
||||||
|
next: Next,
|
||||||
|
) -> Result<Response, impl IntoResponse> {
|
||||||
|
match app_client_session_ext {
|
||||||
|
Some(_val) => (),
|
||||||
|
None => {
|
||||||
|
// auth is required
|
||||||
|
return Err(
|
||||||
|
(StatusCode::UNAUTHORIZED, Html("Unauthorized: application basic HTTP auth is required on this page."))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(next.run(req).await)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// require App-User auth
|
||||||
|
pub async fn enforce_jwt_auth_middleware(
|
||||||
|
State(app_state): State<AppState>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
mut req: Request,
|
||||||
|
next: Next,
|
||||||
|
) -> Result<Response, impl IntoResponse> {
|
||||||
|
let authorization_val = match headers.get("Authorization") {
|
||||||
|
Some(header_val) => header_val.to_str().expect("Header val to be string"),
|
||||||
|
None => {
|
||||||
|
return Err(
|
||||||
|
(StatusCode::UNAUTHORIZED, Html("Unauthorized: JWT must be provided."))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let jwt = match authorization_val.split(" ").nth(1) {
|
||||||
|
Some(val) => val,
|
||||||
|
None => {
|
||||||
|
return Err(
|
||||||
|
(StatusCode::UNAUTHORIZED, Html("Unauthorized: malformed Authorization header."))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let token_claims: AppUserTokenClaims = match verify_token(&app_state.secrets, &jwt) {
|
||||||
|
Ok(val) => val,
|
||||||
|
Err(_e) => {
|
||||||
|
return Err(
|
||||||
|
(StatusCode::UNAUTHORIZED, Html("Unauthorized: The provided JWT is invalid."))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
req.extensions_mut().insert(token_claims);
|
||||||
|
Ok(next.run(req).await)
|
||||||
|
}
|
|
@ -1,2 +1,3 @@
|
||||||
pub mod auth;
|
pub mod user_auth;
|
||||||
|
pub mod app_auth;
|
||||||
pub mod renderer;
|
pub mod renderer;
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
use axum::{extract::{Request, State}, http::StatusCode, middleware::Next, response::Response, Extension};
|
use axum::{extract::{Request, State}, http::StatusCode, middleware::Next, response::Response, Extension};
|
||||||
|
|
||||||
use crate::{renderer::TemplateRenderer, server::AppState, services::session::TokenClaims};
|
use crate::{models::token_claims::UserTokenClaims, renderer::TemplateRenderer, server::AppState};
|
||||||
|
|
||||||
pub async fn renderer_middleware(
|
pub async fn renderer_middleware(
|
||||||
State(app_state): State<AppState>,
|
State(app_state): State<AppState>,
|
||||||
token_claims_ext: Option<Extension<TokenClaims>>,
|
token_claims_ext: Option<Extension<UserTokenClaims>>,
|
||||||
mut req: Request,
|
mut req: Request,
|
||||||
next: Next,
|
next: Next,
|
||||||
) -> Result<Response, StatusCode> {
|
) -> Result<Response, StatusCode> {
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
use axum::{extract::{Request, State}, http::StatusCode, middleware::Next, response::{Html, IntoResponse, Response}, Extension};
|
use axum::{extract::{Request, State}, http::StatusCode, middleware::Next, response::{Html, IntoResponse, Response}, Extension};
|
||||||
use axum_extra::extract::CookieJar;
|
use axum_extra::extract::CookieJar;
|
||||||
|
|
||||||
use crate::{server::AppState, services::session::{TokenClaims, verify_token}};
|
use crate::{
|
||||||
|
models::token_claims::UserTokenClaims,
|
||||||
|
server::AppState,
|
||||||
|
services::session::verify_token
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/// add optional auth to the extension data
|
/// add optional auth to the extension data
|
||||||
|
@ -18,11 +22,11 @@ pub async fn auth_middleware(
|
||||||
return Ok(next.run(req).await)
|
return Ok(next.run(req).await)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let token_claims = match verify_token(&app_state.secrets, &jwt) {
|
let token_claims: UserTokenClaims = match verify_token(&app_state.secrets, &jwt) {
|
||||||
Ok(val) => val,
|
Ok(val) => val,
|
||||||
Err(_e) => {
|
Err(_e) => {
|
||||||
return Err(
|
return Err(
|
||||||
(StatusCode::UNAUTHORIZED, Html("Unauthorized: The provided is invalid."))
|
(StatusCode::UNAUTHORIZED, Html("Unauthorized: The provided JWT is invalid."))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -32,7 +36,7 @@ pub async fn auth_middleware(
|
||||||
|
|
||||||
/// require auth
|
/// require auth
|
||||||
pub async fn enforce_auth_middleware(
|
pub async fn enforce_auth_middleware(
|
||||||
token_claims_ext: Option<Extension<TokenClaims>>,
|
token_claims_ext: Option<Extension<UserTokenClaims>>,
|
||||||
req: Request,
|
req: Request,
|
||||||
next: Next,
|
next: Next,
|
||||||
) -> Result<Response, impl IntoResponse> {
|
) -> Result<Response, impl IntoResponse> {
|
|
@ -2,9 +2,14 @@ use fully_pub::fully_pub;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::types::Json;
|
use sqlx::types::Json;
|
||||||
|
use strum_macros::{Display, EnumString};
|
||||||
|
|
||||||
#[derive(sqlx::Type, Clone, Debug, Serialize, Deserialize, PartialEq)]
|
#[derive(
|
||||||
#[derive(strum_macros::Display)]
|
Clone, Debug, Serialize, Deserialize, PartialEq,
|
||||||
|
sqlx::Type,
|
||||||
|
Display, EnumString
|
||||||
|
)]
|
||||||
|
#[strum(serialize_all = "lowercase")]
|
||||||
#[fully_pub]
|
#[fully_pub]
|
||||||
enum AuthorizationScope {
|
enum AuthorizationScope {
|
||||||
ReadBasics
|
ReadBasics
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
pub mod authorization;
|
pub mod authorization;
|
||||||
|
pub mod token_claims;
|
||||||
|
|
49
src/models/token_claims.rs
Normal file
49
src/models/token_claims.rs
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
use fully_pub::fully_pub;
|
||||||
|
use jsonwebtoken::get_current_timestamp;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
#[fully_pub]
|
||||||
|
struct UserTokenClaims {
|
||||||
|
/// subject: user id
|
||||||
|
sub: String,
|
||||||
|
/// token expiration
|
||||||
|
exp: u64,
|
||||||
|
/// token issuer
|
||||||
|
iss: String
|
||||||
|
// TODO: add roles
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserTokenClaims {
|
||||||
|
pub fn from_user_id(user_id: &str) -> Self {
|
||||||
|
UserTokenClaims {
|
||||||
|
sub: user_id.into(),
|
||||||
|
exp: get_current_timestamp() + 86_000,
|
||||||
|
iss: "Minauth".into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
#[fully_pub]
|
||||||
|
struct AppUserTokenClaims {
|
||||||
|
/// combined subject
|
||||||
|
client_id: String,
|
||||||
|
user_id: String,
|
||||||
|
/// token expiration
|
||||||
|
exp: u64,
|
||||||
|
/// token issuer
|
||||||
|
iss: String
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppUserTokenClaims {
|
||||||
|
pub fn from_client_user_id(client_id: &str, user_id: &str) -> Self {
|
||||||
|
AppUserTokenClaims {
|
||||||
|
client_id: client_id.into(),
|
||||||
|
user_id: user_id.into(),
|
||||||
|
exp: get_current_timestamp() + 86_000,
|
||||||
|
iss: "Minauth".into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -3,14 +3,14 @@ use fully_pub::fully_pub;
|
||||||
use log::error;
|
use log::error;
|
||||||
use minijinja::{context, Environment, Value};
|
use minijinja::{context, Environment, Value};
|
||||||
|
|
||||||
use crate::services::session::TokenClaims;
|
use crate::models::token_claims::UserTokenClaims;
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
#[fully_pub]
|
#[fully_pub]
|
||||||
struct TemplateRenderer {
|
struct TemplateRenderer {
|
||||||
env: Environment<'static>,
|
env: Environment<'static>,
|
||||||
token_claims: Option<TokenClaims>
|
token_claims: Option<UserTokenClaims>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TemplateRenderer {
|
impl TemplateRenderer {
|
||||||
|
|
|
@ -1,7 +1,16 @@
|
||||||
use axum::{middleware, routing::{get, post}, Router};
|
use axum::{middleware, routing::{get, post}, Router};
|
||||||
use tower_http::services::ServeDir;
|
use tower_http::services::ServeDir;
|
||||||
|
|
||||||
use crate::{controllers::ui, middlewares::{auth::{auth_middleware, enforce_auth_middleware}, renderer::renderer_middleware}, server::{AppState, ServerConfig}};
|
use crate::{
|
||||||
|
controllers::ui,
|
||||||
|
controllers::api,
|
||||||
|
middlewares::{
|
||||||
|
user_auth,
|
||||||
|
app_auth,
|
||||||
|
renderer::renderer_middleware
|
||||||
|
},
|
||||||
|
server::{AppState, ServerConfig}
|
||||||
|
};
|
||||||
|
|
||||||
pub fn build_router(server_config: &ServerConfig, app_state: AppState) -> Router<AppState> {
|
pub fn build_router(server_config: &ServerConfig, app_state: AppState) -> Router<AppState> {
|
||||||
let public_routes = Router::new()
|
let public_routes = Router::new()
|
||||||
|
@ -9,7 +18,8 @@ pub fn build_router(server_config: &ServerConfig, app_state: AppState) -> Router
|
||||||
.route("/register", get(ui::register::register_form))
|
.route("/register", get(ui::register::register_form))
|
||||||
.route("/register", post(ui::register::perform_register))
|
.route("/register", post(ui::register::perform_register))
|
||||||
.route("/login", get(ui::login::login_form))
|
.route("/login", get(ui::login::login_form))
|
||||||
.route("/login", post(ui::login::perform_login));
|
.route("/login", post(ui::login::perform_login))
|
||||||
|
.layer(middleware::from_fn_with_state(app_state.clone(), user_auth::auth_middleware));
|
||||||
|
|
||||||
let user_routes = Router::new()
|
let user_routes = Router::new()
|
||||||
.route("/me", get(ui::me::me_page))
|
.route("/me", get(ui::me::me_page))
|
||||||
|
@ -18,13 +28,24 @@ pub fn build_router(server_config: &ServerConfig, app_state: AppState) -> Router
|
||||||
.route("/logout", get(ui::logout::perform_logout))
|
.route("/logout", get(ui::logout::perform_logout))
|
||||||
.route("/authorize", get(ui::authorize::authorize_form))
|
.route("/authorize", get(ui::authorize::authorize_form))
|
||||||
.route("/authorize", post(ui::authorize::perform_authorize))
|
.route("/authorize", post(ui::authorize::perform_authorize))
|
||||||
.layer(middleware::from_fn_with_state(app_state.clone(), enforce_auth_middleware));
|
.layer(middleware::from_fn_with_state(app_state.clone(), user_auth::enforce_auth_middleware))
|
||||||
|
.layer(middleware::from_fn_with_state(app_state.clone(), user_auth::auth_middleware));
|
||||||
|
|
||||||
|
let app_routes = Router::new()
|
||||||
|
.route("/api/token", post(api::oauth2::access_token::get_access_token))
|
||||||
|
.layer(middleware::from_fn_with_state(app_state.clone(), app_auth::enforce_basic_auth_middleware))
|
||||||
|
.layer(middleware::from_fn_with_state(app_state.clone(), app_auth::basic_auth_middleware));
|
||||||
|
|
||||||
|
let app_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));
|
||||||
|
|
||||||
Router::new()
|
Router::new()
|
||||||
.merge(public_routes)
|
.merge(public_routes)
|
||||||
.merge(user_routes)
|
.merge(user_routes)
|
||||||
|
.merge(app_routes)
|
||||||
|
.merge(app_user_routes)
|
||||||
.layer(middleware::from_fn_with_state(app_state.clone(), renderer_middleware))
|
.layer(middleware::from_fn_with_state(app_state.clone(), renderer_middleware))
|
||||||
.layer(middleware::from_fn_with_state(app_state.clone(), auth_middleware))
|
|
||||||
.nest_service(
|
.nest_service(
|
||||||
"/assets",
|
"/assets",
|
||||||
ServeDir::new(server_config.assets_path.clone())
|
ServeDir::new(server_config.assets_path.clone())
|
||||||
|
|
12
src/services/app_session.rs
Normal file
12
src/services/app_session.rs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
|
||||||
|
use fully_pub::fully_pub;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
|
||||||
|
/// represent a general app session (from http basic auth)
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
#[fully_pub]
|
||||||
|
struct AppClientSession {
|
||||||
|
client_id: String
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
pub mod password;
|
pub mod password;
|
||||||
pub mod session;
|
pub mod session;
|
||||||
pub mod oauth2;
|
pub mod oauth2;
|
||||||
|
pub mod app_session;
|
||||||
|
|
|
@ -1,7 +1,20 @@
|
||||||
use crate::models::config::Application;
|
use std::str::FromStr;
|
||||||
|
use anyhow::{Result, Context};
|
||||||
|
|
||||||
|
use crate::models::{authorization::AuthorizationScope, config::Application};
|
||||||
|
|
||||||
pub fn verify_redirect_uri(app: &Application, input_redirect_uri: &str) -> bool {
|
pub fn verify_redirect_uri(app: &Application, input_redirect_uri: &str) -> bool {
|
||||||
app.allowed_redirect_uris
|
app.allowed_redirect_uris
|
||||||
.iter()
|
.iter()
|
||||||
.find(|uri| **uri == input_redirect_uri).is_some()
|
.find(|uri| **uri == input_redirect_uri).is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn parse_scope(scope_str: &str) -> Result<Vec<AuthorizationScope>> {
|
||||||
|
let mut scopes: Vec<AuthorizationScope> = vec![];
|
||||||
|
for part in scope_str.split(' ') {
|
||||||
|
scopes.push(
|
||||||
|
AuthorizationScope::from_str(part).context("Cannot parse space-delimited scope.")?
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Ok(scopes)
|
||||||
|
}
|
||||||
|
|
|
@ -1,24 +1,12 @@
|
||||||
use fully_pub::fully_pub;
|
use fully_pub::fully_pub;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||||
use jsonwebtoken::{encode, decode, get_current_timestamp, Header, Algorithm, Validation, EncodingKey, DecodingKey};
|
use jsonwebtoken::{encode, decode, Header, Algorithm, Validation, EncodingKey, DecodingKey};
|
||||||
|
|
||||||
use crate::models::{config::AppSecrets, user::User};
|
use crate::models::config::AppSecrets;
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
pub fn create_token<T: Serialize>(secrets: &AppSecrets, claims: T) -> String {
|
||||||
#[fully_pub]
|
|
||||||
struct TokenClaims {
|
|
||||||
/// user id
|
|
||||||
sub: String,
|
|
||||||
exp: u64
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_token(secrets: &AppSecrets, user: &User) -> String {
|
|
||||||
let claims = TokenClaims {
|
|
||||||
sub: user.id.clone(),
|
|
||||||
exp: get_current_timestamp() + 86_400
|
|
||||||
};
|
|
||||||
let token = encode(
|
let token = encode(
|
||||||
&Header::default(),
|
&Header::default(),
|
||||||
&claims,
|
&claims,
|
||||||
|
@ -28,8 +16,8 @@ pub fn create_token(secrets: &AppSecrets, user: &User) -> String {
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn verify_token(secrets: &AppSecrets, jwt: &str) -> Result<TokenClaims> {
|
pub fn verify_token<T: DeserializeOwned>(secrets: &AppSecrets, jwt: &str) -> Result<T> {
|
||||||
let token_data = decode::<TokenClaims>(
|
let token_data = decode::<T>(
|
||||||
&jwt,
|
&jwt,
|
||||||
&DecodingKey::from_secret(&secrets.jwt_secret.as_bytes()),
|
&DecodingKey::from_secret(&secrets.jwt_secret.as_bytes()),
|
||||||
&Validation::new(Algorithm::HS256)
|
&Validation::new(Algorithm::HS256)
|
||||||
|
|
22
src/utils.rs
22
src/utils.rs
|
@ -1,4 +1,4 @@
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result, Context};
|
||||||
use argon2::{
|
use argon2::{
|
||||||
password_hash::{
|
password_hash::{
|
||||||
rand_core::OsRng,
|
rand_core::OsRng,
|
||||||
|
@ -6,6 +6,7 @@ use argon2::{
|
||||||
},
|
},
|
||||||
Argon2
|
Argon2
|
||||||
};
|
};
|
||||||
|
use base64::{prelude::BASE64_STANDARD, Engine};
|
||||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||||
|
|
||||||
pub fn get_password_hash(password: String) -> Result<(String, String)> {
|
pub fn get_password_hash(password: String) -> Result<(String, String)> {
|
||||||
|
@ -41,3 +42,22 @@ pub fn get_random_alphanumerical(length: usize) -> String {
|
||||||
.map(char::from)
|
.map(char::from)
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn parse_basic_auth(header_value: &str) -> Result<(String, String)> {
|
||||||
|
let header_val_components: Vec<&str> = header_value.split(" ").collect();
|
||||||
|
let encoded_header_value: &str = header_val_components
|
||||||
|
.get(1)
|
||||||
|
.ok_or(anyhow!("Could not find encoded part of Authorization header value"))?;
|
||||||
|
let basic_auth_str: String = String::from_utf8(
|
||||||
|
BASE64_STANDARD
|
||||||
|
.decode(encoded_header_value)
|
||||||
|
.context("Could not decode base64 in Authorization header value.")?
|
||||||
|
)?;
|
||||||
|
let components: Vec<&str> = basic_auth_str.split(':').collect();
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
components.get(0).ok_or(anyhow!("Expected username in encoded Authorization header value."))?.to_string(),
|
||||||
|
components.get(1).ok_or(anyhow!("Expected password in encoded Authorization header value."))?.to_string()
|
||||||
|
))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue