diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 000000000..464189e01 --- /dev/null +++ b/.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 = "" diff --git a/Cargo.toml b/Cargo.toml index e6b07eb8c..a78f60d3e 100644 --- a/Cargo.toml +++ b/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", diff --git a/examples/openid_connect_demo/.gitignore b/examples/openid_connect_demo/.gitignore new file mode 100644 index 000000000..919c8ee55 --- /dev/null +++ b/examples/openid_connect_demo/.gitignore @@ -0,0 +1,3 @@ +/target +/dist +.env diff --git a/examples/openid_connect_demo/Cargo.toml b/examples/openid_connect_demo/Cargo.toml new file mode 100644 index 000000000..4c5f47061 --- /dev/null +++ b/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"] } diff --git a/examples/openid_connect_demo/Dioxus.toml b/examples/openid_connect_demo/Dioxus.toml new file mode 100644 index 000000000..483dfe957 --- /dev/null +++ b/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 = [] diff --git a/examples/openid_connect_demo/README.md b/examples/openid_connect_demo/README.md new file mode 100644 index 000000000..e16467ddf --- /dev/null +++ b/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` \ No newline at end of file diff --git a/examples/openid_connect_demo/src/constants.rs b/examples/openid_connect_demo/src/constants.rs new file mode 100644 index 000000000..0a0b4950a --- /dev/null +++ b/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"; diff --git a/examples/openid_connect_demo/src/errors.rs b/examples/openid_connect_demo/src/errors.rs new file mode 100644 index 000000000..2b7e3f145 --- /dev/null +++ b/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>, + ), + #[error("Parsing error: {0}")] + Parse(#[from] url::ParseError), + #[error("Request token error: {0}")] + RequestToken( + #[from] + RequestTokenError< + openidconnect::reqwest::Error, + StandardErrorResponse, + >, + ), +} diff --git a/examples/openid_connect_demo/src/main.rs b/examples/openid_connect_demo/src/main.rs new file mode 100644 index 000000000..7ed8db8a9 --- /dev/null +++ b/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 = AtomRef(|_| ClientState::default()); + +// An option is required to prevent the component from being constantly refreshed +pub static FERMI_AUTH_TOKEN: fermi::AtomRef> = AtomRef(|_| None); +pub static FERMI_AUTH_REQUEST: fermi::AtomRef> = 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:: {} } +} + +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); +} diff --git a/examples/openid_connect_demo/src/model/mod.rs b/examples/openid_connect_demo/src/model/mod.rs new file mode 100644 index 000000000..68a79f5c0 --- /dev/null +++ b/examples/openid_connect_demo/src/model/mod.rs @@ -0,0 +1 @@ +pub(crate) mod user; diff --git a/examples/openid_connect_demo/src/model/user.rs b/examples/openid_connect_demo/src/model/user.rs new file mode 100644 index 000000000..b1005fdd2 --- /dev/null +++ b/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, +} diff --git a/examples/openid_connect_demo/src/oidc.rs b/examples/openid_connect_demo/src/oidc.rs new file mode 100644 index 000000000..7fa96c14b --- /dev/null +++ b/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, +} + +/// 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, +} + +#[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, + /// Token used to refresh the tokens if they expire + pub refresh_token: Option, +} + +pub fn email( + client: CoreClient, + id_token: CoreIdToken, + nonce: Nonce, +) -> Result { + 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::::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 { + 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 { + // 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, + StandardErrorResponse, + >, +> { + oidc_client + .exchange_refresh_token(&refresh_token) + .request_async(async_http_client) + .await +} + +pub async fn log_out_url(id_token_hint: CoreIdToken) -> Result { + 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()) +} diff --git a/examples/openid_connect_demo/src/props/client.rs b/examples/openid_connect_demo/src/props/client.rs new file mode 100644 index 000000000..2e02111e1 --- /dev/null +++ b/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 } + } +} diff --git a/examples/openid_connect_demo/src/props/mod.rs b/examples/openid_connect_demo/src/props/mod.rs new file mode 100644 index 000000000..1d3313153 --- /dev/null +++ b/examples/openid_connect_demo/src/props/mod.rs @@ -0,0 +1 @@ +pub(crate) mod client; diff --git a/examples/openid_connect_demo/src/router.rs b/examples/openid_connect_demo/src/router.rs new file mode 100644 index 000000000..0bb80afbb --- /dev/null +++ b/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 }, +} diff --git a/examples/openid_connect_demo/src/storage.rs b/examples/openid_connect_demo/src/storage.rs new file mode 100644 index 000000000..402740cdf --- /dev/null +++ b/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 { + pub key: String, + pub value: T, +} + +pub trait PersistentWrite { + fn persistent_set(atom_ref: &UseAtomRef>, entry: Option); +} + +impl PersistentWrite for AuthTokenState { + fn persistent_set( + atom_ref: &UseAtomRef>, + entry: Option, + ) { + *atom_ref.write() = entry.clone(); + LocalStorage::set(DIOXUS_FRONT_AUTH_TOKEN, entry).unwrap(); + } +} + +impl PersistentWrite for AuthRequestState { + fn persistent_set( + atom_ref: &UseAtomRef>, + entry: Option, + ) { + *atom_ref.write() = entry.clone(); + LocalStorage::set(DIOXUS_FRONT_AUTH_REQUEST, entry).unwrap(); + } +} diff --git a/examples/openid_connect_demo/src/views/header.rs b/examples/openid_connect_demo/src/views/header.rs new file mode 100644 index 000000000..1f2c6e057 --- /dev/null +++ b/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) -> 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::>>); + 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) -> 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 = 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:: {} + } + } + Err(error) => { + rsx! { + div { format!{"Failed to load client: {:?}", error} } + log::info!{"Failed to load client: {:?}", error}, + Outlet:: {} + } + } + }, + None => { + rsx! { + div { + div { "Loading client, please wait" } + Outlet:: {} + } + } + } + }) +} + +#[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 = 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:: {} + } + } + } + // 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:: {} + } + } + } + // Other issue with token decoding + _ => { + log::info!("Other issue with token"); + rsx! { + div { + div { error.to_string() } + Outlet:: {} + } + } + } + }, + } + } + // User is not logged in + None => { + rsx! { + div { + Link { to: auth_request.authorize_url.clone(), "Log in" } + Outlet:: {} + } + } + } + } + } + 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! {{}} + } + }) +} diff --git a/examples/openid_connect_demo/src/views/home.rs b/examples/openid_connect_demo/src/views/home.rs new file mode 100644 index 000000000..f39695302 --- /dev/null +++ b/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" } } +} diff --git a/examples/openid_connect_demo/src/views/login.rs b/examples/openid_connect_demo/src/views/login.rs new file mode 100644 index 000000000..2583503de --- /dev/null +++ b/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 = 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! {{}} + } + }) +} diff --git a/examples/openid_connect_demo/src/views/mod.rs b/examples/openid_connect_demo/src/views/mod.rs new file mode 100644 index 000000000..1b8e08a58 --- /dev/null +++ b/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; diff --git a/examples/openid_connect_demo/src/views/not_found.rs b/examples/openid_connect_demo/src/views/not_found.rs new file mode 100644 index 000000000..6321dc9fc --- /dev/null +++ b/examples/openid_connect_demo/src/views/not_found.rs @@ -0,0 +1,7 @@ +use dioxus::prelude::*; + +#[component] +pub fn NotFound(cx: Scope, route: Vec) -> Element { + let routes = route.join(""); + render! {rsx! {div{routes}}} +}