feat: add AccessToken and ValidAccessToken axum extractors and use them in /api/tsigkey/

This commit is contained in:
transcaffeine 2024-09-15 13:14:25 +02:00
parent b49a4724a8
commit 87c4e424bc
Signed by: transcaffeine
GPG Key ID: 03624C433676E465
5 changed files with 150 additions and 30 deletions

View File

@ -1,17 +1,25 @@
use std::fmt::Display; use std::fmt::Display;
use axum::response::IntoResponse; use openidconnect::AccessToken;
use axum::response::{
IntoResponse,
Redirect,
};
use reqwest::StatusCode; use reqwest::StatusCode;
#[derive(Debug)] #[derive(Debug)]
pub enum AuthError { pub enum AuthError {
OpenIdConfig(openidconnect::ConfigurationError), OpenIdConfig(openidconnect::ConfigurationError),
OpenIdRequest(String),
TokenNotActive(AccessToken),
} }
impl Display for AuthError { impl Display for AuthError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
AuthError::OpenIdConfig(e) => f.write_fmt(format_args!("OpenID Connect configuration error: {:?}", e)), AuthError::OpenIdConfig(e) => f.write_fmt(format_args!("OpenID Connect configuration error: {:?}", e)),
AuthError::OpenIdRequest(s) => f.write_fmt(format_args!("{}", s)),
AuthError::TokenNotActive(t) => f.write_fmt(format_args!("Token {} is not active", t.secret())),
} }
} }
} }
@ -24,11 +32,27 @@ impl From<openidconnect::ConfigurationError> for AuthError {
} }
} }
impl From<String> for AuthError {
fn from(s: String) -> Self {
Self::OpenIdRequest(s)
}
}
impl From<AccessToken> for AuthError {
fn from(t: AccessToken) -> Self {
Self::TokenNotActive(t)
}
}
impl IntoResponse for AuthError { impl IntoResponse for AuthError {
fn into_response(self) -> axum::response::Response { fn into_response(self) -> axum::response::Response {
( match self {
AuthError::TokenNotActive(_) => Redirect::to("/openid/login").into_response(),
_ => (
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
format!("Authentication error: {}", self), format!("Authentication error: {}", self),
).into_response() ).into_response(),
}
} }
} }

View File

