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:
StygmatesandTruong Tan Dat authored and GitHub committed 2023-10-20 12:43:24 -05:00
1 parent 1cf8f71368
commit b836851d02
21 files changed
+736

No files matched your search

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