Browse Source

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>
Stygmates 1 year ago
parent
commit
b836851d02

+ 5 - 0
.cargo/config.toml

@@ -0,0 +1,5 @@
+# All of these variables are used in the `openid_connect_demo` example, they are set here for the CI to work, they are set here because as stated here for now: `https://doc.rust-lang.org/cargo/reference/config.html` the .cargo/config.toml of the inner workspaces are not read when being invoked from the root workspace.
+[env]
+DIOXUS_FRONT_ISSUER_URL  = ""
+DIOXUS_FRONT_CLIENT_ID = ""
+DIOXUS_FRONT_URL = ""

+ 1 - 0
Cargo.toml

@@ -41,6 +41,7 @@ members = [
     "examples/tailwind",
     "examples/PWA-example",
     "examples/query_segments_demo",
+    "examples/openid_connect_demo",
     # Playwright tests
     "playwright-tests/liveview",
     "playwright-tests/web",

+ 3 - 0
examples/openid_connect_demo/.gitignore

@@ -0,0 +1,3 @@
+/target
+/dist
+.env

+ 24 - 0
examples/openid_connect_demo/Cargo.toml

@@ -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"] }

+ 47 - 0
examples/openid_connect_demo/Dioxus.toml

@@ -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 = []

+ 13 - 0
examples/openid_connect_demo/README.md

@@ -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`

+ 2 - 0
examples/openid_connect_demo/src/constants.rs

@@ -0,0 +1,2 @@
+pub const DIOXUS_FRONT_AUTH_TOKEN: &str = "auth_token";
+pub const DIOXUS_FRONT_AUTH_REQUEST: &str = "auth_request";

+ 20 - 0
examples/openid_connect_demo/src/errors.rs

@@ -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>,
+        >,
+    ),
+}

+ 60 - 0
examples/openid_connect_demo/src/main.rs

@@ -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);
+}

+ 1 - 0
examples/openid_connect_demo/src/model/mod.rs

@@ -0,0 +1 @@
+pub(crate) mod user;

+ 7 - 0
examples/openid_connect_demo/src/model/user.rs

@@ -0,0 +1,7 @@
+use uuid::Uuid;
+
+#[derive(PartialEq)]
+pub struct User {
+    pub id: Uuid,
+    pub name: String,
+}

+ 125 - 0
examples/openid_connect_demo/src/oidc.rs

@@ -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())
+}

+ 20 - 0
examples/openid_connect_demo/src/props/client.rs

@@ -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 }
+    }
+}

+ 1 - 0
examples/openid_connect_demo/src/props/mod.rs

@@ -0,0 +1 @@
+pub(crate) mod client;

+ 17 - 0
examples/openid_connect_demo/src/router.rs

@@ -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> },
+}

+ 38 - 0
examples/openid_connect_demo/src/storage.rs

@@ -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();
+    }
+}

+ 250 - 0
examples/openid_connect_demo/src/views/header.rs

@@ -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! {{}}
+        }
+    })
+}

+ 5 - 0
examples/openid_connect_demo/src/views/home.rs

@@ -0,0 +1,5 @@
+use dioxus::prelude::*;
+
+pub fn Home(cx: Scope) -> Element {
+    render! { div { "Hello world" } }
+}

+ 86 - 0
examples/openid_connect_demo/src/views/login.rs

@@ -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! {{}}
+        }
+    })
+}

+ 4 - 0
examples/openid_connect_demo/src/views/mod.rs

@@ -0,0 +1,4 @@
+pub(crate) mod header;
+pub(crate) mod home;
+pub(crate) mod login;
+pub(crate) mod not_found;

+ 7 - 0
examples/openid_connect_demo/src/views/not_found.rs

@@ -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}}}
+}