@ -8,29 +8,45 @@ use axum::response::{
}; };
use crate::AppState; use crate::AppState;
use crate::util::openid::ValidAccessToken;
use crate::model::tsigkey::{ use crate::model::tsigkey::{
TsigKey, TsigKey,
}; };
pub async fn list(State(app_state): State<AppState>) -> impl IntoResponse { pub async fn list(
State(app_state): State<AppState>,
ValidAccessToken(token): ValidAccessToken,
) -> impl IntoResponse {
app_state.pdns_client.list_tsig_keys() app_state.pdns_client.list_tsig_keys()
.await .await
.and_then(|keys| Ok(Json(keys))) .and_then(|keys| Ok(Json(keys)))
} }
pub async fn get(State(app_state): State<AppState>, Path(tsig_key_name): Path<String>) -> impl IntoResponse { pub async fn get(
State(app_state): State<AppState>,
ValidAccessToken(token): ValidAccessToken,
Path(tsig_key_name): Path<String>
) -> impl IntoResponse {
app_state.pdns_client.get_tsig_key(tsig_key_name) app_state.pdns_client.get_tsig_key(tsig_key_name)
.await .await
.and_then(|key| Ok(Json(key))) .and_then(|key| Ok(Json(key)))
} }
pub async fn add(State(app_state): State<AppState>, Json(tsig_key): Json<TsigKey>) -> impl IntoResponse { pub async fn add(
State(app_state): State<AppState>,
ValidAccessToken(token): ValidAccessToken,
Json(tsig_key): Json<TsigKey>,
) -> impl IntoResponse {
app_state.pdns_client.add_tsig_key(tsig_key) app_state.pdns_client.add_tsig_key(tsig_key)
.await .await
.and_then(|key| Ok(Json(key))) .and_then(|key| Ok(Json(key)))
} }
pub async fn delete(State(app_state): State<AppState>, Path(tsig_key_name): Path<String>) -> impl IntoResponse { pub async fn delete(
State(app_state): State<AppState>,
ValidAccessToken(token): ValidAccessToken,
Path(tsig_key_name): Path<String>,
) -> impl IntoResponse {
app_state.pdns_client.delete_tsig_key(tsig_key_name) app_state.pdns_client.delete_tsig_key(tsig_key_name)
.await .await
.and_then(|_| Ok(Json(()))) .and_then(|_| Ok(Json(())))

View File

@ -4,12 +4,12 @@ use std::time::{
}; };
use askama::Template; use askama::Template;
use axum::response::IntoResponse; use axum::response::{
IntoResponse,
};
use axum::extract::State; use axum::extract::State;
use axum_extra::extract::cookie::CookieJar;
use openidconnect::{ use openidconnect::{
AccessToken,
reqwest::async_http_client, reqwest::async_http_client,
TokenIntrospectionResponse, TokenIntrospectionResponse,
}; };
@ -17,32 +17,35 @@ use openidconnect::{
use crate::util::askama::HtmlTemplate; use crate::util::askama::HtmlTemplate;
use crate::AppState; use crate::AppState;
use crate::error::openid::AuthError; use crate::error::openid::AuthError;
use crate::util::openid::AccessToken;
#[derive(Template)] #[derive(Template)]
#[template(path = "user_home.html")] #[template(path = "user_home.html")]
struct UserHomeTemplate { struct UserHomeTemplate {
is_active: bool, active: bool,
username: String, username: String,
duration: Duration, duration: Duration,
} }
pub async fn home( pub async fn home(
State(app_state): State<AppState>, State(app_state): State<AppState>,
cookies: CookieJar AccessToken(token): AccessToken,
) -> Result<impl IntoResponse, AuthError> { ) -> Result<impl IntoResponse, AuthError> {
let now = Instant::now(); let now = Instant::now();
let token_serialized: Option<String> = cookies.get("access_token") let response = app_state.oidc_client
.map(|cookie| cookie.value().to_owned()); .introspect(&token)?
let (is_active, username); .request_async(async_http_client)
(is_active, username) = match token_serialized { .await
Some(token) => { .map_err(|e| e.to_string())?;
let introspection_response = app_state.oidc_client match response.active() {
.introspect(&AccessToken::new(token))? true => {
.request_async(async_http_client).await.unwrap(); let username = response.username().unwrap().to_string();
println!("Token introspected, answer is {:?}", introspection_response); Ok(HtmlTemplate(UserHomeTemplate {
(introspection_response.active(), introspection_response.username().unwrap_or("").to_string()) active: true,
username,
duration: now.elapsed(),
}))
}, },
None => (false, "".to_string()) false => Err(AuthError::TokenNotActive(token)),
}; }
Ok(HtmlTemplate(UserHomeTemplate { is_active, username, duration: now.elapsed() }))
} }

View File

@ -9,7 +9,84 @@ use openidconnect::{
IssuerUrl, IssuerUrl,
RedirectUrl, RedirectUrl,
}; };
use openidconnect::TokenIntrospectionResponse;
use url::Url; use url::Url;
use axum::{
async_trait,
extract::{
FromRef,
FromRequestParts,
State,
},
http::{
StatusCode,
request::Parts,
},
response::Redirect,
};
use axum_extra::extract::cookie::CookieJar;
use crate::AppState;
pub(crate) struct AccessToken(pub(crate) openidconnect::AccessToken);
pub(crate) struct ValidAccessToken(pub(crate) openidconnect::AccessToken);
pub(crate) type AccessTokenRejection = (StatusCode, String);
#[async_trait]
impl<P> FromRequestParts<P> for AccessToken
where
P: Send + Sync
{
type Rejection = AccessTokenRejection;
async fn from_request_parts(parts: &mut Parts, state: &P) -> Result<Self, AccessTokenRejection> {
let cookie_name = "access_token";
let cookies = CookieJar::from_request_parts(parts, state).await.unwrap();
let token: Option<String> = cookies
.get(cookie_name)
.map(|cookie| cookie.value().to_owned());
match token {
Some(token) => Ok(Self(openidconnect::AccessToken::new(token))),
None => Err((
StatusCode::BAD_REQUEST,
format!(
"Request is missing the '{}' cookie",
cookie_name.to_string(),
)),
)
}
}
}
#[async_trait]
impl<P> FromRequestParts<P> for ValidAccessToken
where
AppState: FromRef<P>,
P: Send + Sync,
{
type Rejection = Redirect;
async fn from_request_parts(parts: &mut Parts, state: &P) -> Result<Self, Self::Rejection> {
match AccessToken::from_request_parts(parts, state).await {
Err(_) => Err(Redirect::to("/openid/login")),
Ok(AccessToken(token)) => {
match AppState::from_ref(state)
.oidc_client
.introspect(&token)
.map_err(|_| Redirect::to("/openid/login"))?
.request_async(async_http_client)
.await
.map_err(|_| Redirect::to("/openid/login")) {
Ok(t) => match t.active() {
true => Ok(ValidAccessToken(token)),
false => Err(Redirect::to("/openid/login")),
},
Err(e) => Err(e),
}
}
}
}
}
pub async fn create_client(issuer: Url, id: String, secret: String, redirect_url: Url) -> CoreClient { pub async fn create_client(issuer: Url, id: String, secret: String, redirect_url: Url) -> CoreClient {
let issuer_url = IssuerUrl::from_url(issuer); let issuer_url = IssuerUrl::from_url(issuer);

View File

@ -7,7 +7,7 @@
<h1>User Home</h1> <h1>User Home</h1>
{% endif %} {% endif %}
<p>Your user session is <b>{% if is_active %}active{% else %}inactive{% endif %}</b></p> <p>Your user session is <b>{% if active %}active{% else %}inactive{% endif %}</b></p>
<p>Request took <b>{{ duration.as_millis() }}</b>ms</p> <p>Request took <b>{{ duration.as_millis() }}</b>ms</p>
{% endblock content %} {% endblock content %}