init commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/target
|
5832
Cargo.lock
generated
Normal file
5832
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
13
Cargo.toml
Normal file
13
Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "loco-keycloak-auth"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
authors = ["Gabriel Kaszewski"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
axum-keycloak-auth = { version = "0.8.2" }
|
||||||
|
axum = "0.8.3"
|
||||||
|
loco-rs = "0.15.0"
|
||||||
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
|
serde_json = "1.0.140"
|
||||||
|
tokio = "1.44.2"
|
149
README.md
Normal file
149
README.md
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
# 🔐 loco-keycloak-auth
|
||||||
|
|
||||||
|
A plug-and-play Keycloak authentication layer for [Loco.rs](https://github.com/loco-rs/loco), powered by [axum-keycloak-auth](https://crates.io/crates/axum-keycloak-auth).
|
||||||
|
This crate allows you to easily add secure Keycloak authentication to your Loco web app, with full control over protected routes and clean YAML-based config.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
- ✅ Simple integration with Loco initializers
|
||||||
|
- ✅ Based on `axum-keycloak-auth`
|
||||||
|
- ✅ Configurable via `config.yaml`
|
||||||
|
- ✅ Supports `Block` and `Pass` passthrough modes
|
||||||
|
- ✅ Designed to be flexible: apply middleware only where you want it
|
||||||
|
- ✅ Ideal for securing internal APIs or user-facing endpoints
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ Installation
|
||||||
|
|
||||||
|
Add to your `Cargo.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
loco-keycloak-auth = { git = "https://github.com/yourname/loco-keycloak-auth" }
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note**: If you’re using a local path for development:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
loco-keycloak-auth = { path = "../loco-keycloak-auth" }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠 Setup
|
||||||
|
|
||||||
|
### 1. Add Keycloak config to your `config/config.yaml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
settings:
|
||||||
|
keycloak_settings:
|
||||||
|
url: "https://keycloak.example.com"
|
||||||
|
realm: "myrealm"
|
||||||
|
expected_audiences:
|
||||||
|
- "account"
|
||||||
|
passthrough_mode: "Block" # or "Pass"
|
||||||
|
persist_raw_claims: false
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Add the initializer to your `App` in `app.rs` if you want to have all routes protected.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use loco_keycloak_auth::KeycloakAuthInitializer;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Hooks for App {
|
||||||
|
async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {
|
||||||
|
let keycloak_auth = loco_keycloak_auth::initializer::KeycloakAuthInitializer {};
|
||||||
|
Ok(vec![Box::new(keycloak_auth)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Usage
|
||||||
|
|
||||||
|
### Protect specific endpoints
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use loco_keycloak_auth::Keycloak;
|
||||||
|
|
||||||
|
fn routes(ctx: &AppContext) -> Routes {
|
||||||
|
let keycloak = Keycloak::from_context(ctx).expect("Failed to create Keycloak layer");
|
||||||
|
|
||||||
|
Routes::new()
|
||||||
|
.prefix("secure")
|
||||||
|
.add("/profile", get(profile_handler).layer(keycloak.layer))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 API
|
||||||
|
|
||||||
|
### Settings struct
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct KeycloakSettings {
|
||||||
|
pub url: String,
|
||||||
|
pub realm: String,
|
||||||
|
pub expected_audiences: Vec<String>,
|
||||||
|
pub passthrough_mode: PassthroughMode, // "Block" or "Pass"
|
||||||
|
pub persist_raw_claims: bool,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> `PassthroughMode` lets you decide whether unauthenticated requests should be blocked or passed along.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
We welcome contributions! Here's how to get started:
|
||||||
|
|
||||||
|
### 1. Clone and link locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/GKaszewski/loco-keycloak-auth
|
||||||
|
cd loco-keycloak-auth
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Use in your Loco project with a local path
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
loco-keycloak-auth = { path = "../loco-keycloak-auth" }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Run tests if there are any
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo test
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Submit a PR 🚀
|
||||||
|
|
||||||
|
Please open an issue or discussion first for larger feature proposals or breaking changes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🙌 Credits
|
||||||
|
|
||||||
|
- Built with ❤️ for the [Loco.rs](https://github.com/loco-rs/loco) ecosystem
|
||||||
|
- Powered by [axum-keycloak-auth](https://github.com/filiptibell/axum-keycloak-auth)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📫 Contact
|
||||||
|
|
||||||
|
Questions? Ideas? Want to contribute together?
|
||||||
|
Open an issue or reach out on [GitHub Discussions](https://github.com/GKaszewski/loco-keycloak-auth/discussions).
|
32
src/initializer.rs
Normal file
32
src/initializer.rs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
use axum::Router;
|
||||||
|
use loco_rs::prelude::*;
|
||||||
|
|
||||||
|
use crate::Keycloak;
|
||||||
|
|
||||||
|
/// KeycloakAuthInitializer is an initializer for the Keycloak authentication layer.
|
||||||
|
/// Use this if you want to add Keycloak authentication to all routes in your application.
|
||||||
|
/// It will automatically read the Keycloak settings from the application context.
|
||||||
|
/// This initializer is typically used in the `app.rs` file of your Loco application.
|
||||||
|
/// If you want to have more control over the Keycloak layer, you can use the `Keycloak` struct
|
||||||
|
/// and add the layer to the routes directly.
|
||||||
|
/// ## Example
|
||||||
|
/// ```rust
|
||||||
|
/// async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {
|
||||||
|
/// let keycloak_auth = loco_keycloak_auth::initializer::KeycloakAuthInitializer {};
|
||||||
|
/// Ok(vec![Box::new(keycloak_auth)])
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub struct KeycloakAuthInitializer;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Initializer for KeycloakAuthInitializer {
|
||||||
|
fn name(&self) -> String {
|
||||||
|
"keycloak_auth".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn after_routes(&self, router: Router, ctx: &AppContext) -> Result<Router> {
|
||||||
|
let keycloak = Keycloak::from_context(ctx)
|
||||||
|
.map_err(|err| Error::Message(format!("Failed to create Keycloak layer: {}", err)))?;
|
||||||
|
Ok(router.layer(keycloak.layer))
|
||||||
|
}
|
||||||
|
}
|
60
src/lib.rs
Normal file
60
src/lib.rs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
pub mod initializer;
|
||||||
|
pub mod settings;
|
||||||
|
|
||||||
|
pub use axum_keycloak_auth::decode::KeycloakToken;
|
||||||
|
use axum_keycloak_auth::{
|
||||||
|
Url,
|
||||||
|
instance::{KeycloakAuthInstance, KeycloakConfig},
|
||||||
|
layer::KeycloakAuthLayer,
|
||||||
|
};
|
||||||
|
use loco_rs::prelude::*;
|
||||||
|
use settings::{KeycloakSettings, Settings};
|
||||||
|
|
||||||
|
/// Keycloak is a struct that holds the Keycloak authentication layer.
|
||||||
|
/// ## Usage
|
||||||
|
/// ```rust
|
||||||
|
/// let keycloak = Keycloak::from_context(ctx).expect("Failed to create Keycloak layer");
|
||||||
|
/// let router = Router::new()
|
||||||
|
/// .route("/protected", get(protected_handler))
|
||||||
|
/// .layer(keycloak.layer);
|
||||||
|
/// ```
|
||||||
|
pub struct Keycloak {
|
||||||
|
pub layer: KeycloakAuthLayer<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Keycloak {
|
||||||
|
pub fn from_context(ctx: &AppContext) -> Result<Self> {
|
||||||
|
build_keycloak_layer(ctx).map(|layer| Keycloak { layer })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function builds the Keycloak authentication layer using the settings
|
||||||
|
/// from the application context.
|
||||||
|
pub fn build_keycloak_layer(ctx: &AppContext) -> Result<KeycloakAuthLayer<String>> {
|
||||||
|
let full_settings: Settings = serde_json::from_value(
|
||||||
|
ctx.config
|
||||||
|
.settings
|
||||||
|
.clone()
|
||||||
|
.ok_or_else(|| Error::Message("Missing `settings` in config".into()))?,
|
||||||
|
)
|
||||||
|
.map_err(|err| Error::Message(format!("Invalid settings: {}", err)))?;
|
||||||
|
|
||||||
|
let settings: KeycloakSettings = full_settings.keycloak_settings;
|
||||||
|
|
||||||
|
let instance =
|
||||||
|
KeycloakAuthInstance::new(
|
||||||
|
KeycloakConfig::builder()
|
||||||
|
.server(Url::parse(&settings.url).map_err(|err| {
|
||||||
|
Error::Message(format!("Invalid Keycloak server URL: {}", err))
|
||||||
|
})?)
|
||||||
|
.realm(settings.realm)
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(KeycloakAuthLayer::<String>::builder()
|
||||||
|
.instance(instance)
|
||||||
|
.passthrough_mode(settings.passthrough_mode.0)
|
||||||
|
.persist_raw_claims(settings.persist_raw_claims)
|
||||||
|
.expected_audiences(settings.expected_audiences)
|
||||||
|
.build())
|
||||||
|
}
|
68
src/settings.rs
Normal file
68
src/settings.rs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
use axum_keycloak_auth::PassthroughMode;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
/// Configuration settings for Keycloak authentication.
|
||||||
|
///
|
||||||
|
/// This struct should be placed under the `settings.keycloak_settings` section
|
||||||
|
/// of your Loco application's `config/config.yaml`. It provides all values
|
||||||
|
/// needed to initialize the Keycloak authentication layer.
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct KeycloakSettings {
|
||||||
|
/// The full URL to your Keycloak server (e.g. `https://sso.example.com`).
|
||||||
|
pub url: String,
|
||||||
|
/// The realm name in Keycloak (e.g. `myrealm`).
|
||||||
|
pub realm: String,
|
||||||
|
/// A list of expected audiences in the token (typically contains `"account"`).
|
||||||
|
pub expected_audiences: Vec<String>,
|
||||||
|
/// The mode that determines how the authentication layer behaves.
|
||||||
|
///
|
||||||
|
/// - `PassthroughMode::Block`: Return `401 Unauthorized` on authentication failure.
|
||||||
|
/// - `PassthroughMode::Pass`: Allow unauthenticated access and set auth status as an extension.
|
||||||
|
///
|
||||||
|
/// Default: `Block`
|
||||||
|
pub passthrough_mode: PassthroughModeDef,
|
||||||
|
/// Whether to persist raw Keycloak claims as an Axum extension.
|
||||||
|
///
|
||||||
|
/// Set this to `true` if you want access to the raw token contents.
|
||||||
|
pub persist_raw_claims: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Root struct to hold all custom application settings.
|
||||||
|
///
|
||||||
|
/// This is typically deserialized from the `settings:` section in Loco's
|
||||||
|
/// `config/config.yaml`.
|
||||||
|
/// ## Sample configuration
|
||||||
|
/// ```yaml
|
||||||
|
/// settings:
|
||||||
|
/// keycloak_settings:
|
||||||
|
/// url: "https://sso.example.com"
|
||||||
|
/// realm: "myrealm"
|
||||||
|
/// expected_audiences:
|
||||||
|
/// - "account"
|
||||||
|
/// passthrough_mode: "Block" # or "Pass"
|
||||||
|
/// persist_raw_claims: false
|
||||||
|
/// ```
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct Settings {
|
||||||
|
pub keycloak_settings: KeycloakSettings,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct PassthroughModeDef(pub PassthroughMode);
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for PassthroughModeDef {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s = String::deserialize(deserializer)?;
|
||||||
|
match s.to_lowercase().as_str() {
|
||||||
|
"block" => Ok(PassthroughModeDef(PassthroughMode::Block)),
|
||||||
|
"pass" => Ok(PassthroughModeDef(PassthroughMode::Pass)),
|
||||||
|
_ => Err(serde::de::Error::custom(format!(
|
||||||
|
"Invalid passthrough mode: {}",
|
||||||
|
s
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user