use refresh tokens

This commit is contained in:
Paul Zinselmeyer 2023-11-29 19:07:34 +01:00
parent 9ec50ba251
commit c9f04fe044
Signed by: pfzetto
GPG key ID: 4EEF46A5B276E648
3 changed files with 140 additions and 58 deletions

View file

@ -1,5 +1,7 @@
#![doc = include_str!("../README.md")] #![doc = include_str!("../README.md")]
use std::str::FromStr;
use crate::error::Error; use crate::error::Error;
use http::Uri; use http::Uri;
use openidconnect::{ use openidconnect::{
@ -11,14 +13,13 @@ use openidconnect::{
}, },
reqwest::async_http_client, reqwest::async_http_client,
ClientId, ClientSecret, CsrfToken, EmptyExtraTokenFields, IdTokenFields, IssuerUrl, Nonce, ClientId, ClientSecret, CsrfToken, EmptyExtraTokenFields, IdTokenFields, IssuerUrl, Nonce,
PkceCodeVerifier, StandardErrorResponse, StandardTokenResponse, PkceCodeVerifier, RefreshToken, StandardErrorResponse, StandardTokenResponse,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
pub mod error; pub mod error;
mod extractor; mod extractor;
mod middleware; mod middleware;
mod util;
pub use extractor::{OidcAccessToken, OidcClaims}; pub use extractor::{OidcAccessToken, OidcClaims};
pub use middleware::{OidcAuthLayer, OidcAuthMiddleware, OidcLoginLayer, OidcLoginMiddleware}; pub use middleware::{OidcAuthLayer, OidcAuthMiddleware, OidcLoginLayer, OidcLoginMiddleware};
@ -89,7 +90,7 @@ impl<AC: AdditionalClaims> OidcClient<AC> {
let client = Client::from_provider_metadata( let client = Client::from_provider_metadata(
provider_metadata, provider_metadata,
ClientId::new(client_id), ClientId::new(client_id),
client_secret.map(|x| ClientSecret::new(x)), client_secret.map(ClientSecret::new),
); );
Ok(Self { Ok(Self {
scopes, scopes,
@ -122,4 +123,18 @@ struct OidcSession {
pkce_verifier: PkceCodeVerifier, pkce_verifier: PkceCodeVerifier,
id_token: Option<String>, id_token: Option<String>,
access_token: Option<String>, access_token: Option<String>,
refresh_token: Option<String>,
}
impl OidcSession {
pub(crate) fn id_token<AC: AdditionalClaims>(&self) -> Option<IdToken<AC>> {
self.id_token
.as_ref()
.map(|x| IdToken::<AC>::from_str(x).unwrap())
}
pub(crate) fn refresh_token(&self) -> Option<RefreshToken> {
self.refresh_token
.as_ref()
.map(|x| RefreshToken::new(x.to_string()))
}
} }

View file

@ -10,21 +10,25 @@ use axum::{
}; };
use axum_core::{extract::FromRequestParts, response::Response}; use axum_core::{extract::FromRequestParts, response::Response};
use futures_util::future::BoxFuture; use futures_util::future::BoxFuture;
use http::{Request, Uri}; use http::{uri::PathAndQuery, Request, Uri};
use tower_layer::Layer; use tower_layer::Layer;
use tower_service::Service; use tower_service::Service;
use tower_sessions::Session; use tower_sessions::Session;
use openidconnect::{ use openidconnect::{
core::CoreAuthenticationFlow, reqwest::async_http_client, AccessTokenHash, AuthorizationCode, core::{
CsrfToken, Nonce, OAuth2TokenResponse, PkceCodeChallenge, PkceCodeVerifier, RedirectUrl, Scope, CoreAuthenticationFlow, CoreGenderClaim, CoreIdTokenFields, CoreJsonWebKeyType,
TokenResponse, CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm,
},
reqwest::async_http_client,
AccessTokenHash, AuthorizationCode, CsrfToken, ExtraTokenFields, IdTokenFields, Nonce,
OAuth2TokenResponse, PkceCodeChallenge, PkceCodeVerifier, RedirectUrl, RefreshToken, Scope,
StandardTokenResponse, TokenResponse, TokenType,
}; };
use crate::{ use crate::{
error::{Error, MiddlewareError}, error::{Error, MiddlewareError},
extractor::{OidcAccessToken, OidcClaims}, extractor::{OidcAccessToken, OidcClaims},
util::strip_oidc_from_path,
AdditionalClaims, BoxError, IdToken, OidcClient, OidcQuery, OidcSession, SESSION_KEY, AdditionalClaims, BoxError, IdToken, OidcClient, OidcQuery, OidcSession, SESSION_KEY,
}; };
@ -93,14 +97,16 @@ where
let mut inner = std::mem::replace(&mut self.inner, inner); let mut inner = std::mem::replace(&mut self.inner, inner);
if request.extensions().get::<OidcAccessToken>().is_some() { if request.extensions().get::<OidcAccessToken>().is_some() {
// the OidcAuthMiddleware had a valid id token
Box::pin(async move { Box::pin(async move {
let response: Response = inner let response: Response = inner
.call(request) .call(request)
.await .await
.map_err(|e| MiddlewareError::NextMiddleware(e.into()))?; .map_err(|e| MiddlewareError::NextMiddleware(e.into()))?;
return Ok(response); Ok(response)
}) })
} else { } else {
// no valid id token or refresh token was found and the user has to login
Box::pin(async move { Box::pin(async move {
let (mut parts, _) = request.into_parts(); let (mut parts, _) = request.into_parts();
@ -129,6 +135,9 @@ where
.set_redirect_uri(RedirectUrl::new(handler_uri.to_string())?); .set_redirect_uri(RedirectUrl::new(handler_uri.to_string())?);
if let (Some(mut login_session), Some(query)) = (login_session, query) { if let (Some(mut login_session), Some(query)) = (login_session, query) {
// the request has the request headers of the oidc redirect
// parse the headers and exchange the code for a valid token
if login_session.csrf_token.secret() != &query.state { if login_session.csrf_token.secret() != &query.state {
return Err(MiddlewareError::CsrfTokenInvalid); return Err(MiddlewareError::CsrfTokenInvalid);
} }
@ -165,11 +174,16 @@ where
login_session.id_token = Some(id_token.to_string()); login_session.id_token = Some(id_token.to_string());
login_session.access_token = login_session.access_token =
Some(token_response.access_token().secret().to_string()); Some(token_response.access_token().secret().to_string());
login_session.refresh_token = token_response
.refresh_token()
.map(|x| x.secret().to_string());
session.insert(SESSION_KEY, login_session).unwrap(); session.insert(SESSION_KEY, login_session).unwrap();
Ok(Redirect::temporary(&handler_uri.to_string()).into_response()) Ok(Redirect::temporary(&handler_uri.to_string()).into_response())
} else { } else {
// generate a login url and redirect the user to it
let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
let (auth_url, csrf_token, nonce) = { let (auth_url, csrf_token, nonce) = {
let mut auth = oidcclient.client.authorize_url( let mut auth = oidcclient.client.authorize_url(
@ -191,6 +205,7 @@ where
pkce_verifier, pkce_verifier,
id_token: None, id_token: None,
access_token: None, access_token: None,
refresh_token: None,
}; };
session.insert(SESSION_KEY, oidc_session).unwrap(); session.insert(SESSION_KEY, oidc_session).unwrap();
@ -293,7 +308,7 @@ where
.extensions .extensions
.get::<Session>() .get::<Session>()
.ok_or(MiddlewareError::SessionNotFound)?; .ok_or(MiddlewareError::SessionNotFound)?;
let login_session: Option<OidcSession> = let mut login_session: Option<OidcSession> =
session.get(SESSION_KEY).map_err(MiddlewareError::from)?; session.get(SESSION_KEY).map_err(MiddlewareError::from)?;
let handler_uri = let handler_uri =
@ -303,20 +318,75 @@ where
.client .client
.set_redirect_uri(RedirectUrl::new(handler_uri.to_string())?); .set_redirect_uri(RedirectUrl::new(handler_uri.to_string())?);
if let Some(OidcSession { if let Some(login_session) = &mut login_session {
nonce, let id_token_claims = login_session.id_token::<AC>().and_then(|id_token| {
csrf_token: _, id_token
pkce_verifier: _, .claims(&oidcclient.client.id_token_verifier(), &login_session.nonce)
id_token: Some(id_token), .ok()
access_token, .cloned()
}) = &login_session });
{
let id_token = IdToken::<AC>::from_str(&id_token).unwrap(); match (id_token_claims, login_session.refresh_token()) {
if let Ok(claims) = id_token.claims(&oidcclient.client.id_token_verifier(), nonce) { // stored id token is valid and can be used
(Some(claims), _) => {
parts.extensions.insert(OidcClaims(claims));
parts.extensions.insert(OidcAccessToken(
login_session.access_token.clone().unwrap_or_default(),
));
}
// stored id token is invalid and can't be uses, but we have a refresh token
// and can use it and try to get another id token.
(_, Some(refresh_token)) => {
let mut refresh_request =
oidcclient.client.exchange_refresh_token(&refresh_token);
for scope in oidcclient.scopes.iter() {
refresh_request =
refresh_request.add_scope(Scope::new(scope.to_string()));
}
let token_response =
refresh_request.request_async(async_http_client).await?;
// Extract the ID token claims after verifying its authenticity and nonce.
let id_token = token_response
.id_token()
.ok_or(MiddlewareError::IdTokenMissing)?;
let claims = id_token
.claims(&oidcclient.client.id_token_verifier(), &login_session.nonce)?;
// Verify the access token hash to ensure that the access token hasn't been substituted for
// another user's.
if let Some(expected_access_token_hash) = claims.access_token_hash() {
let actual_access_token_hash = AccessTokenHash::from_token(
token_response.access_token(),
&id_token.signing_alg()?,
)?;
if actual_access_token_hash != *expected_access_token_hash {
return Err(MiddlewareError::AccessTokenHashInvalid);
}
}
login_session.id_token = Some(id_token.to_string());
login_session.access_token =
Some(token_response.access_token().secret().to_string());
login_session.refresh_token = token_response
.refresh_token()
.map(|x| x.secret().to_string());
parts.extensions.insert(OidcClaims(claims.clone())); parts.extensions.insert(OidcClaims(claims.clone()));
parts parts.extensions.insert(OidcAccessToken(
login_session.access_token.clone().unwrap_or_default(),
));
let session = parts
.extensions .extensions
.insert(OidcAccessToken(access_token.clone().unwrap_or_default())); .get::<Session>()
.ok_or(MiddlewareError::SessionNotFound)?;
session.insert(SESSION_KEY, login_session).unwrap();
}
(None, None) => {}
} }
} }
@ -328,7 +398,37 @@ where
.await .await
.map_err(|e| MiddlewareError::NextMiddleware(e.into()))? .map_err(|e| MiddlewareError::NextMiddleware(e.into()))?
.into_response(); .into_response();
return Ok(response); Ok(response)
}) })
} }
} }
/// Helper function to remove the OpenID Connect authentication response query attributes from a
/// [`Uri`].
pub fn strip_oidc_from_path(base_url: Uri, uri: &Uri) -> Result<Uri, MiddlewareError> {
let mut base_url = base_url.into_parts();
base_url.path_and_query = uri
.path_and_query()
.map(|path_and_query| {
let query = path_and_query
.query()
.and_then(|uri| {
uri.split('&')
.filter(|x| {
!x.starts_with("code")
&& !x.starts_with("state")
&& !x.starts_with("session_state")
})
.map(|x| x.to_string())
.reduce(|acc, x| acc + "&" + &x)
})
.map(|x| format!("?{x}"))
.unwrap_or_default();
PathAndQuery::from_maybe_shared(format!("{}{}", path_and_query.path(), query))
})
.transpose()?;
Ok(Uri::from_parts(base_url)?)
}

View file

@ -1,33 +0,0 @@
use http::{uri::PathAndQuery, Uri};
use crate::error::MiddlewareError;
/// Helper function to remove the OpenID Connect authentication response query attributes from a
/// [`Uri`].
pub fn strip_oidc_from_path(base_url: Uri, uri: &Uri) -> Result<Uri, MiddlewareError> {
let mut base_url = base_url.into_parts();
base_url.path_and_query = uri
.path_and_query()
.map(|path_and_query| {
let query = path_and_query
.query()
.and_then(|uri| {
uri.split('&')
.filter(|x| {
!x.starts_with("code")
&& !x.starts_with("state")
&& !x.starts_with("session_state")
})
.map(|x| x.to_string())
.reduce(|acc, x| acc + "&" + &x)
})
.map(|x| "?" + x)
.unwrap_or_default();
PathAndQuery::from_maybe_shared(format!("{}{}", path_and_query.path(), query))
})
.transpose()?;
Ok(Uri::from_parts(base_url)?)
}