feat: add AccessToken and ValidAccessToken axum extractors and use them in /api/tsigkey/
This commit is contained in:
parent
b49a4724a8
commit
87c4e424bc
@ -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(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(())))
|
||||||
|
@ -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() }))
|
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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 %}
|
||||||
|
Loading…
Reference in New Issue
Block a user