Add openidconnect authentication demo (#1500)
* Add openidconnect authentication demo * use_atom_ref usage to have a read/write handle on the atom * Use default * Code rewrite to better reflect the authentication flow * Use the env macro instead of the build.rs to load env variables * Add env variables * Remove unnecessary dependency * Add env variables to the root workspace * Update readme * Bump openidconnect version * Use props to pass the client to the child components * Code clean up --------- Co-authored-by: Truong Tan Dat <truongt@igbmc.fr>
This commit is contained in:
1 parent
1cf8f71368
commit
b836851d02
21 files changed
+736
No files matched your search
@@ -0,0 +1,3 @@
|
||||
/target
|
||||
/dist
|
||||
.env
|
||||
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "openid_auth_demo"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
console_error_panic_hook = "0.1"
|
||||
dioxus-logger = "0.4.1"
|
||||
dioxus = { path = "../../packages/dioxus", version = "*" }
|
||||
dioxus-router = { path = "../../packages/router", version = "*" }
|
||||
dioxus-web = { path = "../../packages/web", version = "*" }
|
||||
fermi = { path = "../../packages/fermi", version = "*" }
|
||||
form_urlencoded = "1.2.0"
|
||||
gloo-storage = "0.3.0"
|
||||
log = "0.4"
|
||||
openidconnect = "3.4.0"
|
||||
reqwest = "0.11.20"
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
serde_json = "1.0.105"
|
||||
thiserror = "1.0.48"
|
||||
uuid = "1.4"
|
||||
web-sys = { version = "0.3", features = ["Request", "Document"] }
|
||||
@@ -0,0 +1,47 @@
|
||||
[application]
|
||||
|
||||
# dioxus project name
|
||||
name = "OpenID Connect authentication demo"
|
||||
|
||||
# default platfrom
|
||||
# you can also use `dioxus serve/build --platform XXX` to use other platform
|
||||
# value: web | desktop
|
||||
default_platform = "web"
|
||||
|
||||
# Web `build` & `serve` dist path
|
||||
out_dir = "dist"
|
||||
|
||||
# resource (static) file folder
|
||||
asset_dir = "public"
|
||||
|
||||
[web.app]
|
||||
|
||||
# HTML title tag content
|
||||
title = "OpenID Connect authentication demo"
|
||||
|
||||
[web.watcher]
|
||||
|
||||
index_on_404 = true
|
||||
|
||||
watch_path = ["src"]
|
||||
|
||||
# include `assets` in web platform
|
||||
[web.resource]
|
||||
|
||||
# CSS style file
|
||||
style = []
|
||||
|
||||
# Javascript code file
|
||||
script = []
|
||||
|
||||
[web.resource.dev]
|
||||
|
||||
# Javascript code file
|
||||
# serve: [dev-server] only
|
||||
script = []
|
||||
|
||||
[application.plugins]
|
||||
|
||||
available = true
|
||||
|
||||
required = []
|
||||
@@ -0,0 +1,13 @@
|
||||
# OpenID Connect example to show how to authenticate an user
|
||||
|
||||
The environment variables in `.cargo/config.toml` must be set in order for this example to work(if this example is just being compiled from the root workspace, the `.cargo/config.toml` from the root workspace must be set as stated in the [Cargo book](https://doc.rust-lang.org/cargo/reference/config.html)).
|
||||
|
||||
Once they are set, you can run `dx serve`
|
||||
|
||||
### Environment variables summary
|
||||
|
||||
```DIOXUS_FRONT_ISSUER_URL``` The openid-connect's issuer url
|
||||
|
||||
```DIOXUS_FRONT_CLIENT_ID``` The openid-connect's client id
|
||||
|
||||
```DIOXUS_FRONT_URL``` The url the frontend is supposed to be running on, it could be for example `http://localhost:8080`
|
||||
@@ -0,0 +1,2 @@
|
||||
pub const DIOXUS_FRONT_AUTH_TOKEN: &str = "auth_token";
|
||||
pub const DIOXUS_FRONT_AUTH_REQUEST: &str = "auth_request";
|
||||
@@ -0,0 +1,20 @@
|
||||
use openidconnect::{core::CoreErrorResponseType, url, RequestTokenError, StandardErrorResponse};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Discovery error: {0}")]
|
||||
OpenIdConnect(
|
||||
#[from] openidconnect::DiscoveryError<openidconnect::reqwest::Error<reqwest::Error>>,
|
||||
),
|
||||
#[error("Parsing error: {0}")]
|
||||
Parse(#[from] url::ParseError),
|
||||
#[error("Request token error: {0}")]
|
||||
RequestToken(
|
||||
#[from]
|
||||
RequestTokenError<
|
||||
openidconnect::reqwest::Error<reqwest::Error>,
|
||||
StandardErrorResponse<CoreErrorResponseType>,
|
||||
>,
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
#![allow(non_snake_case)]
|
||||
use dioxus::prelude::*;
|
||||
use fermi::*;
|
||||
use gloo_storage::{LocalStorage, Storage};
|
||||
use log::LevelFilter;
|
||||
pub(crate) mod constants;
|
||||
pub(crate) mod errors;
|
||||
pub(crate) mod model;
|
||||
pub(crate) mod oidc;
|
||||
pub(crate) mod props;
|
||||
pub(crate) mod router;
|
||||
pub(crate) mod storage;
|
||||
pub(crate) mod views;
|
||||
use oidc::{AuthRequestState, AuthTokenState};
|
||||
use router::Route;
|
||||
|
||||
use dioxus_router::prelude::*;
|
||||
|
||||
use crate::{
|
||||
constants::{DIOXUS_FRONT_AUTH_REQUEST, DIOXUS_FRONT_AUTH_TOKEN},
|
||||
oidc::ClientState,
|
||||
};
|
||||
pub static FERMI_CLIENT: fermi::AtomRef<ClientState> = AtomRef(|_| ClientState::default());
|
||||
|
||||
// An option is required to prevent the component from being constantly refreshed
|
||||
pub static FERMI_AUTH_TOKEN: fermi::AtomRef<Option<AuthTokenState>> = AtomRef(|_| None);
|
||||
pub static FERMI_AUTH_REQUEST: fermi::AtomRef<Option<AuthRequestState>> = AtomRef(|_| None);
|
||||
|
||||
pub static DIOXUS_FRONT_ISSUER_URL: &str = env!("DIOXUS_FRONT_ISSUER_URL");
|
||||
pub static DIOXUS_FRONT_CLIENT_ID: &str = env!("DIOXUS_FRONT_CLIENT_ID");
|
||||
pub static DIOXUS_FRONT_URL: &str = env!("DIOXUS_FRONT_URL");
|
||||
|
||||
fn App(cx: Scope) -> Element {
|
||||
use_init_atom_root(cx);
|
||||
|
||||
// Retrieve the value stored in the browser's storage
|
||||
let stored_auth_token = LocalStorage::get(DIOXUS_FRONT_AUTH_TOKEN)
|
||||
.ok()
|
||||
.unwrap_or(AuthTokenState::default());
|
||||
let fermi_auth_token = use_atom_ref(cx, &FERMI_AUTH_TOKEN);
|
||||
if fermi_auth_token.read().is_none() {
|
||||
*fermi_auth_token.write() = Some(stored_auth_token);
|
||||
}
|
||||
|
||||
let stored_auth_request = LocalStorage::get(DIOXUS_FRONT_AUTH_REQUEST)
|
||||
.ok()
|
||||
.unwrap_or(AuthRequestState::default());
|
||||
let fermi_auth_request = use_atom_ref(cx, &FERMI_AUTH_REQUEST);
|
||||
if fermi_auth_request.read().is_none() {
|
||||
*fermi_auth_request.write() = Some(stored_auth_request);
|
||||
}
|
||||
render! { Router::<Route> {} }
|
||||
}
|
||||
|
||||
fn main() {
|
||||
dioxus_logger::init(LevelFilter::Info).expect("failed to init logger");
|
||||
console_error_panic_hook::set_once();
|
||||
log::info!("starting app");
|
||||
dioxus_web::launch(App);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub(crate) mod user;
|
||||
@@ -0,0 +1,7 @@
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub struct User {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
use openidconnect::{
|
||||
core::{CoreClient, CoreErrorResponseType, CoreIdToken, CoreResponseType, CoreTokenResponse},
|
||||
reqwest::async_http_client,
|
||||
url::Url,
|
||||
AuthenticationFlow, AuthorizationCode, ClaimsVerificationError, ClientId, CsrfToken, IssuerUrl,
|
||||
LogoutRequest, Nonce, ProviderMetadataWithLogout, RedirectUrl, RefreshToken, RequestTokenError,
|
||||
StandardErrorResponse,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{props::client::ClientProps, DIOXUS_FRONT_CLIENT_ID};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ClientState {
|
||||
pub oidc_client: Option<ClientProps>,
|
||||
}
|
||||
|
||||
/// State that holds the nonce and authorization url and the nonce generated to log in an user
|
||||
#[derive(Clone, Deserialize, Serialize, Default)]
|
||||
pub struct AuthRequestState {
|
||||
pub auth_request: Option<AuthRequest>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
pub struct AuthRequest {
|
||||
pub nonce: Nonce,
|
||||
pub authorize_url: String,
|
||||
}
|
||||
|
||||
/// State the tokens returned once the user is authenticated
|
||||
#[derive(Debug, Deserialize, Serialize, Default, Clone)]
|
||||
pub struct AuthTokenState {
|
||||
/// Token used to identify the user
|
||||
pub id_token: Option<CoreIdToken>,
|
||||
/// Token used to refresh the tokens if they expire
|
||||
pub refresh_token: Option<RefreshToken>,
|
||||
}
|
||||
|
||||
pub fn email(
|
||||
client: CoreClient,
|
||||
id_token: CoreIdToken,
|
||||
nonce: Nonce,
|
||||
) -> Result<String, ClaimsVerificationError> {
|
||||
match id_token.claims(&client.id_token_verifier(), &nonce) {
|
||||
Ok(claims) => Ok(claims.clone().email().unwrap().to_string()),
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn authorize_url(client: CoreClient) -> AuthRequest {
|
||||
let (authorize_url, _csrf_state, nonce) = client
|
||||
.authorize_url(
|
||||
AuthenticationFlow::<CoreResponseType>::AuthorizationCode,
|
||||
CsrfToken::new_random,
|
||||
Nonce::new_random,
|
||||
)
|
||||
.add_scope(openidconnect::Scope::new("email".to_string()))
|
||||
.add_scope(openidconnect::Scope::new("profile".to_string()))
|
||||
.url();
|
||||
AuthRequest {
|
||||
authorize_url: authorize_url.to_string(),
|
||||
nonce,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn init_provider_metadata() -> Result<ProviderMetadataWithLogout, crate::errors::Error> {
|
||||
let issuer_url = IssuerUrl::new(crate::DIOXUS_FRONT_ISSUER_URL.to_string())?;
|
||||
Ok(ProviderMetadataWithLogout::discover_async(issuer_url, async_http_client).await?)
|
||||
}
|
||||
|
||||
pub async fn init_oidc_client() -> Result<(ClientId, CoreClient), crate::errors::Error> {
|
||||
let client_id = ClientId::new(crate::DIOXUS_FRONT_CLIENT_ID.to_string());
|
||||
let provider_metadata = init_provider_metadata().await?;
|
||||
let client_secret = None;
|
||||
let redirect_url = RedirectUrl::new(format!("{}/login", crate::DIOXUS_FRONT_URL))?;
|
||||
|
||||
Ok((
|
||||
client_id.clone(),
|
||||
CoreClient::from_provider_metadata(provider_metadata, client_id, client_secret)
|
||||
.set_redirect_uri(redirect_url),
|
||||
))
|
||||
}
|
||||
|
||||
///TODO: Add pkce_pacifier
|
||||
pub async fn token_response(
|
||||
oidc_client: CoreClient,
|
||||
code: String,
|
||||
) -> Result<CoreTokenResponse, crate::errors::Error> {
|
||||
// let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
|
||||
Ok(oidc_client
|
||||
.exchange_code(AuthorizationCode::new(code.clone()))
|
||||
// .set_pkce_verifier(pkce_verifier)
|
||||
.request_async(async_http_client)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn exchange_refresh_token(
|
||||
oidc_client: CoreClient,
|
||||
refresh_token: RefreshToken,
|
||||
) -> Result<
|
||||
CoreTokenResponse,
|
||||
RequestTokenError<
|
||||
openidconnect::reqwest::Error<reqwest::Error>,
|
||||
StandardErrorResponse<CoreErrorResponseType>,
|
||||
>,
|
||||
> {
|
||||
oidc_client
|
||||
.exchange_refresh_token(&refresh_token)
|
||||
.request_async(async_http_client)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn log_out_url(id_token_hint: CoreIdToken) -> Result<Url, crate::errors::Error> {
|
||||
let provider_metadata = init_provider_metadata().await?;
|
||||
let end_session_url = provider_metadata
|
||||
.additional_metadata()
|
||||
.clone()
|
||||
.end_session_endpoint
|
||||
.unwrap();
|
||||
let logout_request: LogoutRequest = LogoutRequest::from(end_session_url);
|
||||
Ok(logout_request
|
||||
.set_client_id(ClientId::new(DIOXUS_FRONT_CLIENT_ID.to_string()))
|
||||
.set_id_token_hint(&id_token_hint)
|
||||
.http_get_url())
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
use dioxus::prelude::*;
|
||||
use openidconnect::{core::CoreClient, ClientId};
|
||||
|
||||
#[derive(Props, Clone, Debug)]
|
||||
pub struct ClientProps {
|
||||
pub client: CoreClient,
|
||||
pub client_id: ClientId,
|
||||
}
|
||||
|
||||
impl PartialEq for ClientProps {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.client_id == other.client_id
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientProps {
|
||||
pub fn new(client_id: ClientId, client: CoreClient) -> Self {
|
||||
ClientProps { client_id, client }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub(crate) mod client;
|
||||
@@ -0,0 +1,17 @@
|
||||
use crate::views::{header::AuthHeader, home::Home, login::Login, not_found::NotFound};
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_router::prelude::*;
|
||||
|
||||
#[derive(Routable, Clone)]
|
||||
pub enum Route {
|
||||
#[layout(AuthHeader)]
|
||||
#[route("/")]
|
||||
Home {},
|
||||
|
||||
// https://dioxuslabs.com/learn/0.4/router/reference/routes#query-segments
|
||||
#[route("/login?:query_string")]
|
||||
Login { query_string: String },
|
||||
#[end_layout]
|
||||
#[route("/:..route")]
|
||||
NotFound { route: Vec<String> },
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
use fermi::UseAtomRef;
|
||||
use gloo_storage::{LocalStorage, Storage};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
constants::{DIOXUS_FRONT_AUTH_REQUEST, DIOXUS_FRONT_AUTH_TOKEN},
|
||||
oidc::{AuthRequestState, AuthTokenState},
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct StorageEntry<T> {
|
||||
pub key: String,
|
||||
pub value: T,
|
||||
}
|
||||
|
||||
pub trait PersistentWrite<T: Serialize + Clone> {
|
||||
fn persistent_set(atom_ref: &UseAtomRef<Option<T>>, entry: Option<T>);
|
||||
}
|
||||
|
||||
impl PersistentWrite<AuthTokenState> for AuthTokenState {
|
||||
fn persistent_set(
|
||||
atom_ref: &UseAtomRef<Option<AuthTokenState>>,
|
||||
entry: Option<AuthTokenState>,
|
||||
) {
|
||||
*atom_ref.write() = entry.clone();
|
||||
LocalStorage::set(DIOXUS_FRONT_AUTH_TOKEN, entry).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl PersistentWrite<AuthRequestState> for AuthRequestState {
|
||||
fn persistent_set(
|
||||
atom_ref: &UseAtomRef<Option<AuthRequestState>>,
|
||||
entry: Option<AuthRequestState>,
|
||||
) {
|
||||
*atom_ref.write() = entry.clone();
|
||||
LocalStorage::set(DIOXUS_FRONT_AUTH_REQUEST, entry).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
use crate::{
|
||||
oidc::{
|
||||
authorize_url, email, exchange_refresh_token, init_oidc_client, log_out_url,
|
||||
AuthRequestState, AuthTokenState, ClientState,
|
||||
},
|
||||
props::client::ClientProps,
|
||||
router::Route,
|
||||
storage::PersistentWrite,
|
||||
FERMI_AUTH_REQUEST, FERMI_AUTH_TOKEN, FERMI_CLIENT,
|
||||
};
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_router::prelude::{Link, Outlet};
|
||||
use fermi::*;
|
||||
use openidconnect::{url::Url, OAuth2TokenResponse, TokenResponse};
|
||||
|
||||
#[component]
|
||||
pub fn LogOut(cx: Scope<ClientProps>) -> Element {
|
||||
let fermi_auth_token = use_atom_ref(cx, &FERMI_AUTH_TOKEN);
|
||||
let fermi_auth_token_read = fermi_auth_token.read().clone();
|
||||
let log_out_url_state = use_state(cx, || None::<Option<Result<Url, crate::errors::Error>>>);
|
||||
cx.render(match fermi_auth_token_read {
|
||||
Some(fermi_auth_token_read) => match fermi_auth_token_read.id_token.clone() {
|
||||
Some(id_token) => match log_out_url_state.get() {
|
||||
Some(log_out_url_result) => match log_out_url_result {
|
||||
Some(uri) => match uri {
|
||||
Ok(uri) => {
|
||||
rsx! {
|
||||
Link {
|
||||
onclick: move |_| {
|
||||
{
|
||||
AuthTokenState::persistent_set(
|
||||
fermi_auth_token,
|
||||
Some(AuthTokenState::default()),
|
||||
);
|
||||
}
|
||||
},
|
||||
to: uri.to_string(),
|
||||
"Log out"
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
rsx! {
|
||||
div { format!{"Failed to load disconnection url: {:?}", error} }
|
||||
}
|
||||
}
|
||||
},
|
||||
None => {
|
||||
rsx! { div { "Loading... Please wait" } }
|
||||
}
|
||||
},
|
||||
None => {
|
||||
let logout_url_task = move || {
|
||||
cx.spawn({
|
||||
let log_out_url_state = log_out_url_state.to_owned();
|
||||
async move {
|
||||
let logout_url = log_out_url(id_token).await;
|
||||
let logout_url_option = Some(logout_url);
|
||||
log_out_url_state.set(Some(logout_url_option));
|
||||
}
|
||||
})
|
||||
};
|
||||
logout_url_task();
|
||||
rsx! { div{"Loading log out url... Please wait"}}
|
||||
}
|
||||
},
|
||||
None => {
|
||||
rsx! {{}}
|
||||
}
|
||||
},
|
||||
None => {
|
||||
rsx! {{}}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn RefreshToken(cx: Scope<ClientProps>) -> Element {
|
||||
let fermi_auth_token = use_atom_ref(cx, &FERMI_AUTH_TOKEN);
|
||||
let fermi_auth_request = use_atom_ref(cx, &FERMI_AUTH_REQUEST);
|
||||
let fermi_auth_token_read = fermi_auth_token.read().clone();
|
||||
cx.render(match fermi_auth_token_read {
|
||||
Some(fermi_auth_client_read) => match fermi_auth_client_read.refresh_token {
|
||||
Some(refresh_token) => {
|
||||
let fermi_auth_token = fermi_auth_token.to_owned();
|
||||
let fermi_auth_request = fermi_auth_request.to_owned();
|
||||
let client = cx.props.client.clone();
|
||||
let exchange_refresh_token_spawn = move || {
|
||||
cx.spawn({
|
||||
async move {
|
||||
let exchange_refresh_token =
|
||||
exchange_refresh_token(client, refresh_token).await;
|
||||
match exchange_refresh_token {
|
||||
Ok(response_token) => {
|
||||
AuthTokenState::persistent_set(
|
||||
&fermi_auth_token,
|
||||
Some(AuthTokenState {
|
||||
id_token: response_token.id_token().cloned(),
|
||||
refresh_token: response_token.refresh_token().cloned(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
Err(_error) => {
|
||||
AuthTokenState::persistent_set(
|
||||
&fermi_auth_token,
|
||||
Some(AuthTokenState::default()),
|
||||
);
|
||||
AuthRequestState::persistent_set(
|
||||
&fermi_auth_request,
|
||||
Some(AuthRequestState::default()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
exchange_refresh_token_spawn();
|
||||
rsx! { div { "Refreshing session, please wait" } }
|
||||
}
|
||||
None => {
|
||||
rsx! { div { "Id token expired and no refresh token found" } }
|
||||
}
|
||||
},
|
||||
None => {
|
||||
rsx! {{}}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn LoadClient(cx: Scope) -> Element {
|
||||
let init_client_future = use_future(cx, (), |_| async move { init_oidc_client().await });
|
||||
let fermi_client: &UseAtomRef<ClientState> = use_atom_ref(cx, &FERMI_CLIENT);
|
||||
cx.render(match init_client_future.value() {
|
||||
Some(client_props) => match client_props {
|
||||
Ok((client_id, client)) => {
|
||||
*fermi_client.write() = ClientState {
|
||||
oidc_client: Some(ClientProps::new(client_id.clone(), client.clone())),
|
||||
};
|
||||
rsx! {
|
||||
div { "Client successfully loaded" }
|
||||
Outlet::<Route> {}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
rsx! {
|
||||
div { format!{"Failed to load client: {:?}", error} }
|
||||
log::info!{"Failed to load client: {:?}", error},
|
||||
Outlet::<Route> {}
|
||||
}
|
||||
}
|
||||
},
|
||||
None => {
|
||||
rsx! {
|
||||
div {
|
||||
div { "Loading client, please wait" }
|
||||
Outlet::<Route> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn AuthHeader(cx: Scope) -> Element {
|
||||
let auth_token = use_atom_ref(cx, &FERMI_AUTH_TOKEN);
|
||||
let fermi_auth_request = use_atom_ref(cx, &FERMI_AUTH_REQUEST);
|
||||
let fermi_client: &UseAtomRef<ClientState> = use_atom_ref(cx, &FERMI_CLIENT);
|
||||
let client = fermi_client.read().oidc_client.clone();
|
||||
let auth_request_read = fermi_auth_request.read().clone();
|
||||
let auth_token_read = auth_token.read().clone();
|
||||
cx.render(match (client, auth_request_read, auth_token_read) {
|
||||
// We have everything we need to attempt to authenticate the user
|
||||
(Some(client_props), Some(auth_request), Some(auth_token)) => {
|
||||
match auth_request.auth_request {
|
||||
Some(auth_request) => {
|
||||
match auth_token.id_token {
|
||||
Some(id_token) => {
|
||||
match email(
|
||||
client_props.client.clone(),
|
||||
id_token.clone(),
|
||||
auth_request.nonce.clone(),
|
||||
) {
|
||||
Ok(email) => {
|
||||
rsx! {
|
||||
div {
|
||||
div { email }
|
||||
LogOut { client_id: client_props.client_id, client: client_props.client }
|
||||
Outlet::<Route> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Id token failed to be decoded
|
||||
Err(error) => match error {
|
||||
// Id token failed to be decoded because it expired, we refresh it
|
||||
openidconnect::ClaimsVerificationError::Expired(_message) => {
|
||||
log::info!("Token expired");
|
||||
rsx! {
|
||||
div {
|
||||
RefreshToken {client_id: client_props.client_id, client: client_props.client}
|
||||
Outlet::<Route> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Other issue with token decoding
|
||||
_ => {
|
||||
log::info!("Other issue with token");
|
||||
rsx! {
|
||||
div {
|
||||
div { error.to_string() }
|
||||
Outlet::<Route> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
// User is not logged in
|
||||
None => {
|
||||
rsx! {
|
||||
div {
|
||||
Link { to: auth_request.authorize_url.clone(), "Log in" }
|
||||
Outlet::<Route> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let auth_request = authorize_url(client_props.client);
|
||||
AuthRequestState::persistent_set(
|
||||
fermi_auth_request,
|
||||
Some(AuthRequestState {
|
||||
auth_request: Some(auth_request),
|
||||
}),
|
||||
);
|
||||
rsx! { div { "Loading nonce" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
// Client is not initialized yet, we need it for everything
|
||||
(None, _, _) => {
|
||||
rsx! { LoadClient {} }
|
||||
}
|
||||
// We need everything loaded before doing anything
|
||||
(_client, _auth_request, _auth_token) => {
|
||||
rsx! {{}}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
pub fn Home(cx: Scope) -> Element {
|
||||
render! { div { "Hello world" } }
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
use crate::{
|
||||
oidc::{token_response, AuthRequestState, AuthTokenState},
|
||||
router::Route,
|
||||
storage::PersistentWrite,
|
||||
DIOXUS_FRONT_URL, FERMI_AUTH_REQUEST, FERMI_AUTH_TOKEN, FERMI_CLIENT,
|
||||
};
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_router::prelude::{Link, NavigationTarget};
|
||||
use fermi::*;
|
||||
use openidconnect::{OAuth2TokenResponse, TokenResponse};
|
||||
|
||||
#[component]
|
||||
pub fn Login(cx: Scope, query_string: String) -> Element {
|
||||
let fermi_client = use_atom_ref(cx, &FERMI_CLIENT);
|
||||
let fermi_auth_token = use_atom_ref(cx, &FERMI_AUTH_TOKEN);
|
||||
let home_url: NavigationTarget<Route> = DIOXUS_FRONT_URL.parse().unwrap();
|
||||
let fermi_auth_request = use_atom_ref(cx, &FERMI_AUTH_REQUEST);
|
||||
let client = fermi_client.read().oidc_client.clone();
|
||||
let auth_token_read = fermi_auth_token.read().clone();
|
||||
cx.render(match (client, auth_token_read) {
|
||||
(Some(client_props), Some(auth_token_read)) => {
|
||||
match (auth_token_read.id_token, auth_token_read.refresh_token) {
|
||||
(Some(_id_token), Some(_refresh_token)) => {
|
||||
rsx! {
|
||||
div { "Sign in successful" }
|
||||
Link { to: home_url, "Go back home" }
|
||||
}
|
||||
}
|
||||
// If the refresh token is set but not the id_token, there was an error, we just go back home and reset their value
|
||||
(None, Some(_)) | (Some(_), None) => {
|
||||
rsx! {
|
||||
div { "Error while attempting to log in" }
|
||||
Link {
|
||||
to: home_url,
|
||||
onclick: move |_| {
|
||||
AuthTokenState::persistent_set(fermi_auth_token, Some(AuthTokenState::default()));
|
||||
AuthRequestState::persistent_set(
|
||||
fermi_auth_request,
|
||||
Some(AuthRequestState::default()),
|
||||
);
|
||||
},
|
||||
"Go back home"
|
||||
}
|
||||
}
|
||||
}
|
||||
(None, None) => {
|
||||
let mut query_pairs = form_urlencoded::parse(query_string.as_bytes());
|
||||
let code_pair = query_pairs.find(|(key, _value)| key == "code");
|
||||
match code_pair {
|
||||
Some((_key, code)) => {
|
||||
let auth_code = code.to_string();
|
||||
let token_response_spawn = move ||{
|
||||
cx.spawn({
|
||||
let fermi_auth_token = fermi_auth_token.to_owned();
|
||||
async move {
|
||||
let token_response_result = token_response(client_props.client, auth_code).await;
|
||||
match token_response_result{
|
||||
Ok(token_response) => {
|
||||
let id_token = token_response.id_token().unwrap();
|
||||
AuthTokenState::persistent_set(&fermi_auth_token, Some(AuthTokenState {
|
||||
id_token: Some(id_token.clone()),
|
||||
refresh_token: token_response.refresh_token().cloned()
|
||||
}));
|
||||
}
|
||||
Err(error) => {
|
||||
log::warn!{"{error}"};
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
token_response_spawn();
|
||||
rsx!{ div {} }
|
||||
}
|
||||
None => {
|
||||
rsx! { div { "No code provided" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(_, _) => {
|
||||
rsx! {{}}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub(crate) mod header;
|
||||
pub(crate) mod home;
|
||||
pub(crate) mod login;
|
||||
pub(crate) mod not_found;
|
||||
@@ -0,0 +1,7 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn NotFound(cx: Scope, route: Vec<String>) -> Element {
|
||||
let routes = route.join("");
|
||||
render! {rsx! {div{routes}}}
|
||||
}
|
||||
Reference in new issue
Block a user