jwt validation
This commit is contained in:
parent
29689243f4
commit
89da8cc07f
9 changed files with 2721 additions and 281 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,2 +1,2 @@
|
||||||
/target
|
/target
|
||||||
/Cargo.lock
|
/result
|
||||||
|
|
2056
Cargo.lock
generated
Normal file
2056
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
17
Cargo.toml
17
Cargo.toml
|
@ -7,10 +7,21 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = "0.6"
|
axum = "0.6"
|
||||||
axum-extra = {version="0.7", features=["cookie", "cookie-private"]}
|
axum-extra = {version="0.7", features=["cookie", "cookie-private"], optional=true}
|
||||||
openidconnect = "3.0"
|
openidconnect = {version="3.0", optional=true}
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
serde = "1.0"
|
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
reqwest = { version="0.11", default_features=false}
|
reqwest = { version="0.11", default_features=false}
|
||||||
|
|
||||||
|
|
||||||
|
serde = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
|
||||||
|
jsonwebtoken = {version="^8.3", optional=true}
|
||||||
|
tower = {version="^0.4", optional=true}
|
||||||
|
futures-util = {version="^0.3",optional=true}
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = [ "jwt", "oidc" ]
|
||||||
|
oidc = [ "openidconnect", "axum-extra" ]
|
||||||
|
jwt = [ "tower", "jsonwebtoken", "futures-util", "reqwest/json", "reqwest/rustls-tls", "serde/derive" ]
|
||||||
|
|
129
flake.lock
Normal file
129
flake.lock
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"crane": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-compat": "flake-compat",
|
||||||
|
"flake-utils": [
|
||||||
|
"flake-utils"
|
||||||
|
],
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"rust-overlay": [
|
||||||
|
"rust-overlay"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1692750383,
|
||||||
|
"narHash": "sha256-n5P5HOXuu23UB1h9PuayldnRRVQuXJLpoO+xqtMO3ws=",
|
||||||
|
"owner": "ipetkov",
|
||||||
|
"repo": "crane",
|
||||||
|
"rev": "ef5d11e3c2e5b3924eb0309dba2e1fea2d9062ae",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "ipetkov",
|
||||||
|
"repo": "crane",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-compat": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1673956053,
|
||||||
|
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
|
||||||
|
"owner": "edolstra",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "edolstra",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1692799911,
|
||||||
|
"narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1693003285,
|
||||||
|
"narHash": "sha256-5nm4yrEHKupjn62MibENtfqlP6pWcRTuSKrMiH9bLkc=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "5690c4271f2998c304a45c91a0aeb8fb69feaea7",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"crane": "crane",
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"rust-overlay": "rust-overlay"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rust-overlay": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": [
|
||||||
|
"flake-utils"
|
||||||
|
],
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1693015707,
|
||||||
|
"narHash": "sha256-SFr93DYn502sVT9nB5U8/cKg1INyEk/jCeq8tHioz7Y=",
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"rev": "e90223633068a44f0fb62374e0fa360ccc987292",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
62
flake.nix
Normal file
62
flake.nix
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
{
|
||||||
|
description = "axum_oidc";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
rust-overlay = {
|
||||||
|
url = "github:oxalica/rust-overlay";
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.follows = "nixpkgs";
|
||||||
|
flake-utils.follows = "flake-utils";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
crane = {
|
||||||
|
url = "github:ipetkov/crane";
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.follows = "nixpkgs";
|
||||||
|
flake-utils.follows = "flake-utils";
|
||||||
|
rust-overlay.follows = "rust-overlay";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, flake-utils, rust-overlay, crane}:
|
||||||
|
flake-utils.lib.eachDefaultSystem
|
||||||
|
(system:
|
||||||
|
let
|
||||||
|
overlays = [ (import rust-overlay) ];
|
||||||
|
pkgs = import nixpkgs {
|
||||||
|
inherit system overlays;
|
||||||
|
};
|
||||||
|
|
||||||
|
rustToolchain = pkgs.rust-bin.stable.latest.default;
|
||||||
|
|
||||||
|
craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain;
|
||||||
|
src = craneLib.cleanCargoSource (craneLib.path ./.);
|
||||||
|
|
||||||
|
nativeBuildInputs = with pkgs; [ rustToolchain pkg-config ];
|
||||||
|
buildInputs = with pkgs; [ ];
|
||||||
|
|
||||||
|
commonArgs = {
|
||||||
|
inherit src buildInputs nativeBuildInputs;
|
||||||
|
};
|
||||||
|
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
|
||||||
|
|
||||||
|
bin = craneLib.buildPackage (commonArgs // {
|
||||||
|
inherit cargoArtifacts;
|
||||||
|
});
|
||||||
|
|
||||||
|
in
|
||||||
|
with pkgs;
|
||||||
|
{
|
||||||
|
packages = {
|
||||||
|
inherit bin;
|
||||||
|
default = bin;
|
||||||
|
};
|
||||||
|
devShells.default = mkShell {
|
||||||
|
inputsFrom = [ bin ];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
31
src/error.rs
31
src/error.rs
|
@ -1,11 +1,15 @@
|
||||||
use axum::response::{IntoResponse, Redirect};
|
use axum::response::{IntoResponse, Redirect};
|
||||||
|
use reqwest::StatusCode;
|
||||||
|
|
||||||
|
#[cfg(feature = "oidc")]
|
||||||
use axum_extra::extract::PrivateCookieJar;
|
use axum_extra::extract::PrivateCookieJar;
|
||||||
|
|
||||||
|
#[cfg(feature = "oidc")]
|
||||||
use openidconnect::{
|
use openidconnect::{
|
||||||
core::CoreErrorResponseType, url::ParseError, ClaimsVerificationError, DiscoveryError,
|
core::CoreErrorResponseType, url::ParseError, ClaimsVerificationError, DiscoveryError,
|
||||||
SigningError, StandardErrorResponse,
|
SigningError, StandardErrorResponse,
|
||||||
};
|
};
|
||||||
use reqwest::StatusCode;
|
#[cfg(feature = "oidc")]
|
||||||
|
|
||||||
type RequestTokenError = openidconnect::RequestTokenError<
|
type RequestTokenError = openidconnect::RequestTokenError<
|
||||||
openidconnect::reqwest::Error<reqwest::Error>,
|
openidconnect::reqwest::Error<reqwest::Error>,
|
||||||
StandardErrorResponse<CoreErrorResponseType>,
|
StandardErrorResponse<CoreErrorResponseType>,
|
||||||
|
@ -13,14 +17,23 @@ type RequestTokenError = openidconnect::RequestTokenError<
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
|
#[cfg(feature = "oidc")]
|
||||||
#[error("discovery error: {:?}", 0)]
|
#[error("discovery error: {:?}", 0)]
|
||||||
Discovery(#[from] DiscoveryError<openidconnect::reqwest::Error<reqwest::Error>>),
|
Discovery(#[from] DiscoveryError<openidconnect::reqwest::Error<reqwest::Error>>),
|
||||||
|
|
||||||
|
#[cfg(feature = "oidc")]
|
||||||
#[error("parse error: {:?}", 0)]
|
#[error("parse error: {:?}", 0)]
|
||||||
Parse(#[from] ParseError),
|
Parse(#[from] ParseError),
|
||||||
|
|
||||||
|
#[cfg(feature = "oidc")]
|
||||||
#[error("request token error: {:?}", 0)]
|
#[error("request token error: {:?}", 0)]
|
||||||
RequestToken(#[from] RequestTokenError),
|
RequestToken(#[from] RequestTokenError),
|
||||||
|
|
||||||
|
#[cfg(feature = "oidc")]
|
||||||
#[error("claims verification error: {:?}", 0)]
|
#[error("claims verification error: {:?}", 0)]
|
||||||
ClaimsVerification(#[from] ClaimsVerificationError),
|
ClaimsVerification(#[from] ClaimsVerificationError),
|
||||||
|
|
||||||
|
#[cfg(feature = "oidc")]
|
||||||
#[error("signing error: {:?}", 0)]
|
#[error("signing error: {:?}", 0)]
|
||||||
Signing(#[from] SigningError),
|
Signing(#[from] SigningError),
|
||||||
|
|
||||||
|
@ -30,26 +43,40 @@ pub enum Error {
|
||||||
#[error("url parsing error: {:?}", 0)]
|
#[error("url parsing error: {:?}", 0)]
|
||||||
UrlParsing(#[from] axum::http::Error),
|
UrlParsing(#[from] axum::http::Error),
|
||||||
|
|
||||||
|
#[cfg(feature = "oidc")]
|
||||||
#[error("csrf token is invalid")]
|
#[error("csrf token is invalid")]
|
||||||
CsrfTokenInvalid,
|
CsrfTokenInvalid,
|
||||||
|
|
||||||
|
#[cfg(feature = "oidc")]
|
||||||
#[error("id token not found")]
|
#[error("id token not found")]
|
||||||
IdTokenNotFound,
|
IdTokenNotFound,
|
||||||
|
|
||||||
|
#[cfg(feature = "oidc")]
|
||||||
#[error("access token hash is invalid")]
|
#[error("access token hash is invalid")]
|
||||||
AccessTokenHashInvalid,
|
AccessTokenHashInvalid,
|
||||||
|
|
||||||
|
#[cfg(feature = "oidc")]
|
||||||
#[error("just a redirect")]
|
#[error("just a redirect")]
|
||||||
Redirect((PrivateCookieJar, Redirect)),
|
Redirect((PrivateCookieJar, Redirect)),
|
||||||
|
|
||||||
|
#[error("reqwest: {0}")]
|
||||||
|
Reqwest(#[from] reqwest::Error),
|
||||||
|
|
||||||
|
#[cfg(feature = "jwt")]
|
||||||
|
#[error("jsonwebtoken: {0}")]
|
||||||
|
JsonWebToken(#[from] jsonwebtoken::errors::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoResponse for Error {
|
impl IntoResponse for Error {
|
||||||
fn into_response(self) -> axum::response::Response {
|
fn into_response(self) -> axum::response::Response {
|
||||||
match self {
|
match self {
|
||||||
|
#[cfg(feature = "oidc")]
|
||||||
Self::CsrfTokenInvalid => {
|
Self::CsrfTokenInvalid => {
|
||||||
{ (StatusCode::BAD_REQUEST, "csrf token is invalid").into_response() }
|
{ (StatusCode::BAD_REQUEST, "csrf token is invalid").into_response() }
|
||||||
.into_response()
|
.into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "oidc")]
|
||||||
Self::Redirect(redirect) => redirect.into_response(),
|
Self::Redirect(redirect) => redirect.into_response(),
|
||||||
_ => (StatusCode::INTERNAL_SERVER_ERROR, "internal server error").into_response(),
|
_ => (StatusCode::INTERNAL_SERVER_ERROR, "internal server error").into_response(),
|
||||||
}
|
}
|
||||||
|
|
140
src/jwt.rs
Normal file
140
src/jwt.rs
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
use std::{
|
||||||
|
marker::PhantomData,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
body::Body,
|
||||||
|
http::Request,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use futures_util::future::BoxFuture;
|
||||||
|
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
|
||||||
|
use reqwest::StatusCode;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use tower::{Layer, Service};
|
||||||
|
|
||||||
|
use crate::error::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct Claims<A: Clone> {
|
||||||
|
pub aud: Vec<String>,
|
||||||
|
pub exp: usize,
|
||||||
|
pub iat: usize,
|
||||||
|
pub iss: String,
|
||||||
|
pub sub: String,
|
||||||
|
pub azp: String,
|
||||||
|
|
||||||
|
pub name: String,
|
||||||
|
pub preferred_username: String,
|
||||||
|
pub given_name: String,
|
||||||
|
pub family_name: String,
|
||||||
|
pub email: String,
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub additional: A,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct JwtLayer<A: Clone> {
|
||||||
|
algorithm: Algorithm,
|
||||||
|
issuer: Vec<String>,
|
||||||
|
audience: Vec<String>,
|
||||||
|
pubkey: DecodingKey,
|
||||||
|
_a: PhantomData<A>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct IssuerDiscovery {
|
||||||
|
public_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: Clone> JwtLayer<A> {
|
||||||
|
pub async fn new(issuer: String, audience: String) -> Result<Self, Error> {
|
||||||
|
let issuer_key = reqwest::get(&issuer)
|
||||||
|
.await?
|
||||||
|
.json::<IssuerDiscovery>()
|
||||||
|
.await?
|
||||||
|
.public_key;
|
||||||
|
|
||||||
|
let pem = format!(
|
||||||
|
"-----BEGIN PUBLIC KEY-----\n{}\n-----END PUBLIC KEY-----",
|
||||||
|
issuer_key
|
||||||
|
);
|
||||||
|
|
||||||
|
let pubkey = DecodingKey::from_rsa_pem(pem.as_bytes())?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
algorithm: Algorithm::RS256,
|
||||||
|
issuer: vec![issuer],
|
||||||
|
audience: vec![audience],
|
||||||
|
pubkey,
|
||||||
|
_a: PhantomData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, A: Clone> Layer<S> for JwtLayer<A> {
|
||||||
|
type Service = JwtService<S, A>;
|
||||||
|
|
||||||
|
fn layer(&self, inner: S) -> Self::Service {
|
||||||
|
let mut validation = Validation::new(self.algorithm);
|
||||||
|
validation.set_issuer(&self.issuer);
|
||||||
|
validation.set_audience(&self.audience);
|
||||||
|
validation.validate_nbf = true;
|
||||||
|
JwtService {
|
||||||
|
validation,
|
||||||
|
pubkey: self.pubkey.clone(),
|
||||||
|
inner,
|
||||||
|
_a: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct JwtService<S, A: Clone> {
|
||||||
|
validation: Validation,
|
||||||
|
pubkey: DecodingKey,
|
||||||
|
inner: S,
|
||||||
|
_a: PhantomData<A>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, A: Clone> Service<Request<Body>> for JwtService<S, A>
|
||||||
|
where
|
||||||
|
S: Service<Request<Body>, Response = Response> + Send + 'static,
|
||||||
|
S::Future: Send + 'static,
|
||||||
|
A: Clone + for<'a> Deserialize<'a> + 'static + Sync + Send,
|
||||||
|
{
|
||||||
|
type Response = S::Response;
|
||||||
|
type Error = S::Error;
|
||||||
|
type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||||
|
|
||||||
|
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||||
|
self.inner.poll_ready(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn call(&mut self, mut req: Request<Body>) -> Self::Future {
|
||||||
|
let token = req
|
||||||
|
.headers()
|
||||||
|
.get("Authorization")
|
||||||
|
.and_then(|x| x.to_str().ok())
|
||||||
|
.map(|x| x.chars().skip(7).collect::<String>());
|
||||||
|
|
||||||
|
let token =
|
||||||
|
token.and_then(|x| decode::<Claims<A>>(&x, &self.pubkey, &self.validation).ok());
|
||||||
|
let token_exists = token.is_some();
|
||||||
|
|
||||||
|
if let Some(token) = token {
|
||||||
|
req.extensions_mut().insert(token.claims);
|
||||||
|
}
|
||||||
|
|
||||||
|
let future = self.inner.call(req);
|
||||||
|
Box::pin(async move {
|
||||||
|
if token_exists {
|
||||||
|
let response: Response = future.await?;
|
||||||
|
Ok(response)
|
||||||
|
} else {
|
||||||
|
Ok((StatusCode::UNAUTHORIZED, "access token invalid").into_response())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
279
src/lib.rs
279
src/lib.rs
|
@ -1,278 +1,7 @@
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use axum::{
|
|
||||||
extract::{FromRef, FromRequestParts, Query},
|
|
||||||
http::request::Parts,
|
|
||||||
response::Redirect,
|
|
||||||
};
|
|
||||||
use axum_extra::extract::{
|
|
||||||
cookie::{Cookie, SameSite},
|
|
||||||
PrivateCookieJar,
|
|
||||||
};
|
|
||||||
use error::Error;
|
|
||||||
use openidconnect::{
|
|
||||||
core::{
|
|
||||||
CoreAuthDisplay, CoreAuthPrompt, CoreAuthenticationFlow, CoreErrorResponseType,
|
|
||||||
CoreGenderClaim, CoreJsonWebKey, CoreJsonWebKeyType, CoreJsonWebKeyUse,
|
|
||||||
CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm, CoreProviderMetadata,
|
|
||||||
CoreRevocableToken, CoreRevocationErrorResponse, CoreTokenIntrospectionResponse,
|
|
||||||
CoreTokenType,
|
|
||||||
},
|
|
||||||
reqwest::async_http_client,
|
|
||||||
AccessTokenHash, AuthorizationCode, Client, ClientId, ClientSecret, CsrfToken,
|
|
||||||
EmptyExtraTokenFields, IdTokenClaims, IdTokenFields, IssuerUrl, Nonce, OAuth2TokenResponse,
|
|
||||||
PkceCodeChallenge, PkceCodeVerifier, RedirectUrl, Scope, StandardErrorResponse,
|
|
||||||
StandardTokenResponse, TokenResponse,
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
pub use axum::http::Uri;
|
|
||||||
pub use axum_extra::extract::cookie::Key;
|
|
||||||
|
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
|
||||||
const LOGIN_COOKIE_NAME: &str = "OIDC_LOGIN";
|
#[cfg(feature = "jwt")]
|
||||||
|
pub mod jwt;
|
||||||
|
|
||||||
pub trait AdditionalClaims: openidconnect::AdditionalClaims + Clone + Sync + Send {}
|
#[cfg(feature = "oidc")]
|
||||||
|
pub mod oidc;
|
||||||
type OidcTokenResponse<AC> = StandardTokenResponse<
|
|
||||||
IdTokenFields<
|
|
||||||
AC,
|
|
||||||
EmptyExtraTokenFields,
|
|
||||||
CoreGenderClaim,
|
|
||||||
CoreJweContentEncryptionAlgorithm,
|
|
||||||
CoreJwsSigningAlgorithm,
|
|
||||||
CoreJsonWebKeyType,
|
|
||||||
>,
|
|
||||||
CoreTokenType,
|
|
||||||
>;
|
|
||||||
|
|
||||||
pub type OidcClient<AC> = Client<
|
|
||||||
AC,
|
|
||||||
CoreAuthDisplay,
|
|
||||||
CoreGenderClaim,
|
|
||||||
CoreJweContentEncryptionAlgorithm,
|
|
||||||
CoreJwsSigningAlgorithm,
|
|
||||||
CoreJsonWebKeyType,
|
|
||||||
CoreJsonWebKeyUse,
|
|
||||||
CoreJsonWebKey,
|
|
||||||
CoreAuthPrompt,
|
|
||||||
StandardErrorResponse<CoreErrorResponseType>,
|
|
||||||
OidcTokenResponse<AC>,
|
|
||||||
CoreTokenType,
|
|
||||||
CoreTokenIntrospectionResponse,
|
|
||||||
CoreRevocableToken,
|
|
||||||
CoreRevocationErrorResponse,
|
|
||||||
>;
|
|
||||||
|
|
||||||
pub type IdToken<AZ> = openidconnect::IdToken<
|
|
||||||
AZ,
|
|
||||||
CoreGenderClaim,
|
|
||||||
CoreJweContentEncryptionAlgorithm,
|
|
||||||
CoreJwsSigningAlgorithm,
|
|
||||||
CoreJsonWebKeyType,
|
|
||||||
>;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct OidcApplication<AC: AdditionalClaims> {
|
|
||||||
application_base: Uri,
|
|
||||||
scopes: Vec<String>,
|
|
||||||
cookie_key: Key,
|
|
||||||
client: OidcClient<AC>,
|
|
||||||
}
|
|
||||||
impl<AC: AdditionalClaims> OidcApplication<AC> {
|
|
||||||
pub async fn create(
|
|
||||||
application_base: Uri,
|
|
||||||
issuer: String,
|
|
||||||
client_id: String,
|
|
||||||
client_secret: Option<String>,
|
|
||||||
scopes: Vec<String>,
|
|
||||||
cookie_key: Key,
|
|
||||||
) -> Result<Self, Error> {
|
|
||||||
let provider_metadata = CoreProviderMetadata::discover_async(
|
|
||||||
IssuerUrl::new(issuer).unwrap(),
|
|
||||||
async_http_client,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
let client = OidcClient::<AC>::from_provider_metadata(
|
|
||||||
provider_metadata,
|
|
||||||
ClientId::new(client_id),
|
|
||||||
client_secret.map(ClientSecret::new),
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
application_base,
|
|
||||||
scopes,
|
|
||||||
cookie_key,
|
|
||||||
client,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct EmptyAdditionalClaims {}
|
|
||||||
impl openidconnect::AdditionalClaims for EmptyAdditionalClaims {}
|
|
||||||
impl AdditionalClaims for EmptyAdditionalClaims {}
|
|
||||||
|
|
||||||
pub struct ClaimsExtractor<AC: AdditionalClaims>(pub IdTokenClaims<AC, CoreGenderClaim>);
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl<S, AC> FromRequestParts<S> for ClaimsExtractor<AC>
|
|
||||||
where
|
|
||||||
S: Send + Sync,
|
|
||||||
AC: AdditionalClaims,
|
|
||||||
OidcApplication<AC>: FromRef<S>,
|
|
||||||
{
|
|
||||||
type Rejection = Error;
|
|
||||||
|
|
||||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
|
||||||
let application: OidcApplication<AC> = OidcApplication::from_ref(state);
|
|
||||||
|
|
||||||
let handler_uri = Uri::builder()
|
|
||||||
.scheme(application.application_base.scheme().unwrap().clone())
|
|
||||||
.authority(application.application_base.authority().unwrap().clone())
|
|
||||||
.path_and_query(strip_oidc_from_path(&parts.uri))
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
let mut client = application.client;
|
|
||||||
client = client.set_redirect_uri(RedirectUrl::new(handler_uri.to_string())?);
|
|
||||||
let mut jar = PrivateCookieJar::from_headers(&parts.headers, application.cookie_key);
|
|
||||||
|
|
||||||
let login_session = jar.get(LOGIN_COOKIE_NAME);
|
|
||||||
let query = Query::<OidcQuery>::from_request_parts(parts, state)
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
if let Some(login_session) = &login_session {
|
|
||||||
let login_session: LoginSession = serde_json::from_str(login_session.value())?;
|
|
||||||
if let Some(access_token) = login_session.access_token {
|
|
||||||
let access_token = IdToken::<AC>::from_str(&access_token).unwrap();
|
|
||||||
if let Ok(claims) =
|
|
||||||
access_token.claims(&client.id_token_verifier(), &login_session.nonce)
|
|
||||||
{
|
|
||||||
return Ok(Self(claims.clone()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let (Some(login_session), Some(Query(query))) = (login_session, query) {
|
|
||||||
let mut login_session: LoginSession = serde_json::from_str(login_session.value())?;
|
|
||||||
|
|
||||||
if login_session.csrf_token.secret() != &query.state {
|
|
||||||
return Err(Error::CsrfTokenInvalid);
|
|
||||||
}
|
|
||||||
|
|
||||||
let token_response = client
|
|
||||||
.exchange_code(AuthorizationCode::new(query.code.to_string()))
|
|
||||||
// Set the PKCE code verifier.
|
|
||||||
.set_pkce_verifier(PkceCodeVerifier::new(
|
|
||||||
login_session.pkce_verifier.secret().to_string(),
|
|
||||||
))
|
|
||||||
.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(Error::IdTokenNotFound)?;
|
|
||||||
let claims = id_token.claims(&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(Error::AccessTokenHashInvalid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
login_session.access_token = Some(id_token.to_string());
|
|
||||||
|
|
||||||
let login_session = serde_json::to_string(&login_session)?;
|
|
||||||
jar = jar.add(create_cookie(login_session));
|
|
||||||
|
|
||||||
Err(Error::Redirect((
|
|
||||||
jar,
|
|
||||||
Redirect::temporary(
|
|
||||||
handler_uri
|
|
||||||
.path_and_query()
|
|
||||||
.map(|x| x.as_str())
|
|
||||||
.unwrap_or(handler_uri.path()),
|
|
||||||
),
|
|
||||||
)))
|
|
||||||
} else {
|
|
||||||
let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
|
|
||||||
let (auth_url, csrf_token, nonce) = {
|
|
||||||
let mut auth = client.authorize_url(
|
|
||||||
CoreAuthenticationFlow::AuthorizationCode,
|
|
||||||
CsrfToken::new_random,
|
|
||||||
Nonce::new_random,
|
|
||||||
);
|
|
||||||
|
|
||||||
for scope in application.scopes.iter() {
|
|
||||||
auth = auth.add_scope(Scope::new(scope.to_string()));
|
|
||||||
}
|
|
||||||
auth.set_pkce_challenge(pkce_challenge).url()
|
|
||||||
};
|
|
||||||
|
|
||||||
let login_session = LoginSession {
|
|
||||||
nonce,
|
|
||||||
csrf_token,
|
|
||||||
pkce_verifier,
|
|
||||||
access_token: None,
|
|
||||||
};
|
|
||||||
let login_session = serde_json::to_string(&login_session)?;
|
|
||||||
jar = jar.add(create_cookie(login_session));
|
|
||||||
|
|
||||||
Err(Error::Redirect((
|
|
||||||
jar,
|
|
||||||
Redirect::temporary(auth_url.as_str()),
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_cookie(login_session: String) -> Cookie<'static> {
|
|
||||||
let mut cookie = Cookie::new(LOGIN_COOKIE_NAME, login_session);
|
|
||||||
cookie.set_same_site(SameSite::Lax);
|
|
||||||
cookie.set_secure(true);
|
|
||||||
cookie.set_http_only(true);
|
|
||||||
cookie
|
|
||||||
}
|
|
||||||
|
|
||||||
fn strip_oidc_from_path(uri: &Uri) -> String {
|
|
||||||
let query = uri
|
|
||||||
.query()
|
|
||||||
.map(|uri| {
|
|
||||||
uri.split('&')
|
|
||||||
.filter(|x| {
|
|
||||||
!x.starts_with("code")
|
|
||||||
&& !x.starts_with("state")
|
|
||||||
&& !x.starts_with("session_state")
|
|
||||||
})
|
|
||||||
.fold(String::new(), |acc, x| acc + "&" + x)
|
|
||||||
.chars()
|
|
||||||
.skip(1)
|
|
||||||
.collect::<String>()
|
|
||||||
})
|
|
||||||
.unwrap_or_default();
|
|
||||||
uri.path().to_string() + &query
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct OidcQuery {
|
|
||||||
code: String,
|
|
||||||
state: String,
|
|
||||||
#[allow(dead_code)]
|
|
||||||
session_state: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
struct LoginSession {
|
|
||||||
nonce: Nonce,
|
|
||||||
csrf_token: CsrfToken,
|
|
||||||
pkce_verifier: PkceCodeVerifier,
|
|
||||||
access_token: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
286
src/oidc.rs
Normal file
286
src/oidc.rs
Normal file
|
@ -0,0 +1,286 @@
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use axum::{
|
||||||
|
extract::{FromRef, FromRequestParts, Query},
|
||||||
|
http::{request::Parts, Uri},
|
||||||
|
response::Redirect,
|
||||||
|
};
|
||||||
|
pub use axum_extra::extract::cookie::Key;
|
||||||
|
use axum_extra::extract::{
|
||||||
|
cookie::{Cookie, SameSite},
|
||||||
|
PrivateCookieJar,
|
||||||
|
};
|
||||||
|
use error::Error;
|
||||||
|
use openidconnect::{
|
||||||
|
core::{
|
||||||
|
CoreAuthDisplay, CoreAuthPrompt, CoreAuthenticationFlow, CoreErrorResponseType,
|
||||||
|
CoreGenderClaim, CoreJsonWebKey, CoreJsonWebKeyType, CoreJsonWebKeyUse,
|
||||||
|
CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm, CoreProviderMetadata,
|
||||||
|
CoreRevocableToken, CoreRevocationErrorResponse, CoreTokenIntrospectionResponse,
|
||||||
|
CoreTokenType,
|
||||||
|
},
|
||||||
|
reqwest::async_http_client,
|
||||||
|
AccessTokenHash, AuthorizationCode, Client, ClientId, ClientSecret, CsrfToken,
|
||||||
|
EmptyExtraTokenFields, IdTokenClaims, IdTokenFields, IssuerUrl, Nonce, OAuth2TokenResponse,
|
||||||
|
PkceCodeChallenge, PkceCodeVerifier, RedirectUrl, Scope, StandardErrorResponse,
|
||||||
|
StandardTokenResponse, TokenResponse,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::error;
|
||||||
|
|
||||||
|
const LOGIN_COOKIE_NAME: &str = "OIDC_LOGIN";
|
||||||
|
|
||||||
|
pub trait AdditionalClaims: openidconnect::AdditionalClaims + Clone + Sync + Send {}
|
||||||
|
|
||||||
|
type OidcTokenResponse<AC> = StandardTokenResponse<
|
||||||
|
IdTokenFields<
|
||||||
|
AC,
|
||||||
|
EmptyExtraTokenFields,
|
||||||
|
CoreGenderClaim,
|
||||||
|
CoreJweContentEncryptionAlgorithm,
|
||||||
|
CoreJwsSigningAlgorithm,
|
||||||
|
CoreJsonWebKeyType,
|
||||||
|
>,
|
||||||
|
CoreTokenType,
|
||||||
|
>;
|
||||||
|
|
||||||
|
pub type OidcClient<AC> = Client<
|
||||||
|
AC,
|
||||||
|
CoreAuthDisplay,
|
||||||
|
CoreGenderClaim,
|
||||||
|
CoreJweContentEncryptionAlgorithm,
|
||||||
|
CoreJwsSigningAlgorithm,
|
||||||
|
CoreJsonWebKeyType,
|
||||||
|
CoreJsonWebKeyUse,
|
||||||
|
CoreJsonWebKey,
|
||||||
|
CoreAuthPrompt,
|
||||||
|
StandardErrorResponse<CoreErrorResponseType>,
|
||||||
|
OidcTokenResponse<AC>,
|
||||||
|
CoreTokenType,
|
||||||
|
CoreTokenIntrospectionResponse,
|
||||||
|
CoreRevocableToken,
|
||||||
|
CoreRevocationErrorResponse,
|
||||||
|
>;
|
||||||
|
|
||||||
|
pub type IdToken<AZ> = openidconnect::IdToken<
|
||||||
|
AZ,
|
||||||
|
CoreGenderClaim,
|
||||||
|
CoreJweContentEncryptionAlgorithm,
|
||||||
|
CoreJwsSigningAlgorithm,
|
||||||
|
CoreJsonWebKeyType,
|
||||||
|
>;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct OidcApplication<AC: AdditionalClaims> {
|
||||||
|
application_base: Uri,
|
||||||
|
scopes: Vec<String>,
|
||||||
|
cookie_key: Key,
|
||||||
|
client: OidcClient<AC>,
|
||||||
|
}
|
||||||
|
impl<AC: AdditionalClaims> OidcApplication<AC> {
|
||||||
|
pub async fn create(
|
||||||
|
application_base: Uri,
|
||||||
|
issuer: String,
|
||||||
|
client_id: String,
|
||||||
|
client_secret: Option<String>,
|
||||||
|
scopes: Vec<String>,
|
||||||
|
cookie_key: Key,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
let provider_metadata = CoreProviderMetadata::discover_async(
|
||||||
|
IssuerUrl::new(issuer).unwrap(),
|
||||||
|
async_http_client,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let client = OidcClient::<AC>::from_provider_metadata(
|
||||||
|
provider_metadata,
|
||||||
|
ClientId::new(client_id),
|
||||||
|
client_secret.map(ClientSecret::new),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
application_base,
|
||||||
|
scopes,
|
||||||
|
cookie_key,
|
||||||
|
client,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct EmptyAdditionalClaims {}
|
||||||
|
impl openidconnect::AdditionalClaims for EmptyAdditionalClaims {}
|
||||||
|
impl AdditionalClaims for EmptyAdditionalClaims {}
|
||||||
|
|
||||||
|
pub struct OidcExtractor<AC: AdditionalClaims> {
|
||||||
|
pub claims: IdTokenClaims<AC, CoreGenderClaim>,
|
||||||
|
pub access_token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<S, AC> FromRequestParts<S> for OidcExtractor<AC>
|
||||||
|
where
|
||||||
|
S: Send + Sync,
|
||||||
|
AC: AdditionalClaims,
|
||||||
|
OidcApplication<AC>: FromRef<S>,
|
||||||
|
{
|
||||||
|
type Rejection = Error;
|
||||||
|
|
||||||
|
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||||
|
let application: OidcApplication<AC> = OidcApplication::from_ref(state);
|
||||||
|
|
||||||
|
let handler_uri = Uri::builder()
|
||||||
|
.scheme(application.application_base.scheme().unwrap().clone())
|
||||||
|
.authority(application.application_base.authority().unwrap().clone())
|
||||||
|
.path_and_query(strip_oidc_from_path(&parts.uri))
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
let mut client = application.client;
|
||||||
|
client = client.set_redirect_uri(RedirectUrl::new(handler_uri.to_string())?);
|
||||||
|
let mut jar = PrivateCookieJar::from_headers(&parts.headers, application.cookie_key);
|
||||||
|
|
||||||
|
let login_session = jar.get(LOGIN_COOKIE_NAME);
|
||||||
|
let query = Query::<OidcQuery>::from_request_parts(parts, state)
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
if let Some(login_session) = &login_session {
|
||||||
|
let login_session: LoginSession = serde_json::from_str(login_session.value())?;
|
||||||
|
if let Some(id_token) = login_session.id_token {
|
||||||
|
let id_token = IdToken::<AC>::from_str(&id_token).unwrap();
|
||||||
|
if let Ok(claims) =
|
||||||
|
id_token.claims(&client.id_token_verifier(), &login_session.nonce)
|
||||||
|
{
|
||||||
|
return Ok(Self {
|
||||||
|
claims: claims.clone(),
|
||||||
|
access_token: login_session.access_token.unwrap_or_default(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let (Some(login_session), Some(Query(query))) = (login_session, query) {
|
||||||
|
let mut login_session: LoginSession = serde_json::from_str(login_session.value())?;
|
||||||
|
|
||||||
|
if login_session.csrf_token.secret() != &query.state {
|
||||||
|
return Err(Error::CsrfTokenInvalid);
|
||||||
|
}
|
||||||
|
|
||||||
|
let token_response = client
|
||||||
|
.exchange_code(AuthorizationCode::new(query.code.to_string()))
|
||||||
|
// Set the PKCE code verifier.
|
||||||
|
.set_pkce_verifier(PkceCodeVerifier::new(
|
||||||
|
login_session.pkce_verifier.secret().to_string(),
|
||||||
|
))
|
||||||
|
.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(Error::IdTokenNotFound)?;
|
||||||
|
let claims = id_token.claims(&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(Error::AccessTokenHashInvalid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
login_session.id_token = Some(id_token.to_string());
|
||||||
|
login_session.access_token = Some(token_response.access_token().secret().to_string());
|
||||||
|
|
||||||
|
let login_session = serde_json::to_string(&login_session)?;
|
||||||
|
jar = jar.add(create_cookie(login_session));
|
||||||
|
|
||||||
|
Err(Error::Redirect((
|
||||||
|
jar,
|
||||||
|
Redirect::temporary(
|
||||||
|
handler_uri
|
||||||
|
.path_and_query()
|
||||||
|
.map(|x| x.as_str())
|
||||||
|
.unwrap_or(handler_uri.path()),
|
||||||
|
),
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
|
||||||
|
let (auth_url, csrf_token, nonce) = {
|
||||||
|
let mut auth = client.authorize_url(
|
||||||
|
CoreAuthenticationFlow::AuthorizationCode,
|
||||||
|
CsrfToken::new_random,
|
||||||
|
Nonce::new_random,
|
||||||
|
);
|
||||||
|
|
||||||
|
for scope in application.scopes.iter() {
|
||||||
|
auth = auth.add_scope(Scope::new(scope.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
auth.set_pkce_challenge(pkce_challenge).url()
|
||||||
|
};
|
||||||
|
|
||||||
|
let login_session = LoginSession {
|
||||||
|
nonce,
|
||||||
|
csrf_token,
|
||||||
|
pkce_verifier,
|
||||||
|
id_token: None,
|
||||||
|
access_token: None,
|
||||||
|
};
|
||||||
|
let login_session = serde_json::to_string(&login_session)?;
|
||||||
|
jar = jar.add(create_cookie(login_session));
|
||||||
|
|
||||||
|
Err(Error::Redirect((
|
||||||
|
jar,
|
||||||
|
Redirect::temporary(auth_url.as_str()),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_cookie(login_session: String) -> Cookie<'static> {
|
||||||
|
let mut cookie = Cookie::new(LOGIN_COOKIE_NAME, login_session);
|
||||||
|
cookie.set_same_site(SameSite::Lax);
|
||||||
|
cookie.set_secure(true);
|
||||||
|
cookie.set_http_only(true);
|
||||||
|
cookie
|
||||||
|
}
|
||||||
|
|
||||||
|
fn strip_oidc_from_path(uri: &Uri) -> String {
|
||||||
|
let query = uri
|
||||||
|
.query()
|
||||||
|
.map(|uri| {
|
||||||
|
uri.split('&')
|
||||||
|
.filter(|x| {
|
||||||
|
!x.starts_with("code")
|
||||||
|
&& !x.starts_with("state")
|
||||||
|
&& !x.starts_with("session_state")
|
||||||
|
})
|
||||||
|
.fold(String::new(), |acc, x| acc + "&" + x)
|
||||||
|
.chars()
|
||||||
|
.skip(1)
|
||||||
|
.collect::<String>()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
uri.path().to_string() + &query
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct OidcQuery {
|
||||||
|
code: String,
|
||||||
|
state: String,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
session_state: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct LoginSession {
|
||||||
|
nonce: Nonce,
|
||||||
|
csrf_token: CsrfToken,
|
||||||
|
pkce_verifier: PkceCodeVerifier,
|
||||||
|
id_token: Option<String>,
|
||||||
|
access_token: Option<String>,
|
||||||
|
}
|
Reference in a new issue