feat: add token introspection on sample /user/home endpoint and inject access_token into client cookie
This commit is contained in:
parent
436dbe1393
commit
2fc6caad1c
12
Cargo.lock
generated
12
Cargo.lock
generated
@ -165,6 +165,7 @@ dependencies = [
|
|||||||
"axum",
|
"axum",
|
||||||
"axum-core",
|
"axum-core",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"cookie",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http 1.1.0",
|
"http 1.1.0",
|
||||||
"http-body 1.0.1",
|
"http-body 1.0.1",
|
||||||
@ -372,6 +373,17 @@ dependencies = [
|
|||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cookie"
|
||||||
|
version = "0.18.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
|
||||||
|
dependencies = [
|
||||||
|
"percent-encoding",
|
||||||
|
"time",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
|
@ -9,7 +9,7 @@ edition = "2021"
|
|||||||
askama = "0.12.1"
|
askama = "0.12.1"
|
||||||
axum = { version = "0.7.5", features = ["tracing", "multipart", "macros", "http2"] }
|
axum = { version = "0.7.5", features = ["tracing", "multipart", "macros", "http2"] }
|
||||||
axum-core = { version = "0.4.3", features = ["tracing"] }
|
axum-core = { version = "0.4.3", features = ["tracing"] }
|
||||||
axum-extra = { version = "0.9.3", features = ["form"] }
|
axum-extra = { version = "0.9.3", features = ["cookie", "form"] }
|
||||||
config = "0.14.0"
|
config = "0.14.0"
|
||||||
http = "1.1.0"
|
http = "1.1.0"
|
||||||
openidconnect = "3.5.0"
|
openidconnect = "3.5.0"
|
||||||
|
@ -11,3 +11,4 @@ oidc:
|
|||||||
client_id: client-id-you-generated
|
client_id: client-id-you-generated
|
||||||
client_secret: client-secret-that-you-generated-or-copied
|
client_secret: client-secret-that-you-generated-or-copied
|
||||||
callback: https://the-public-domain-of-this.service.tld/openid/callback
|
callback: https://the-public-domain-of-this.service.tld/openid/callback
|
||||||
|
introspection_url: [...]
|
||||||
|
@ -27,6 +27,7 @@ pub struct OidcConfig {
|
|||||||
pub client_id: String,
|
pub client_id: String,
|
||||||
pub client_secret: String,
|
pub client_secret: String,
|
||||||
pub callback: Url,
|
pub callback: Url,
|
||||||
|
pub introspection_url: Url,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
@ -1 +1,2 @@
|
|||||||
|
pub mod openid;
|
||||||
pub mod powerdns;
|
pub mod powerdns;
|
||||||
|
34
src/error/openid.rs
Normal file
34
src/error/openid.rs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use axum::response::IntoResponse;
|
||||||
|
use reqwest::StatusCode;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum AuthError {
|
||||||
|
OpenIdConfig(openidconnect::ConfigurationError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for AuthError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
AuthError::OpenIdConfig(e) => f.write_fmt(format_args!("OpenID Connect configuration error: {:?}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for AuthError {}
|
||||||
|
|
||||||
|
impl From<openidconnect::ConfigurationError> for AuthError {
|
||||||
|
fn from(e: openidconnect::ConfigurationError) -> Self {
|
||||||
|
Self::OpenIdConfig(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for AuthError {
|
||||||
|
fn into_response(self) -> axum::response::Response {
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("Authentication error: {}", self),
|
||||||
|
).into_response()
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
use axum::response::{
|
use axum::response::{
|
||||||
IntoResponse,
|
IntoResponse,
|
||||||
Redirect,
|
Redirect,
|
||||||
|
Response,
|
||||||
};
|
};
|
||||||
use axum::extract::{
|
use axum::extract::{
|
||||||
State,
|
State,
|
||||||
@ -47,19 +48,19 @@ pub async fn callback(
|
|||||||
State(app_state): State<AppState>,
|
State(app_state): State<AppState>,
|
||||||
Query(callback): Query<OidcCallback>,
|
Query(callback): Query<OidcCallback>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
println!("Received auth code: {}", callback.code.clone());
|
|
||||||
let token_response = app_state.oidc_client
|
let token_response = app_state.oidc_client
|
||||||
.exchange_code(AuthorizationCode::new(callback.code.to_owned()))
|
.exchange_code(AuthorizationCode::new(callback.code.to_owned()))
|
||||||
.request_async(async_http_client)
|
.request_async(async_http_client)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
println!("Exchanged for access token: {}", token_response.access_token().secret());
|
Response::builder()
|
||||||
println!("\tvalid for scopes {:?}", token_response.scopes());
|
.status(200)
|
||||||
Redirect::to("/user/home")
|
// TODO: set 'Secure' flag when served over https
|
||||||
// let id_token_verifier: CoreIdTokenVerifier = app_state.oidc_client.id_token_verifier();
|
.header("Set-Cookie", format!(
|
||||||
// let id_token_claims: &CoreIdTokenClaims = token_response
|
"access_token={}; SameSite=Strict; HttpOnly; Path=/;",
|
||||||
// .extra_fields()
|
token_response.access_token().secret(),
|
||||||
// .id_token()
|
))
|
||||||
// .expect("IdP did not return an ID token")
|
.header("Content-Type", "text/html")
|
||||||
// .claims(&id_token_verifier, &nonce)
|
.body("Click here to go to your <a href=\"/user/home\">User Home</a>".to_string())
|
||||||
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,48 @@
|
|||||||
|
use std::time::{
|
||||||
|
Duration,
|
||||||
|
Instant,
|
||||||
|
};
|
||||||
|
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
|
use axum::extract::State;
|
||||||
|
use axum_extra::extract::cookie::CookieJar;
|
||||||
|
|
||||||
|
use openidconnect::{
|
||||||
|
AccessToken,
|
||||||
|
reqwest::async_http_client,
|
||||||
|
TokenIntrospectionResponse,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::util::askama::HtmlTemplate;
|
use crate::util::askama::HtmlTemplate;
|
||||||
|
use crate::AppState;
|
||||||
|
use crate::error::openid::AuthError;
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "user_home.html")]
|
#[template(path = "user_home.html")]
|
||||||
struct UserHomeTemplate {}
|
struct UserHomeTemplate {
|
||||||
|
is_active: bool,
|
||||||
pub async fn home() -> impl IntoResponse {
|
username: String,
|
||||||
HtmlTemplate(UserHomeTemplate {})
|
duration: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn home(
|
||||||
|
State(app_state): State<AppState>,
|
||||||
|
cookies: CookieJar
|
||||||
|
) -> Result<impl IntoResponse, AuthError> {
|
||||||
|
let now = Instant::now();
|
||||||
|
let token_serialized: Option<String> = cookies.get("access_token")
|
||||||
|
.map(|cookie| cookie.value().to_owned());
|
||||||
|
let (is_active, username);
|
||||||
|
(is_active, username) = match token_serialized {
|
||||||
|
Some(token) => {
|
||||||
|
let introspection_response = app_state.oidc_client
|
||||||
|
.introspect(&AccessToken::new(token))?
|
||||||
|
.request_async(async_http_client).await.unwrap();
|
||||||
|
println!("Token introspected, answer is {:?}", introspection_response);
|
||||||
|
(introspection_response.active(), introspection_response.username().unwrap_or("").to_string())
|
||||||
|
},
|
||||||
|
None => (false, "".to_string())
|
||||||
|
};
|
||||||
|
Ok(HtmlTemplate(UserHomeTemplate { is_active, username, duration: now.elapsed() }))
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,10 @@ use tokio::sync::Mutex;
|
|||||||
|
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use axum::routing::{get, post};
|
use axum::routing::{get, post};
|
||||||
use openidconnect::core::CoreClient;
|
use openidconnect::{
|
||||||
|
IntrospectionUrl,
|
||||||
|
core::CoreClient,
|
||||||
|
};
|
||||||
|
|
||||||
pub mod model;
|
pub mod model;
|
||||||
mod config;
|
mod config;
|
||||||
@ -32,7 +35,8 @@ async fn main() {
|
|||||||
config.oidc.client_id,
|
config.oidc.client_id,
|
||||||
config.oidc.client_secret,
|
config.oidc.client_secret,
|
||||||
config.oidc.callback,
|
config.oidc.callback,
|
||||||
).await;
|
).await
|
||||||
|
.set_introspection_uri(IntrospectionUrl::from_url(config.oidc.introspection_url));
|
||||||
|
|
||||||
let pdns_client = util::powerdns::PowerDnsApi::new(
|
let pdns_client = util::powerdns::PowerDnsApi::new(
|
||||||
config.powerdns.server_url,
|
config.powerdns.server_url,
|
||||||
|
@ -1,5 +1,13 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
{% if username != "" %}
|
||||||
|
<h1>Hello <i>{{ username }}</i></h1>
|
||||||
|
{% else %}
|
||||||
<h1>User Home</h1>
|
<h1>User Home</h1>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p>Your user session is <b>{% if is_active %}active{% else %}inactive{% endif %}</b></p>
|
||||||
|
|
||||||
|
<p>Request took <b>{{ duration.as_millis() }}</b>ms</p>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
Loading…
Reference in New Issue
Block a user