From 80619164e3db34f70d4c4bce3a4f5bb32963a738 Mon Sep 17 00:00:00 2001 From: Johanna Dorothea Reichmann Date: Sun, 8 Sep 2024 20:47:11 +0200 Subject: [PATCH] feat: add token introspection on sample /user/home endpoint and inject access_token into client cookie --- Cargo.lock | 12 +++++++++++ Cargo.toml | 2 +- config.example.yml | 1 + src/config.rs | 1 + src/error/mod.rs | 1 + src/error/openid.rs | 43 +++++++++++++++++++++++++++++++++++++++ src/handlers/openid.rs | 22 +++++++++++--------- src/handlers/user.rs | 44 ++++++++++++++++++++++++++++++++++++---- src/main.rs | 8 ++++++-- templates/user_home.html | 10 ++++++++- 10 files changed, 126 insertions(+), 18 deletions(-) create mode 100644 src/error/openid.rs diff --git a/Cargo.lock b/Cargo.lock index c233d35..0fdbc01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -165,6 +165,7 @@ dependencies = [ "axum", "axum-core", "bytes", + "cookie", "futures-util", "http 1.1.0", "http-body 1.0.1", @@ -372,6 +373,17 @@ dependencies = [ "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]] name = "core-foundation" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index 01861f1..5887ea1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ edition = "2021" askama = "0.12.1" axum = { version = "0.7.5", features = ["tracing", "multipart", "macros", "http2"] } 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" http = "1.1.0" openidconnect = "3.5.0" diff --git a/config.example.yml b/config.example.yml index e406e3b..153f047 100644 --- a/config.example.yml +++ b/config.example.yml @@ -11,3 +11,4 @@ oidc: client_id: client-id-you-generated client_secret: client-secret-that-you-generated-or-copied callback: https://the-public-domain-of-this.service.tld/openid/callback + introspection_url: [...] diff --git a/src/config.rs b/src/config.rs index 20d0c8f..09ba94f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -27,6 +27,7 @@ pub struct OidcConfig { pub client_id: String, pub client_secret: String, pub callback: Url, + pub introspection_url: Url, } #[derive(Debug, Deserialize, Clone)] diff --git a/src/error/mod.rs b/src/error/mod.rs index e64ec54..c48b64e 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -1 +1,2 @@ +pub mod openid; pub mod powerdns; diff --git a/src/error/openid.rs b/src/error/openid.rs new file mode 100644 index 0000000..994fc94 --- /dev/null +++ b/src/error/openid.rs @@ -0,0 +1,43 @@ +use std::fmt::Display; + +use axum::response::IntoResponse; +use reqwest::StatusCode; + +#[derive(Debug)] +pub enum AuthError { + OpenIdConfig(openidconnect::ConfigurationError), +// ReqTokenError(openidconnect::RequestTokenError), +} + +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)), +// AuthError::ReqTokenError(e) => f.write_fmt(format_args!("Request token error: {:?}", e)), + } + } +} + +impl std::error::Error for AuthError {} + +impl From for AuthError { + fn from(e: openidconnect::ConfigurationError) -> Self { + Self::OpenIdConfig(e) + } +} + +//impl From> for AuthError { +// fn from(e: openidconnect::RequestTokenError) -> Self { +// Self::ReqTokenError(e) +// } +//} + + +impl IntoResponse for AuthError { + fn into_response(self) -> axum::response::Response { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Authentication error: {}", self), + ).into_response() + } +} diff --git a/src/handlers/openid.rs b/src/handlers/openid.rs index b752dbd..e61e273 100644 --- a/src/handlers/openid.rs +++ b/src/handlers/openid.rs @@ -1,6 +1,7 @@ use axum::response::{ IntoResponse, Redirect, + Response, }; use axum::extract::{ State, @@ -47,19 +48,20 @@ pub async fn callback( State(app_state): State, Query(callback): Query, ) -> impl IntoResponse { - println!("Received auth code: {}", callback.code.clone()); let token_response = app_state.oidc_client .exchange_code(AuthorizationCode::new(callback.code.to_owned())) .request_async(async_http_client) .await .unwrap(); - println!("Exchanged for access token: {}", token_response.access_token().secret()); - println!("\tvalid for scopes {:?}", token_response.scopes()); - Redirect::to("/user/home") -// let id_token_verifier: CoreIdTokenVerifier = app_state.oidc_client.id_token_verifier(); -// let id_token_claims: &CoreIdTokenClaims = token_response -// .extra_fields() -// .id_token() -// .expect("IdP did not return an ID token") -// .claims(&id_token_verifier, &nonce) + Response::builder() + .status(200) + // TODO: set 'Secure' flag when served over https + .header("Set-Cookie", format!( + "access_token={}; SameSite=Strict; HttpOnly; Path=/;", + token_response.access_token().secret(), + )) + //.header("Location", "/user/home") + .header("Content-Type", "text/html") + .body("Click here to go to your User Home".to_string()) + .unwrap() } diff --git a/src/handlers/user.rs b/src/handlers/user.rs index d5eef91..14afb29 100644 --- a/src/handlers/user.rs +++ b/src/handlers/user.rs @@ -1,12 +1,48 @@ +use std::time::{ + Duration, + Instant, +}; + use askama::Template; 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::AppState; +use crate::error::openid::AuthError; #[derive(Template)] #[template(path = "user_home.html")] -struct UserHomeTemplate {} - -pub async fn home() -> impl IntoResponse { - HtmlTemplate(UserHomeTemplate {}) +struct UserHomeTemplate { + is_active: bool, + username: String, + duration: Duration, +} + +pub async fn home( + State(app_state): State, + cookies: CookieJar +) -> Result { + let now = Instant::now(); + let token_serialized: Option = 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() })) } diff --git a/src/main.rs b/src/main.rs index b2a29ca..44fc73a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,10 @@ use tokio::sync::Mutex; use axum::Router; use axum::routing::{get, post}; -use openidconnect::core::CoreClient; +use openidconnect::{ + IntrospectionUrl, + core::CoreClient, +}; pub mod model; mod config; @@ -32,7 +35,8 @@ async fn main() { config.oidc.client_id, config.oidc.client_secret, config.oidc.callback, - ).await; + ).await + .set_introspection_uri(IntrospectionUrl::from_url(config.oidc.introspection_url)); let pdns_client = util::powerdns::PowerDnsApi::new( config.powerdns.server_url, diff --git a/templates/user_home.html b/templates/user_home.html index 8cc3128..c3cfdcd 100644 --- a/templates/user_home.html +++ b/templates/user_home.html @@ -1,5 +1,13 @@ {% extends "base.html" %} {% block content %} -

User Home

+{% if username != "" %} +

Hello {{ username }}

+{% else %} +

User Home

+{% endif %} + +

Your user session is {% if is_active %}active{% else %}inactive{% endif %}

+ +

Request took {{ duration.as_millis() }}ms

{% endblock content %}