mirror of
https://github.com/pfzetto/axum-oidc.git
synced 2024-11-24 12:32:49 +01:00
Compare commits
2 commits
43406661f6
...
e62aba722c
Author | SHA1 | Date | |
---|---|---|---|
e62aba722c | |||
c9f63180b3 |
8 changed files with 483 additions and 91 deletions
11
.github/workflows/ci.yml
vendored
11
.github/workflows/ci.yml
vendored
|
@ -21,14 +21,17 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }}
|
- run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }}
|
||||||
- run: cargo build --verbose
|
- run: cargo build --verbose --release
|
||||||
- run: cargo test --verbose
|
- run: cargo test --verbose --release
|
||||||
|
|
||||||
build_examples:
|
build_and_test_examples:
|
||||||
name: axum-oidc - examples
|
name: axum-oidc - examples
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
- run: sudo apt install chromium-browser -y
|
||||||
- run: rustup update stable && rustup default stable
|
- run: rustup update stable && rustup default stable
|
||||||
- run: cargo build --verbose
|
- run: cargo build --verbose --release
|
||||||
|
working-directory: ./examples/basic
|
||||||
|
- run: cargo test --verbose --release
|
||||||
working-directory: ./examples/basic
|
working-directory: ./examples/basic
|
||||||
|
|
|
@ -13,3 +13,11 @@ tower = "0.4"
|
||||||
tower-sessions = "0.12"
|
tower-sessions = "0.12"
|
||||||
|
|
||||||
dotenvy = "0.15"
|
dotenvy = "0.15"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
testcontainers = "0.15.0"
|
||||||
|
tokio = { version = "1.37.0", features = ["rt-multi-thread"] }
|
||||||
|
reqwest = { version = "0.12", features = ["rustls-tls"], default-features = false }
|
||||||
|
env_logger = "0.11.3"
|
||||||
|
log = "0.4.21"
|
||||||
|
headless_chrome = "1.0.9"
|
||||||
|
|
22
examples/basic/README.md
Normal file
22
examples/basic/README.md
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
This example is a basic web application to demonstrate the features of the `axum-oidc`-crate.
|
||||||
|
It has three endpoints:
|
||||||
|
- `/logout` - Logout of the current session using `OIDC RP-Initiated Logout`.
|
||||||
|
- `/foo` - A handler that only can be accessed when logged in.
|
||||||
|
- `/bar` - A handler that can be accessed logged out and logged in. It will greet the user with their name if they are logged in.
|
||||||
|
|
||||||
|
# Running the Example
|
||||||
|
## Dependencies
|
||||||
|
You will need a running OpenID Connect capable issuer like [Keycloak](https://www.keycloak.org/getting-started/getting-started-docker) and a valid client for the issuer.
|
||||||
|
|
||||||
|
You can take a look at the `tests/`-folder to see how the automated keycloak deployment for the integration tests work.
|
||||||
|
|
||||||
|
## Setup Environment
|
||||||
|
Create a `.env`-file that contains the following keys:
|
||||||
|
```
|
||||||
|
APP_URL=http://127.0.0.1:8080
|
||||||
|
ISSUER=<your-issuer>
|
||||||
|
CLIENT_ID=<your-client-id>
|
||||||
|
CLIENT_SECRET=<your-client-secret>
|
||||||
|
```
|
||||||
|
## Run the application
|
||||||
|
`RUST_LOG=debug cargo run`
|
82
examples/basic/src/lib.rs
Normal file
82
examples/basic/src/lib.rs
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
use axum::{
|
||||||
|
error_handling::HandleErrorLayer, http::Uri, response::IntoResponse, routing::get, Router,
|
||||||
|
};
|
||||||
|
use axum_oidc::{
|
||||||
|
error::MiddlewareError, EmptyAdditionalClaims, OidcAuthLayer, OidcClaims, OidcLoginLayer,
|
||||||
|
OidcRpInitiatedLogout,
|
||||||
|
};
|
||||||
|
use tokio::net::TcpListener;
|
||||||
|
use tower::ServiceBuilder;
|
||||||
|
use tower_sessions::{
|
||||||
|
cookie::{time::Duration, SameSite},
|
||||||
|
Expiry, MemoryStore, SessionManagerLayer,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn run(
|
||||||
|
app_url: String,
|
||||||
|
issuer: String,
|
||||||
|
client_id: String,
|
||||||
|
client_secret: Option<String>,
|
||||||
|
) {
|
||||||
|
let session_store = MemoryStore::default();
|
||||||
|
let session_layer = SessionManagerLayer::new(session_store)
|
||||||
|
.with_secure(false)
|
||||||
|
.with_same_site(SameSite::Lax)
|
||||||
|
.with_expiry(Expiry::OnInactivity(Duration::seconds(120)));
|
||||||
|
|
||||||
|
let oidc_login_service = ServiceBuilder::new()
|
||||||
|
.layer(HandleErrorLayer::new(|e: MiddlewareError| async {
|
||||||
|
e.into_response()
|
||||||
|
}))
|
||||||
|
.layer(OidcLoginLayer::<EmptyAdditionalClaims>::new());
|
||||||
|
|
||||||
|
let oidc_auth_service = ServiceBuilder::new()
|
||||||
|
.layer(HandleErrorLayer::new(|e: MiddlewareError| async {
|
||||||
|
e.into_response()
|
||||||
|
}))
|
||||||
|
.layer(
|
||||||
|
OidcAuthLayer::<EmptyAdditionalClaims>::discover_client(
|
||||||
|
Uri::from_maybe_shared(app_url).expect("valid APP_URL"),
|
||||||
|
issuer,
|
||||||
|
client_id,
|
||||||
|
client_secret,
|
||||||
|
vec![],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/foo", get(authenticated))
|
||||||
|
.route("/logout", get(logout))
|
||||||
|
.layer(oidc_login_service)
|
||||||
|
.route("/bar", get(maybe_authenticated))
|
||||||
|
.layer(oidc_auth_service)
|
||||||
|
.layer(session_layer);
|
||||||
|
|
||||||
|
let listener = TcpListener::bind("[::]:8080").await.unwrap();
|
||||||
|
axum::serve(listener, app.into_make_service())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn authenticated(claims: OidcClaims<EmptyAdditionalClaims>) -> impl IntoResponse {
|
||||||
|
format!("Hello {}", claims.subject().as_str())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn maybe_authenticated(
|
||||||
|
claims: Option<OidcClaims<EmptyAdditionalClaims>>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
if let Some(claims) = claims {
|
||||||
|
format!(
|
||||||
|
"Hello {}! You are already logged in from another Handler.",
|
||||||
|
claims.subject().as_str()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
"Hello anon!".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn logout(logout: OidcRpInitiatedLogout) -> impl IntoResponse {
|
||||||
|
logout.with_post_logout_redirect(Uri::from_static("https://pfzetto.de"))
|
||||||
|
}
|
|
@ -1,17 +1,4 @@
|
||||||
use axum::{
|
use basic::run;
|
||||||
error_handling::HandleErrorLayer, http::Uri, response::IntoResponse, routing::get, Router,
|
|
||||||
};
|
|
||||||
use axum_oidc::{
|
|
||||||
error::MiddlewareError, EmptyAdditionalClaims, OidcAuthLayer, OidcClaims, OidcLoginLayer,
|
|
||||||
OidcRpInitiatedLogout,
|
|
||||||
};
|
|
||||||
use tokio::net::TcpListener;
|
|
||||||
use tower::ServiceBuilder;
|
|
||||||
use tower_sessions::{
|
|
||||||
cookie::{time::Duration, SameSite},
|
|
||||||
Expiry, MemoryStore, SessionManagerLayer,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
dotenvy::dotenv().ok();
|
dotenvy::dotenv().ok();
|
||||||
|
@ -19,66 +6,5 @@ async fn main() {
|
||||||
let issuer = std::env::var("ISSUER").expect("ISSUER env variable");
|
let issuer = std::env::var("ISSUER").expect("ISSUER env variable");
|
||||||
let client_id = std::env::var("CLIENT_ID").expect("CLIENT_ID env variable");
|
let client_id = std::env::var("CLIENT_ID").expect("CLIENT_ID env variable");
|
||||||
let client_secret = std::env::var("CLIENT_SECRET").ok();
|
let client_secret = std::env::var("CLIENT_SECRET").ok();
|
||||||
|
run(app_url, issuer, client_id, client_secret).await
|
||||||
let session_store = MemoryStore::default();
|
|
||||||
let session_layer = SessionManagerLayer::new(session_store)
|
|
||||||
.with_secure(false)
|
|
||||||
.with_same_site(SameSite::Lax)
|
|
||||||
.with_expiry(Expiry::OnInactivity(Duration::seconds(120)));
|
|
||||||
|
|
||||||
let oidc_login_service = ServiceBuilder::new()
|
|
||||||
.layer(HandleErrorLayer::new(|e: MiddlewareError| async {
|
|
||||||
e.into_response()
|
|
||||||
}))
|
|
||||||
.layer(OidcLoginLayer::<EmptyAdditionalClaims>::new());
|
|
||||||
|
|
||||||
let oidc_auth_service = ServiceBuilder::new()
|
|
||||||
.layer(HandleErrorLayer::new(|e: MiddlewareError| async {
|
|
||||||
e.into_response()
|
|
||||||
}))
|
|
||||||
.layer(
|
|
||||||
OidcAuthLayer::<EmptyAdditionalClaims>::discover_client(
|
|
||||||
Uri::from_maybe_shared(app_url).expect("valid APP_URL"),
|
|
||||||
issuer,
|
|
||||||
client_id,
|
|
||||||
client_secret,
|
|
||||||
vec![],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let app = Router::new()
|
|
||||||
.route("/foo", get(authenticated))
|
|
||||||
.route("/logout", get(logout))
|
|
||||||
.layer(oidc_login_service)
|
|
||||||
.route("/bar", get(maybe_authenticated))
|
|
||||||
.layer(oidc_auth_service)
|
|
||||||
.layer(session_layer);
|
|
||||||
|
|
||||||
let listener = TcpListener::bind("[::]:8080").await.unwrap();
|
|
||||||
axum::serve(listener, app.into_make_service())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn authenticated(claims: OidcClaims<EmptyAdditionalClaims>) -> impl IntoResponse {
|
|
||||||
format!("Hello {}", claims.subject().as_str())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn maybe_authenticated(
|
|
||||||
claims: Option<OidcClaims<EmptyAdditionalClaims>>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
if let Some(claims) = claims {
|
|
||||||
format!(
|
|
||||||
"Hello {}! You are already logged in from another Handler.",
|
|
||||||
claims.subject().as_str()
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
"Hello anon!".to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn logout(logout: OidcRpInitiatedLogout) -> impl IntoResponse {
|
|
||||||
logout.with_post_logout_redirect(Uri::from_static("https://pfzetto.de"))
|
|
||||||
}
|
}
|
||||||
|
|
101
examples/basic/tests/integration.rs
Normal file
101
examples/basic/tests/integration.rs
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
mod keycloak;
|
||||||
|
|
||||||
|
use headless_chrome::Browser;
|
||||||
|
use log::info;
|
||||||
|
use testcontainers::*;
|
||||||
|
|
||||||
|
use crate::keycloak::{Client, Keycloak, Realm, User};
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn first() {
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
|
let docker = clients::Cli::default();
|
||||||
|
|
||||||
|
let alice = User {
|
||||||
|
username: "alice".to_string(),
|
||||||
|
email: "alice@example.com".to_string(),
|
||||||
|
firstname: "alice".to_string(),
|
||||||
|
lastname: "doe".to_string(),
|
||||||
|
password: "alice".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let basic_client = Client {
|
||||||
|
client_id: "axum-oidc-example-basic".to_string(),
|
||||||
|
client_secret: Some("123456".to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let keycloak = Keycloak::start(
|
||||||
|
vec![Realm {
|
||||||
|
name: "test".to_string(),
|
||||||
|
users: vec![alice.clone()],
|
||||||
|
clients: vec![basic_client.clone()],
|
||||||
|
}],
|
||||||
|
&docker,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
info!("starting basic example app");
|
||||||
|
|
||||||
|
let app_url = "http://127.0.0.1:8080/";
|
||||||
|
let app_handle = tokio::spawn(basic::run(
|
||||||
|
app_url.to_string(),
|
||||||
|
format!("{}/realms/test", keycloak.url()),
|
||||||
|
basic_client.client_id.to_string(),
|
||||||
|
basic_client.client_secret.clone(),
|
||||||
|
));
|
||||||
|
|
||||||
|
info!("starting browser");
|
||||||
|
|
||||||
|
let browser = Browser::default().unwrap();
|
||||||
|
let tab = browser.new_tab().unwrap();
|
||||||
|
|
||||||
|
tab.navigate_to(&format!("{}bar", app_url)).unwrap();
|
||||||
|
let body = tab
|
||||||
|
.wait_for_xpath(r#"/html/body/pre"#)
|
||||||
|
.unwrap()
|
||||||
|
.get_inner_text()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(body, "Hello anon!");
|
||||||
|
|
||||||
|
tab.navigate_to(&format!("{}foo", app_url)).unwrap();
|
||||||
|
let username = tab.wait_for_xpath(r#"//*[@id="username"]"#).unwrap();
|
||||||
|
username.type_into(&alice.username).unwrap();
|
||||||
|
let password = tab.wait_for_xpath(r#"//*[@id="password"]"#).unwrap();
|
||||||
|
password.type_into(&alice.password).unwrap();
|
||||||
|
let submit = tab.wait_for_xpath(r#"//*[@id="kc-login"]"#).unwrap();
|
||||||
|
submit.click().unwrap();
|
||||||
|
|
||||||
|
let body = tab
|
||||||
|
.wait_for_xpath(r#"/html/body/pre"#)
|
||||||
|
.unwrap()
|
||||||
|
.get_inner_text()
|
||||||
|
.unwrap();
|
||||||
|
assert!(body.starts_with("Hello ") && body.contains('-'));
|
||||||
|
|
||||||
|
tab.navigate_to(&format!("{}bar", app_url)).unwrap();
|
||||||
|
let body = tab
|
||||||
|
.wait_for_xpath(r#"/html/body/pre"#)
|
||||||
|
.unwrap()
|
||||||
|
.get_inner_text()
|
||||||
|
.unwrap();
|
||||||
|
assert!(body.contains("! You are already logged in from another Handler."));
|
||||||
|
|
||||||
|
tab.navigate_to(&format!("{}logout", app_url)).unwrap();
|
||||||
|
tab.wait_until_navigated().unwrap();
|
||||||
|
|
||||||
|
tab.navigate_to(&format!("{}bar", app_url)).unwrap();
|
||||||
|
let body = tab
|
||||||
|
.wait_for_xpath(r#"/html/body/pre"#)
|
||||||
|
.unwrap()
|
||||||
|
.get_inner_text()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(body, "Hello anon!");
|
||||||
|
|
||||||
|
tab.navigate_to(&format!("{}foo", app_url)).unwrap();
|
||||||
|
tab.wait_until_navigated().unwrap();
|
||||||
|
tab.find_element_by_xpath(r#"//*[@id="username"]"#).unwrap();
|
||||||
|
|
||||||
|
tab.close(true).unwrap();
|
||||||
|
app_handle.abort();
|
||||||
|
}
|
180
examples/basic/tests/keycloak.rs
Normal file
180
examples/basic/tests/keycloak.rs
Normal file
|
@ -0,0 +1,180 @@
|
||||||
|
use log::info;
|
||||||
|
use std::time::Duration;
|
||||||
|
use testcontainers::*;
|
||||||
|
|
||||||
|
use testcontainers::core::ExecCommand;
|
||||||
|
use testcontainers::{core::WaitFor, Container, Image, RunnableImage};
|
||||||
|
|
||||||
|
struct KeycloakImage;
|
||||||
|
|
||||||
|
impl Image for KeycloakImage {
|
||||||
|
type Args = Vec<String>;
|
||||||
|
|
||||||
|
fn name(&self) -> String {
|
||||||
|
"quay.io/keycloak/keycloak".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tag(&self) -> String {
|
||||||
|
"latest".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ready_conditions(&self) -> Vec<WaitFor> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Keycloak<'a> {
|
||||||
|
container: Container<'a, KeycloakImage>,
|
||||||
|
realms: Vec<Realm>,
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Realm {
|
||||||
|
pub name: String,
|
||||||
|
pub clients: Vec<Client>,
|
||||||
|
pub users: Vec<User>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Client {
|
||||||
|
pub client_id: String,
|
||||||
|
pub client_secret: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct User {
|
||||||
|
pub username: String,
|
||||||
|
pub email: String,
|
||||||
|
pub firstname: String,
|
||||||
|
pub lastname: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Keycloak<'a> {
|
||||||
|
pub async fn start(realms: Vec<Realm>, docker: &'a clients::Cli) -> Keycloak<'a> {
|
||||||
|
info!("starting keycloak");
|
||||||
|
|
||||||
|
let keycloak_image = RunnableImage::from((KeycloakImage, vec!["start-dev".to_string()]))
|
||||||
|
.with_env_var(("KEYCLOAK_ADMIN", "admin"))
|
||||||
|
.with_env_var(("KEYCLOAK_ADMIN_PASSWORD", "admin"));
|
||||||
|
let container = docker.run(keycloak_image);
|
||||||
|
|
||||||
|
let keycloak = Self {
|
||||||
|
url: format!("http://127.0.0.1:{}", container.get_host_port_ipv4(8080),),
|
||||||
|
container,
|
||||||
|
realms,
|
||||||
|
};
|
||||||
|
|
||||||
|
let issuer = format!(
|
||||||
|
"http://127.0.0.1:{}/realms/{}",
|
||||||
|
keycloak.container.get_host_port_ipv4(8080),
|
||||||
|
"test"
|
||||||
|
);
|
||||||
|
|
||||||
|
while reqwest::get(&issuer).await.is_err() {
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
keycloak.execute("/opt/keycloak/bin/kcadm.sh config credentials --server http://127.0.0.1:8080 --realm master --user admin --password admin".to_string()).await;
|
||||||
|
|
||||||
|
for realm in keycloak.realms.iter() {
|
||||||
|
keycloak.create_realm(&realm.name).await;
|
||||||
|
for client in realm.clients.iter() {
|
||||||
|
keycloak
|
||||||
|
.create_client(
|
||||||
|
&client.client_id,
|
||||||
|
client.client_secret.as_deref(),
|
||||||
|
&realm.name,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
for user in realm.users.iter() {
|
||||||
|
keycloak
|
||||||
|
.create_user(
|
||||||
|
&user.username,
|
||||||
|
&user.email,
|
||||||
|
&user.firstname,
|
||||||
|
&user.lastname,
|
||||||
|
&user.password,
|
||||||
|
&realm.name,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keycloak
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn url(&self) -> &str {
|
||||||
|
&self.url
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_realm(&self, name: &str) {
|
||||||
|
self.execute(format!(
|
||||||
|
"/opt/keycloak/bin/kcadm.sh create realms -s realm={} -s enabled=true",
|
||||||
|
name
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_client(&self, client_id: &str, client_secret: Option<&str>, realm: &str) {
|
||||||
|
if let Some(client_secret) = client_secret {
|
||||||
|
self.execute(format!(
|
||||||
|
r#"/opt/keycloak/bin/kcadm.sh create clients -r {} -f - << EOF
|
||||||
|
{{
|
||||||
|
"clientId": "{}",
|
||||||
|
"secret": "{}",
|
||||||
|
"redirectUris": ["*"]
|
||||||
|
}}
|
||||||
|
EOF
|
||||||
|
"#,
|
||||||
|
realm, client_id, client_secret
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
} else {
|
||||||
|
self.execute(format!(
|
||||||
|
r#"/opt/keycloak/bin/kcadm.sh create clients -r {} -f - << EOF
|
||||||
|
{{
|
||||||
|
"clientId": "{}",
|
||||||
|
"redirectUris": ["*"]
|
||||||
|
}}
|
||||||
|
EOF
|
||||||
|
"#,
|
||||||
|
realm, client_id
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_user(
|
||||||
|
&self,
|
||||||
|
username: &str,
|
||||||
|
email: &str,
|
||||||
|
firstname: &str,
|
||||||
|
lastname: &str,
|
||||||
|
password: &str,
|
||||||
|
realm: &str,
|
||||||
|
) {
|
||||||
|
let id = self.execute(
|
||||||
|
format!(
|
||||||
|
"/opt/keycloak/bin/kcadm.sh create users -r {} -s username={} -s enabled=true -s emailVerified=true -s email={} -s firstName={} -s lastName={}",
|
||||||
|
realm, username, email, firstname, lastname
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
self.execute(format!(
|
||||||
|
"/opt/keycloak/bin/kcadm.sh set-password -r {} --username {} --new-password {}",
|
||||||
|
realm, username, password
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute(&self, cmd: String) {
|
||||||
|
self.container.exec(ExecCommand {
|
||||||
|
cmd,
|
||||||
|
ready_conditions: vec![],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
92
src/lib.rs
92
src/lib.rs
|
@ -14,9 +14,9 @@ use openidconnect::{
|
||||||
CoreRevocationErrorResponse, CoreSubjectIdentifierType, CoreTokenIntrospectionResponse,
|
CoreRevocationErrorResponse, CoreSubjectIdentifierType, CoreTokenIntrospectionResponse,
|
||||||
CoreTokenType,
|
CoreTokenType,
|
||||||
},
|
},
|
||||||
reqwest::async_http_client,
|
AccessToken, ClientId, ClientSecret, CsrfToken, EmptyExtraTokenFields, HttpRequest,
|
||||||
AccessToken, ClientId, ClientSecret, CsrfToken, EmptyExtraTokenFields, IdTokenFields,
|
HttpResponse, IdTokenFields, IssuerUrl, Nonce, PkceCodeVerifier, RefreshToken,
|
||||||
IssuerUrl, Nonce, PkceCodeVerifier, RefreshToken, StandardErrorResponse, StandardTokenResponse,
|
StandardErrorResponse, StandardTokenResponse,
|
||||||
};
|
};
|
||||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||||
|
|
||||||
|
@ -72,7 +72,7 @@ type Client<AC> = openidconnect::Client<
|
||||||
CoreRevocationErrorResponse,
|
CoreRevocationErrorResponse,
|
||||||
>;
|
>;
|
||||||
|
|
||||||
type ProviderMetadata = openidconnect::ProviderMetadata<
|
pub type ProviderMetadata = openidconnect::ProviderMetadata<
|
||||||
AdditionalProviderMetadata,
|
AdditionalProviderMetadata,
|
||||||
CoreAuthDisplay,
|
CoreAuthDisplay,
|
||||||
CoreClientAuthMethod,
|
CoreClientAuthMethod,
|
||||||
|
@ -103,17 +103,14 @@ pub struct OidcClient<AC: AdditionalClaims> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<AC: AdditionalClaims> OidcClient<AC> {
|
impl<AC: AdditionalClaims> OidcClient<AC> {
|
||||||
/// create a new [`OidcClient`] by fetching the required information from the
|
/// create a new [`OidcClient`] from an existing [`ProviderMetadata`].
|
||||||
/// `/.well-known/openid-configuration` endpoint of the issuer.
|
pub fn from_provider_metadata(
|
||||||
pub async fn discover_new(
|
provider_metadata: ProviderMetadata,
|
||||||
application_base_url: Uri,
|
application_base_url: Uri,
|
||||||
issuer: String,
|
|
||||||
client_id: String,
|
client_id: String,
|
||||||
client_secret: Option<String>,
|
client_secret: Option<String>,
|
||||||
scopes: Vec<String>,
|
scopes: Vec<String>,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
let provider_metadata =
|
|
||||||
ProviderMetadata::discover_async(IssuerUrl::new(issuer)?, async_http_client).await?;
|
|
||||||
let end_session_endpoint = provider_metadata
|
let end_session_endpoint = provider_metadata
|
||||||
.additional_metadata()
|
.additional_metadata()
|
||||||
.end_session_endpoint
|
.end_session_endpoint
|
||||||
|
@ -134,6 +131,79 @@ impl<AC: AdditionalClaims> OidcClient<AC> {
|
||||||
end_session_endpoint,
|
end_session_endpoint,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// create a new [`OidcClient`] by fetching the required information from the
|
||||||
|
/// `/.well-known/openid-configuration` endpoint of the issuer.
|
||||||
|
pub async fn discover_new(
|
||||||
|
application_base_url: Uri,
|
||||||
|
issuer: String,
|
||||||
|
client_id: String,
|
||||||
|
client_secret: Option<String>,
|
||||||
|
scopes: Vec<String>,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
let client = reqwest::Client::default();
|
||||||
|
Self::discover_new_with_client(
|
||||||
|
application_base_url,
|
||||||
|
issuer,
|
||||||
|
client_id,
|
||||||
|
client_secret,
|
||||||
|
scopes,
|
||||||
|
&client,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// create a new [`OidcClient`] by fetching the required information from the
|
||||||
|
/// `/.well-known/openid-configuration` endpoint of the issuer using the provided
|
||||||
|
/// `reqwest::Client`.
|
||||||
|
pub async fn discover_new_with_client(
|
||||||
|
application_base_url: Uri,
|
||||||
|
issuer: String,
|
||||||
|
client_id: String,
|
||||||
|
client_secret: Option<String>,
|
||||||
|
scopes: Vec<String>,
|
||||||
|
client: &reqwest::Client,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
// modified version of `openidconnect::reqwest::async_client::async_http_client`.
|
||||||
|
let async_http_client = |request: HttpRequest| async move {
|
||||||
|
let mut request_builder = client
|
||||||
|
.request(request.method, request.url.as_str())
|
||||||
|
.body(request.body);
|
||||||
|
for (name, value) in &request.headers {
|
||||||
|
request_builder = request_builder.header(name.as_str(), value.as_bytes());
|
||||||
|
}
|
||||||
|
let request = request_builder
|
||||||
|
.build()
|
||||||
|
.map_err(openidconnect::reqwest::Error::Reqwest)?;
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.execute(request)
|
||||||
|
.await
|
||||||
|
.map_err(openidconnect::reqwest::Error::Reqwest)?;
|
||||||
|
|
||||||
|
let status_code = response.status();
|
||||||
|
let headers = response.headers().to_owned();
|
||||||
|
let chunks = response
|
||||||
|
.bytes()
|
||||||
|
.await
|
||||||
|
.map_err(openidconnect::reqwest::Error::Reqwest)?;
|
||||||
|
Ok(HttpResponse {
|
||||||
|
status_code,
|
||||||
|
headers,
|
||||||
|
body: chunks.to_vec(),
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let provider_metadata =
|
||||||
|
ProviderMetadata::discover_async(IssuerUrl::new(issuer)?, async_http_client).await?;
|
||||||
|
Self::from_provider_metadata(
|
||||||
|
provider_metadata,
|
||||||
|
application_base_url,
|
||||||
|
client_id,
|
||||||
|
client_secret,
|
||||||
|
scopes,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// an empty struct to be used as the default type for the additional claims generic
|
/// an empty struct to be used as the default type for the additional claims generic
|
||||||
|
@ -172,7 +242,7 @@ struct AuthenticatedSession<AC: AdditionalClaims> {
|
||||||
/// additional metadata that is discovered on client creation via the
|
/// additional metadata that is discovered on client creation via the
|
||||||
/// `.well-knwon/openid-configuration` endpoint.
|
/// `.well-knwon/openid-configuration` endpoint.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
struct AdditionalProviderMetadata {
|
pub struct AdditionalProviderMetadata {
|
||||||
end_session_endpoint: Option<String>,
|
end_session_endpoint: Option<String>,
|
||||||
}
|
}
|
||||||
impl openidconnect::AdditionalProviderMetadata for AdditionalProviderMetadata {}
|
impl openidconnect::AdditionalProviderMetadata for AdditionalProviderMetadata {}
|
||||||
|
|
Loading…
Reference in a new issue