Skip to content

Commit 4e6e2ff

Browse files
author
system
committed
Add OIDC Example
1 parent 20c885a commit 4e6e2ff

8 files changed

Lines changed: 248 additions & 1 deletion

File tree

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@ members = [
1818
"examples/request-tracking",
1919
"examples/engine",
2020
"rwf-admin",
21-
"examples/files", "examples/users", "examples/openapi",
21+
"examples/files", "examples/users", "examples/openapi", "examples/oidc",
2222
]
2323
exclude = ["examples/rails", "rwf-ruby", "examples/django", "rwf-fuzz"]

examples/oidc/Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[package]
2+
name = "oidc"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
[dependencies]
7+
rwf = {path = "../../rwf"}
8+
tokio = {version = "1.49.0", features = ["full"]}
9+
serde = {version = "1.0.228", features = ["derive"]}
10+
serde_json = "1.0.149"

examples/oidc/README.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# OIDC
2+
3+
Rwf comes with an OIDC Integration. One only need to implement a User struct with the `OidcUser` trait implemented.
4+
5+
```rust
6+
#[derive(macros::Model, Clone, Serialize, Deserialize)]
7+
struct User {
8+
id: Option<i64>,
9+
name: String,
10+
email: String,
11+
token: serde_json::Value,
12+
expire: OffsetDateTime,
13+
}
14+
15+
#[async_trait]
16+
impl OidcUser for User {
17+
async fn from_token(
18+
token: StandardTokenResponse<CoreIdTokenFields, CoreTokenType>,
19+
userinfo: UserInfoClaims<EmptyAdditionalClaims, CoreGenderClaim>,
20+
) -> Result<Self, rwf::model::Error> {
21+
let name = userinfo
22+
.standard_claims()
23+
.preferred_username()
24+
.unwrap()
25+
.to_string();
26+
let email = userinfo.standard_claims().email().unwrap().to_string();
27+
let expire = OffsetDateTime::now_utc()
28+
.checked_add(Duration::nanoseconds(
29+
token.expires_in().unwrap().as_nanos() as i64,
30+
))
31+
.unwrap();
32+
let token = serde_json::json!({"access": token.access_token(), "refresh": token.refresh_token().unwrap()});
33+
let mut conn = get_connection().await?;
34+
User::create(&[
35+
("name", name.to_value()),
36+
("email", email.to_value()),
37+
("expire", expire.to_value()),
38+
("token", token.to_value()),
39+
])
40+
.unique_by(&["email"])
41+
.fetch(&mut conn)
42+
.await
43+
}
44+
45+
fn access_token(&self) -> AccessToken {
46+
serde_json::from_value(self.token.get("access").unwrap().clone()).unwrap()
47+
}
48+
49+
fn refresh_token(&self) -> RefreshToken {
50+
serde_json::from_value(self.token.get("refresh").unwrap().clone()).unwrap()
51+
}
52+
53+
fn expire(&self) -> &OffsetDateTime {
54+
&self.expire
55+
}
56+
57+
fn update_token(
58+
mut self,
59+
token: StandardTokenResponse<CoreIdTokenFields, CoreTokenType>,
60+
) -> Self {
61+
self.token = serde_json::json!({"access": token.access_token(), "refresh": token.refresh_token().unwrap()});
62+
self
63+
}
64+
}
65+
```
66+
67+
## Login
68+
69+
To protect a Route with the OIDC Login, one have to set the `OidcAuthentication` handler as the `AuthHandler` for Controller.
70+
```rust
71+
struct TestController {
72+
auth: AuthHandler,
73+
}
74+
75+
impl Default for TestController {
76+
fn default() -> Self {
77+
Self {
78+
auth: OidcAuthentication::<User>::default().handler(),
79+
}
80+
}
81+
}
82+
83+
#[async_trait]
84+
impl Controller for TestController {
85+
fn auth(&self) -> &AuthHandler {
86+
&self.auth
87+
}
88+
89+
async fn handle(&self, request: &Request) -> Result<Response, Error> {
90+
Ok(Respnse::not_implemented())
91+
}
92+
}
93+
```
94+
95+
## OIDC Response Handler
96+
97+
The AuthHandler is only responsible for issue a Request to the OIDC Provider. A Endpoint to handle the OIDC Response is required also.
98+
99+
```rust
100+
#[tokio::main]
101+
async fn main() -> Result<(), rwf::http::Error> {
102+
migrate().await?;
103+
rwf::http::Server::new(vec![
104+
route!("/" => TestController),
105+
route!("/oidc" => OidcController::<User>),
106+
])
107+
.launch()
108+
.await
109+
}
110+
```
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DROP TABLE users;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CREATE TABLE users (
2+
id bigserial primary key,
3+
name varchar(255) not null,
4+
email varchar(255) not null unique,
5+
expire timestamptz not null,
6+
token json not null
7+
);

