feat: add token introspection on sample /user/home endpoint and inject access_token into client cookie
This commit is contained in:
		
							
								
								
									
										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 %}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user