examples/oidc/rwf.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[general]
2+
secret_key = "WcPlOPnQzEJ/yPI4VCFhpofKnTE1uNbvl2tQsyAyz1g="
3+
4+
[database]
5+
name = "rwf_oidc"
6+
7+
[oidc]
8+
client_id="rwf_oidc_client"
9+
client_secret="rwf_oidc_secret"
10+
redirect_url="http://127.0.0.1:8000/oidc"
11+
discovery_url="https://oidc.rwf.tld/"

examples/oidc/src/main.rs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
use rwf::controller::oidc::*;
2+
use rwf::model::{get_connection, migrate};
3+
use rwf::prelude::*;
4+
#[derive(macros::Model, Clone, Serialize, Deserialize)]
5+
struct User {
6+
id: Option<i64>,
7+
name: String,
8+
email: String,
9+
token: serde_json::Value,
10+
expire: OffsetDateTime,
11+
}
12+
#[async_trait]
13+
impl OidcUser for User {
14+
async fn from_token(
15+
token: StandardTokenResponse<CoreIdTokenFields, CoreTokenType>,
16+
userinfo: UserInfoClaims<EmptyAdditionalClaims, CoreGenderClaim>,
17+
) -> Result<Self, rwf::model::Error> {
18+
let name = userinfo
19+
.standard_claims()
20+
.preferred_username()
21+
.unwrap()
22+
.to_string();
23+
let email = userinfo.standard_claims().email().unwrap().to_string();
24+
let expire = OffsetDateTime::now_utc()
25+
.checked_add(Duration::nanoseconds(
26+
token.expires_in().unwrap().as_nanos() as i64,
27+
))
28+
.unwrap();
29+
let token = serde_json::json!({"access": token.access_token(), "refresh": token.refresh_token().unwrap()});
30+
let mut conn = get_connection().await?;
31+
User::create(&[
32+
("name", name.to_value()),
33+
("email", email.to_value()),
34+
("expire", expire.to_value()),
35+
("token", token.to_value()),
36+
])
37+
.unique_by(&["email"])
38+
.fetch(&mut conn)
39+
.await
40+
}
41+
42+
fn access_token(&self) -> AccessToken {
43+
serde_json::from_value(self.token.get("access").unwrap().clone()).unwrap()
44+
}
45+
46+
fn refresh_token(&self) -> RefreshToken {
47+
serde_json::from_value(self.token.get("refresh").unwrap().clone()).unwrap()
48+
}
49+
50+
fn expire(&self) -> &OffsetDateTime {
51+
&self.expire
52+
}
53+
54+
fn update_token(
55+
mut self,
56+
token: StandardTokenResponse<CoreIdTokenFields, CoreTokenType>,
57+
) -> Self {
58+
self.token = serde_json::json!({"access": token.access_token(), "refresh": token.refresh_token().unwrap()});
59+
self
60+
}
61+
}
62+
63+
struct TestController {
64+
auth: AuthHandler,
65+
}
66+
67+
impl Default for TestController {
68+
fn default() -> Self {
69+
Self {
70+
auth: OidcAuthentication::<User>::default().handler(),
71+
}
72+
}
73+
}
74+
75+
#[async_trait]
76+
impl Controller for TestController {
77+
fn auth(&self) -> &AuthHandler {
78+
&self.auth
79+
}
80+
81+
async fn handle(&self, request: &Request) -> Result<Response, Error> {
82+
let mut conn = get_connection().await?;
83+
let user: User = request.user_required(&mut conn).await?;
84+
Ok(Response::new().html(format!("<h1>Hello {}</h1>", user.name)))
85+
}
86+
}
87+
88+
#[tokio::main]
89+
async fn main() -> Result<(), rwf::http::Error> {
90+
migrate().await?;
91+
rwf::http::Server::new(vec![
92+
route!("/" => TestController),
93+
// Take OIDC Config from rwf.toml (or Env).
94+
route!("/oidc" => OidcController::<User>),
95+
])
96+
.launch()
97+
.await
98+
}

0 commit comments

Comments
 (0)