From ddccbea32ae5eb419f391ce203a72af99946d0ae Mon Sep 17 00:00:00 2001 From: Jens Reimann Date: Fri, 26 Jan 2024 08:53:59 +0100 Subject: [PATCH 01/24] chore: fix typo --- src/agent/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/agent/mod.rs b/src/agent/mod.rs index 01c04bb..8490878 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -31,7 +31,7 @@ use yew::Callback; /// /// ## Non-exhaustive struct /// -/// The struct is "non exhaustive", which means that it is possible to add fields without breaking the API. +/// The struct is "non-exhaustive", which means that it is possible to add fields without breaking the API. /// /// In order to create an instance, follow the following pattern: /// @@ -87,7 +87,7 @@ impl LoginOptions { self } - /// Use `yew-nested-route` history api for post-login redirect callback + /// Use `yew-nested-router` history api for post-login redirect callback #[cfg(feature = "yew-nested-router")] pub fn with_nested_router_redirect(mut self) -> Self { let callback = Callback::from(|url: String| { From 31d584dc8b89c7a90d270d8f357e80693093c08f Mon Sep 17 00:00:00 2001 From: Jens Reimann Date: Fri, 26 Jan 2024 08:57:44 +0100 Subject: [PATCH 02/24] chore: clear warning --- src/prelude.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/prelude.rs b/src/prelude.rs index 7d3d705..e53ad5c 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -2,7 +2,6 @@ pub use crate::agent::{AgentConfiguration, LoginOptions, OAuth2Error, OAuth2Operations}; pub use crate::components::*; -pub use crate::config::*; pub use crate::context::*; pub use crate::hook::*; From 215e9c2d3ddc044c751238860d6719314c28995e Mon Sep 17 00:00:00 2001 From: Jens Reimann Date: Fri, 26 Jan 2024 08:58:01 +0100 Subject: [PATCH 03/24] refactor: make the values of the storage keys internal --- src/agent/state.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/agent/state.rs b/src/agent/state.rs index e6ef6c3..c265b6c 100644 --- a/src/agent/state.rs +++ b/src/agent/state.rs @@ -1,7 +1,7 @@ -pub const STORAGE_KEY_CSRF_TOKEN: &str = "ctron/oauth2/csrfToken"; -pub const STORAGE_KEY_LOGIN_STATE: &str = "ctron/oauth2/loginState"; -pub const STORAGE_KEY_REDIRECT_URL: &str = "ctron/oauth2/redirectUrl"; -pub const STORAGE_KEY_POST_LOGIN_URL: &str = "ctron/oauth2/postLoginUrl"; +pub(crate) const STORAGE_KEY_CSRF_TOKEN: &str = "ctron/oauth2/csrfToken"; +pub(crate) const STORAGE_KEY_LOGIN_STATE: &str = "ctron/oauth2/loginState"; +pub(crate) const STORAGE_KEY_REDIRECT_URL: &str = "ctron/oauth2/redirectUrl"; +pub(crate) const STORAGE_KEY_POST_LOGIN_URL: &str = "ctron/oauth2/postLoginUrl"; #[derive(Debug)] pub struct State { From 55b57c27f0052ceb60d15f86900965b84adfebd3 Mon Sep 17 00:00:00 2001 From: Jens Reimann Date: Fri, 26 Jan 2024 08:58:25 +0100 Subject: [PATCH 04/24] docs: add some docs --- src/agent/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/agent/mod.rs b/src/agent/mod.rs index 8490878..0b52445 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -62,11 +62,13 @@ impl LoginOptions { LoginOptions::default() } + /// Set the query parameters for the login request pub fn with_query(mut self, query: impl IntoIterator) -> Self { self.query = HashMap::from_iter(query); self } + /// Extend the current query parameters for the login request pub fn with_extended_query( mut self, query: impl IntoIterator, From ae155baa18dcf391319e89c29bcb22a5fa61ad23 Mon Sep 17 00:00:00 2001 From: Jens Reimann Date: Fri, 26 Jan 2024 09:00:02 +0100 Subject: [PATCH 05/24] build: go back to a simple version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 9ffffa3..6facc17 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ web-sys = { version = "0.3", features = [ ] } openidconnect = { version = "3.0", optional = true } -yew-nested-router = { version = ">=0.6.3, <0.7", optional = true } +yew-nested-router = { version = "0.6.3", optional = true } [features] # Enable for Yew nested router support From a2993c42bebf7b6199e2f23643107212ce377800 Mon Sep 17 00:00:00 2001 From: Jens Reimann Date: Fri, 26 Jan 2024 09:13:11 +0100 Subject: [PATCH 06/24] fix: drop unused type arguments, clean up --- src/config.rs | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/config.rs b/src/config.rs index 917aa09..2804457 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,19 +7,13 @@ pub mod openid { use super::*; /// OpenID Connect client configuration + #[non_exhaustive] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Config { /// The client ID pub client_id: String, /// The OpenID connect issuer URL. pub issuer_url: String, - #[serde(default)] - /// Additional, non-required configuration, with a default. - pub additional: Additional, - } - - #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] - pub struct Additional { /// An override for the end session URL. pub end_session_url: Option, /// The URL to navigate to after the logout has been completed. @@ -34,6 +28,8 @@ pub mod openid { /// Those audiences are allowed in addition to the client ID. pub additional_trusted_audiences: Vec, } + + impl Config {} } /// Configuration for OAuth2 @@ -41,6 +37,12 @@ pub mod oauth2 { use super::*; /// Plain OAuth2 client configuration + /// + /// ## Non-exhaustive + /// + /// This struct is `#[non_exhaustive]`, so it is not possible to directly create a struct. You can do this using + /// the [`Config::new`] function. + #[non_exhaustive] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Config { /// The client ID @@ -52,14 +54,11 @@ pub mod oauth2 { } impl Config { - pub fn new(client_id: C, auth_url: A, token_url: T) -> Self - where - C: Into, - A: Into, - T: Into, - S: IntoIterator, - I: Into, - { + pub fn new( + client_id: impl Into, + auth_url: impl Into, + token_url: impl Into, + ) -> Self { Self { client_id: client_id.into(), auth_url: auth_url.into(), From 962a94fe86da2ebc914fdb4ef00f995eb5fba13a Mon Sep 17 00:00:00 2001 From: Jens Reimann Date: Fri, 26 Jan 2024 09:46:34 +0100 Subject: [PATCH 07/24] refactor: cleanups before the next release Aside from a bunch of cleanups, this also makes the main configurations non-exhaustive, in order to be prepared for future extensions. BREAKING-CHANGE: In addition to the other configuration/option struct, this also makes the main configuration non-exhaustive. So it is no longer possible to directly create a new struct, but use the "new" function instead and modify the struct using the `with_*` functions. --- README.md | 26 +++++++-------- src/agent/config.rs | 1 + src/agent/mod.rs | 5 +-- src/config.rs | 81 +++++++++++++++++++++++++++++++++++++++++++-- src/hook.rs | 2 ++ src/lib.rs | 27 +++++++-------- src/prelude.rs | 2 +- 7 files changed, 109 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index fad80bb..11e6f95 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,8 @@ Add to your `Cargo.toml`: yew-oauth2 = "0.9" ``` -By default, the `router` integration is disabled, you can enable it using: +By default, the `router` integration for [`yew-nested-router`](https://github.com/ctron/yew-nested-router) is disabled, +you can enable it using: ```toml yew-oauth2 = { version = "0.9", features = ["router"] } @@ -31,14 +32,14 @@ use yew_oauth2::oauth2::*; // use `openid::*` when using OpenID connect #[function_component(MyApplication)] fn my_app() -> Html { - let config = Config { - client_id: "my-client".into(), - auth_url: "https://my-sso/auth/realms/my-realm/protocol/openid-connect/auth".into(), - token_url: "https://my-sso/auth/realms/my-realm/protocol/openid-connect/token".into(), - }; + let config = Config::new( + "my-client", + "https://my-sso/auth/realms/my-realm/protocol/openid-connect/auth", + "https://my-sso/auth/realms/my-realm/protocol/openid-connect/token" + ); html!( - + ) @@ -48,13 +49,10 @@ fn my_app() -> Html { fn my_app_main() -> Html { let agent = use_auth_agent().expect("Must be nested inside an OAuth2 component"); - let login = { - let agent = agent.clone(); - Callback::from(move |_| { - let _ = agent.start_login(); - }) - }; - let logout = Callback::from(move |_| { + let login = use_callback(agent.clone(), |_, agent| { + let _ = agent.start_login(); + }); + let logout = use_callback(agent, |_, agent| { let _ = agent.logout(); }); diff --git a/src/agent/config.rs b/src/agent/config.rs index 6605655..bc7409f 100644 --- a/src/agent/config.rs +++ b/src/agent/config.rs @@ -2,6 +2,7 @@ use super::LoginOptions; use crate::agent::Client; use std::time::Duration; +#[doc(hidden)] #[derive(Clone, Debug)] pub struct AgentConfiguration { pub config: C::Configuration, diff --git a/src/agent/mod.rs b/src/agent/mod.rs index 0b52445..78087d7 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -4,13 +4,14 @@ pub mod client; mod config; mod error; mod ops; -pub mod state; +mod state; pub use client::*; -pub use config::*; pub use error::*; pub use ops::*; +pub(crate) use config::*; + use crate::context::{Authentication, OAuth2Context, Reason}; use gloo_storage::{errors::StorageError, SessionStorage, Storage}; use gloo_timers::callback::Timeout; diff --git a/src/config.rs b/src/config.rs index 2804457..e998e85 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,6 +7,11 @@ pub mod openid { use super::*; /// OpenID Connect client configuration + /// + /// ## Non-exhaustive + /// + /// This struct is `#[non_exhaustive]`, so it is not possible to directly create a struct, creating a new struct + /// is done using the [`Config::new`] function. Additional properties are set using the `with_*` functions. #[non_exhaustive] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Config { @@ -29,7 +34,76 @@ pub mod openid { pub additional_trusted_audiences: Vec, } - impl Config {} + impl Config { + /// Create a new configuration + pub fn new(client_id: impl Into, issuer_url: impl Into) -> Self { + Self { + client_id: client_id.into(), + issuer_url: issuer_url.into(), + + end_session_url: None, + after_logout_url: None, + post_logout_redirect_name: None, + additional_trusted_audiences: vec![], + } + } + + /// Set an override for the URL for ending the session. + pub fn with_end_session_url(mut self, end_session_url: impl Into>) -> Self { + self.end_session_url = end_session_url.into(); + self + } + + /// Set the URL the issuer should redirect to after the logout + pub fn with_after_logout_url( + mut self, + after_logout_url: impl Into>, + ) -> Self { + self.after_logout_url = after_logout_url.into(); + self + } + + /// Set the name of the post logout redirect query parameter + pub fn with_post_logout_redirect_name( + mut self, + post_logout_redirect_name: impl Into>, + ) -> Self { + self.post_logout_redirect_name = post_logout_redirect_name.into(); + self + } + + /// Set the additionally trusted audiences + pub fn with_additional_trusted_audiences( + mut self, + additional_trusted_audiences: impl IntoIterator>, + ) -> Self { + self.additional_trusted_audiences = additional_trusted_audiences + .into_iter() + .map(|s| s.into()) + .collect(); + self + } + + /// Extend the additionally trusted audiences. + pub fn extend_additional_trusted_audiences( + mut self, + additional_trusted_audiences: impl IntoIterator>, + ) -> Self { + self.additional_trusted_audiences + .extend(additional_trusted_audiences.into_iter().map(|s| s.into())); + self + } + + /// Add an additionally trusted audience. + pub fn add_additional_trusted_audience( + mut self, + additional_trusted_audience: impl Into, + ) -> Self { + self.additional_trusted_audiences + .push(additional_trusted_audience.into()); + self + } + } } /// Configuration for OAuth2 @@ -40,8 +114,8 @@ pub mod oauth2 { /// /// ## Non-exhaustive /// - /// This struct is `#[non_exhaustive]`, so it is not possible to directly create a struct. You can do this using - /// the [`Config::new`] function. + /// This struct is `#[non_exhaustive]`, so it is not possible to directly create a struct, creating a new struct + /// is done using the [`crate::openid::Config::new`] function. #[non_exhaustive] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Config { @@ -54,6 +128,7 @@ pub mod oauth2 { } impl Config { + /// Create a new configuration pub fn new( client_id: impl Into, auth_url: impl Into, diff --git a/src/hook.rs b/src/hook.rs index a1ea7c1..2c52bc4 100644 --- a/src/hook.rs +++ b/src/hook.rs @@ -1,3 +1,5 @@ +//! Hooks for Yew + use crate::{context::LatestAccessToken, prelude::OAuth2Context}; use yew::prelude::*; diff --git a/src/lib.rs b/src/lib.rs index 7f12160..0a05685 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,13 +6,13 @@ //! This crate supports both plain OAuth2 and Open ID Connect (OIDC). OIDC layers a few features //! on top of OAuth2 (like logout URLs, discovery, …). //! -//! In order to use OIDC, you will need to enable the feature `openidconnect`. +//! In order to use OIDC, you will need to enable the feature `openid`. //! //! ## Example //! //! **NOTE:** Also see the readme for more examples. //! -//! Can be used like: +//! The following is a basic example: //! //! ```rust //! use yew::prelude::*; @@ -21,14 +21,14 @@ //! //! #[function_component(MyApplication)] //! fn my_app() -> Html { -//! let config = Config { -//! client_id: "my-client".into(), -//! auth_url: "https://my-sso/auth/realms/my-realm/protocol/openid-connect/auth".into(), -//! token_url: "https://my-sso/auth/realms/my-realm/protocol/openid-connect/token".into(), -//! }; +//! let config = Config::new( +//! "my-client", +//! "https://my-sso/auth/realms/my-realm/protocol/openid-connect/auth", +//! "https://my-sso/auth/realms/my-realm/protocol/openid-connect/token" +//! ); //! //! html!( -//! +//! //! //! //! ) @@ -38,13 +38,10 @@ //! fn my_app_main() -> Html { //! let agent = use_auth_agent().expect("Must be nested inside an OAuth2 component"); //! -//! let login = { -//! let agent = agent.clone(); -//! Callback::from(move |_| { -//! let _ = agent.start_login(); -//! }) -//! }; -//! let logout = Callback::from(move |_| { +//! let login = use_callback(agent.clone(), |_, agent| { +//! let _ = agent.start_login(); +//! }); +//! let logout = use_callback(agent, |_, agent| { //! let _ = agent.logout(); //! }); //! diff --git a/src/prelude.rs b/src/prelude.rs index e53ad5c..16f6465 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1,6 +1,6 @@ //! The prelude, includes most things you will need. -pub use crate::agent::{AgentConfiguration, LoginOptions, OAuth2Error, OAuth2Operations}; +pub use crate::agent::{LoginOptions, OAuth2Error, OAuth2Operations}; pub use crate::components::*; pub use crate::context::*; pub use crate::hook::*; From 9f2cdd243fc8d9803a75e3daf9af43fbe56bbd58 Mon Sep 17 00:00:00 2001 From: Jens Reimann Date: Fri, 26 Jan 2024 09:47:52 +0100 Subject: [PATCH 08/24] refactor: drop the legacy re-export BREAKING-CHANGE: All hooks are part of the `crate::hooks` module now. This legacy re-export was removed. --- src/context/mod.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/context/mod.rs b/src/context/mod.rs index a554a12..9893097 100644 --- a/src/context/mod.rs +++ b/src/context/mod.rs @@ -6,9 +6,6 @@ use std::cell::RefCell; use std::rc::Rc; pub use utils::*; -// re-export from there to keep API stable -pub use crate::hook::use_auth_state; - #[cfg(feature = "openid")] pub type Claims = openidconnect::IdTokenClaims< openidconnect::EmptyAdditionalClaims, From 243c864b2584a65a5bd76bf5505c91ce500df56e Mon Sep 17 00:00:00 2001 From: Jens Reimann Date: Fri, 26 Jan 2024 09:48:56 +0100 Subject: [PATCH 09/24] chore: uptick version --- Cargo.toml | 2 +- src/components/use_authentication.rs | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6facc17..cb89051 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "yew-oauth2" -version = "0.9.3" +version = "0.10.0" authors = ["Jens Reimann "] edition = "2021" license = "Apache-2.0" diff --git a/src/components/use_authentication.rs b/src/components/use_authentication.rs index 61dd1ea..6863cd5 100644 --- a/src/components/use_authentication.rs +++ b/src/components/use_authentication.rs @@ -1,7 +1,10 @@ //! The [`UseAuthentication`] component use super::missing_context; -use crate::context::{use_auth_state, Authentication, OAuth2Context}; +use crate::{ + context::{Authentication, OAuth2Context}, + hook::use_auth_state, +}; use std::rc::Rc; use yew::prelude::*; From b0d8e3f37fdf110373b407aabbbccec8e677919e Mon Sep 17 00:00:00 2001 From: Jens Reimann Date: Fri, 26 Jan 2024 10:21:02 +0100 Subject: [PATCH 10/24] refactor: tweak the API a bit, make it public again, document --- src/agent/mod.rs | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/agent/mod.rs b/src/agent/mod.rs index 78087d7..b93fdda 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -45,17 +45,22 @@ use yew::Callback; #[derive(Debug, Clone, Default)] #[non_exhaustive] pub struct LoginOptions { - pub(crate) query: HashMap, + /// Additional query parameters sent to the issuer. + pub query: HashMap, /// Defines the redirect URL. /// /// If this field is empty, the current URL is used as a redirect URL. - pub(crate) redirect_url: Option, + pub redirect_url: Option, /// Defines callback used for post-login redirect. /// - /// If None, disables post-login redirect - pub(crate) post_login_redirect_callback: Option>, + /// In cases where the issuer is asked to redirect to a different page than the one being active when starting + /// the login flow, this callback will be called with the current (when starting) URL once the login handshake + /// is complete. + /// + /// If `None`, disables post-login redirect. + pub post_login_redirect_callback: Option>, } impl LoginOptions { @@ -70,27 +75,30 @@ impl LoginOptions { } /// Extend the current query parameters for the login request - pub fn with_extended_query( - mut self, - query: impl IntoIterator, - ) -> Self { + pub fn extend_query(mut self, query: impl IntoIterator) -> Self { self.query.extend(query); self } - /// Define the redirect URL + /// Add a query parameter for the login request + pub fn add_query(mut self, key: impl Into, value: impl Into) -> Self { + self.query.insert(key.into(), value.into()); + self + } + + /// Set the redirect URL pub fn with_redirect_url(mut self, redirect_url: impl Into) -> Self { self.redirect_url = Some(redirect_url.into()); self } - /// Define callback for post-login redirect + /// Set a callback for post-login redirect pub fn with_redirect_callback(mut self, redirect_callback: Callback) -> Self { self.post_login_redirect_callback = Some(redirect_callback); self } - /// Use `yew-nested-router` history api for post-login redirect callback + /// Use `yew-nested-router` History API for post-login redirect callback #[cfg(feature = "yew-nested-router")] pub fn with_nested_router_redirect(mut self) -> Self { let callback = Callback::from(|url: String| { @@ -117,6 +125,10 @@ pub struct LogoutOptions { } impl LogoutOptions { + pub fn new() -> Self { + Self::default() + } + pub fn with_target(mut self, target: impl Into) -> Self { self.target = Some(target.into()); self From c494a36306163cb3d42e0f22e534e52de1ea3e27 Mon Sep 17 00:00:00 2001 From: Jens Reimann Date: Fri, 26 Jan 2024 10:21:13 +0100 Subject: [PATCH 11/24] refactor: rename properties --- src/components/context/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/context/mod.rs b/src/components/context/mod.rs index 81566d3..eafa1ea 100644 --- a/src/components/context/mod.rs +++ b/src/components/context/mod.rs @@ -14,7 +14,7 @@ use yew::prelude::*; /// Properties for the context component. #[derive(Clone, Debug, Properties)] -pub struct Props { +pub struct OAuth2Properties { /// The client configuration pub config: C::Configuration, @@ -44,7 +44,7 @@ pub struct Props { pub options: Option, } -impl PartialEq for Props { +impl PartialEq for OAuth2Properties { fn eq(&self, other: &Self) -> bool { self.config == other.config && self.scopes == other.scopes @@ -70,7 +70,7 @@ pub enum Msg { impl Component for OAuth2 { type Message = Msg; - type Properties = Props; + type Properties = OAuth2Properties; fn create(ctx: &Context) -> Self { let config = Self::make_config(ctx.props()); @@ -130,7 +130,7 @@ impl Component for OAuth2 { } impl OAuth2 { - fn make_config(props: &Props) -> AgentConfiguration { + fn make_config(props: &OAuth2Properties) -> AgentConfiguration { AgentConfiguration { config: props.config.clone(), scopes: props.scopes.clone(), From c8af310d16d11b653d5630857841a11a902a8f29 Mon Sep 17 00:00:00 2001 From: Jens Reimann Date: Fri, 26 Jan 2024 10:25:53 +0100 Subject: [PATCH 12/24] refactor: destructure to ensure we consumed all the fields --- src/agent/client/oauth2.rs | 14 ++++++++++---- src/agent/client/openid.rs | 29 +++++++++++++++-------------- src/agent/config.rs | 1 - src/agent/mod.rs | 20 ++++++++++++++------ src/components/context/mod.rs | 4 ---- 5 files changed, 39 insertions(+), 29 deletions(-) diff --git a/src/agent/client/oauth2.rs b/src/agent/client/oauth2.rs index aacd005..299bf4e 100644 --- a/src/agent/client/oauth2.rs +++ b/src/agent/client/oauth2.rs @@ -41,19 +41,25 @@ impl OAuth2Client { #[async_trait(?Send)] impl Client for OAuth2Client { - type TokenResponse = ::oauth2::basic::BasicTokenResponse; + type TokenResponse = BasicTokenResponse; type Configuration = oauth2::Config; type LoginState = LoginState; type SessionState = (); async fn from_config(config: Self::Configuration) -> Result { + let oauth2::Config { + client_id, + auth_url, + token_url, + } = config; + let client = BasicClient::new( - ClientId::new(config.client_id), + ClientId::new(client_id), None, - AuthUrl::new(config.auth_url) + AuthUrl::new(auth_url) .map_err(|err| OAuth2Error::Configuration(format!("invalid auth URL: {err}")))?, Some( - TokenUrl::new(config.token_url).map_err(|err| { + TokenUrl::new(token_url).map_err(|err| { OAuth2Error::Configuration(format!("invalid token URL: {err}")) })?, ), diff --git a/src/agent/client/openid.rs b/src/agent/client/openid.rs index 4abd1e5..209b6c7 100644 --- a/src/agent/client/openid.rs +++ b/src/agent/client/openid.rs @@ -85,7 +85,16 @@ impl Client for OpenIdClient { ); async fn from_config(config: Self::Configuration) -> Result { - let issuer = IssuerUrl::new(config.issuer_url) + let openid::Config { + client_id, + issuer_url, + end_session_url, + after_logout_url, + post_logout_redirect_name, + additional_trusted_audiences, + } = config; + + let issuer = IssuerUrl::new(issuer_url) .map_err(|err| OAuth2Error::Configuration(format!("invalid issuer URL: {err}")))?; let metadata = ExtendedProviderMetadata::discover_async(issuer, async_http_client) @@ -94,9 +103,7 @@ impl Client for OpenIdClient { OAuth2Error::Configuration(format!("Failed to discover client: {err}")) })?; - let end_session_url = config - .additional - .end_session_url + let end_session_url = end_session_url .map(|url| Url::parse(&url)) .transpose() .map_err(|err| { @@ -104,20 +111,14 @@ impl Client for OpenIdClient { })? .or_else(|| metadata.additional_metadata().end_session_endpoint.clone()); - let after_logout_url = config.additional.after_logout_url; - - let client = CoreClient::from_provider_metadata( - metadata, - ClientId::new(config.client_id.clone()), - None, - ); + let client = CoreClient::from_provider_metadata(metadata, ClientId::new(client_id), None); Ok(Self { client, end_session_url, after_logout_url, - post_logout_redirect_name: config.additional.post_logout_redirect_name, - additional_trusted_audiences: config.additional.additional_trusted_audiences, + post_logout_redirect_name, + additional_trusted_audiences, }) } @@ -271,7 +272,7 @@ impl OpenIdClient { fn after_logout_url(&self) -> Option { if let Some(after) = &self.after_logout_url { if Url::parse(after).is_ok() { - // test if the is an absolute URL + // test if this is an absolute URL return Some(after.to_string()); } diff --git a/src/agent/config.rs b/src/agent/config.rs index bc7409f..6c09c15 100644 --- a/src/agent/config.rs +++ b/src/agent/config.rs @@ -10,7 +10,6 @@ pub struct AgentConfiguration { pub grace_period: Duration, pub audience: Option, pub options: Option, - pub valid_audiences: Option>, } impl PartialEq for AgentConfiguration { diff --git a/src/agent/mod.rs b/src/agent/mod.rs index b93fdda..68f633e 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -326,13 +326,21 @@ where } async fn make_client(config: AgentConfiguration) -> Result<(C, InnerConfig), OAuth2Error> { - let client = C::from_config(config.config).await?; + let AgentConfiguration { + config, + scopes, + grace_period, + audience, + options, + } = config; + + let client = C::from_config(config).await?; let inner = InnerConfig { - scopes: config.scopes, - grace_period: config.grace_period, - audience: config.audience, - options: config.options, + scopes, + grace_period, + audience, + options, }; Ok((client, inner)) @@ -341,7 +349,7 @@ where /// When initializing, try to detect the state from the URL and session state. /// /// Returns `false` if there is no authentication state found and the result is final. - /// Otherwise it returns `true` and spawns a request for e.g. a code exchange. + /// Otherwise, it returns `true` and spawns a request for e.g. a code exchange. async fn detect_state(&mut self) -> Result { let client = self.client.as_ref().ok_or(OAuth2Error::NotInitialized)?; diff --git a/src/components/context/mod.rs b/src/components/context/mod.rs index eafa1ea..0576e3c 100644 --- a/src/components/context/mod.rs +++ b/src/components/context/mod.rs @@ -32,9 +32,6 @@ pub struct OAuth2Properties { #[prop_or_default] pub audience: Option, - #[prop_or_default] - pub valid_audiences: Option>, - /// Children which will have access to the [`OAuth2Context`]. #[prop_or_default] pub children: Children, @@ -135,7 +132,6 @@ impl OAuth2 { config: props.config.clone(), scopes: props.scopes.clone(), grace_period: props.grace_period, - valid_audiences: props.valid_audiences.clone(), options: props.options.clone(), audience: props.audience.clone(), } From a901bb6d1d45ad5b2d975a448e575ea653794ef5 Mon Sep 17 00:00:00 2001 From: Jens Reimann Date: Fri, 26 Jan 2024 10:42:57 +0100 Subject: [PATCH 13/24] fix: the default options did not get applied --- src/agent/config.rs | 7 +++-- src/agent/mod.rs | 49 ++++++++++++++++++++++++++--------- src/agent/ops.rs | 8 ++---- src/components/context/mod.rs | 13 +++++++--- 4 files changed, 53 insertions(+), 24 deletions(-) diff --git a/src/agent/config.rs b/src/agent/config.rs index 6c09c15..7d7f704 100644 --- a/src/agent/config.rs +++ b/src/agent/config.rs @@ -1,4 +1,4 @@ -use super::LoginOptions; +use super::{LoginOptions, LogoutOptions}; use crate::agent::Client; use std::time::Duration; @@ -9,7 +9,9 @@ pub struct AgentConfiguration { pub scopes: Vec, pub grace_period: Duration, pub audience: Option, - pub options: Option, + + pub default_login_options: Option, + pub default_logout_options: Option, } impl PartialEq for AgentConfiguration { @@ -17,6 +19,7 @@ impl PartialEq for AgentConfiguration { self.config == other.config && self.scopes == other.scopes && self.grace_period == other.grace_period + && self.audience == other.audience } } diff --git a/src/agent/mod.rs b/src/agent/mod.rs index 68f633e..386a1c1 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -140,8 +140,8 @@ where C: Client, { Configure(AgentConfiguration), - StartLogin(LoginOptions), - Logout(LogoutOptions), + StartLogin(Option), + Logout(Option), Refresh, } @@ -188,7 +188,8 @@ pub struct InnerConfig { scopes: Vec, grace_period: Duration, audience: Option, - options: Option, + default_login_options: Option, + default_logout_options: Option, } impl InnerAgent @@ -331,7 +332,8 @@ where scopes, grace_period, audience, - options, + default_login_options, + default_logout_options, } = config; let client = C::from_config(config).await?; @@ -340,7 +342,8 @@ where scopes, grace_period, audience, - options, + default_login_options, + default_logout_options, }; Ok((client, inner)) @@ -419,7 +422,7 @@ where fn post_login_redirect(&self) -> Result<(), OAuth2Error> { let config = self.config.as_ref().ok_or(OAuth2Error::NotInitialized)?; let Some(redirect_callback) = config - .options + .default_login_options .as_ref() .and_then(|opts| opts.post_login_redirect_callback.clone()) else { @@ -529,10 +532,13 @@ where self.configured(Self::make_client(config).await).await; } - fn start_login(&mut self, options: LoginOptions) -> Result<(), OAuth2Error> { + fn start_login(&mut self, options: Option) -> Result<(), OAuth2Error> { let client = self.client.as_ref().ok_or(OAuth2Error::NotInitialized)?; let config = self.config.as_ref().ok_or(OAuth2Error::NotInitialized)?; + let options = + options.unwrap_or_else(|| config.default_login_options.clone().unwrap_or_default()); + let current_url = Self::current_url().map_err(OAuth2Error::StartLogin)?; // take the parameter value first, then the agent configured value, then fall back to the default @@ -540,7 +546,7 @@ where .redirect_url .or_else(|| { config - .options + .default_login_options .as_ref() .and_then(|opts| opts.redirect_url.clone()) }) @@ -565,7 +571,7 @@ where let mut login_url = login_context.url; login_url.query_pairs_mut().extend_pairs(options.query); - if let Some(options) = &config.options { + if let Some(options) = &config.default_login_options { login_url .query_pairs_mut() .extend_pairs(options.query.clone()); @@ -586,12 +592,19 @@ where Ok(()) } - fn logout_opts(&mut self, options: LogoutOptions) { + fn logout_opts(&mut self, options: Option) { if let Some(client) = &self.client { if let Some(session_state) = self.session_state.clone() { // let the client know that log out, clients may navigate to a different // page log::debug!("Notify client of logout"); + let options = options + .or_else(|| { + self.config + .as_ref() + .and_then(|config| config.default_logout_options.clone()) + }) + .unwrap_or_default(); client.logout(session_state, options); } } @@ -618,15 +631,27 @@ where .map_err(|_| Error::NoAgent) } + fn start_login(&self) -> Result<(), Error> { + self.tx + .try_send(Msg::StartLogin(None)) + .map_err(|_| Error::NoAgent) + } + fn start_login_opts(&self, options: LoginOptions) -> Result<(), Error> { self.tx - .try_send(Msg::StartLogin(options)) + .try_send(Msg::StartLogin(Some(options))) + .map_err(|_| Error::NoAgent) + } + + fn logout(&self) -> Result<(), Error> { + self.tx + .try_send(Msg::Logout(None)) .map_err(|_| Error::NoAgent) } fn logout_opts(&self, options: LogoutOptions) -> Result<(), Error> { self.tx - .try_send(Msg::Logout(options)) + .try_send(Msg::Logout(Some(options))) .map_err(|_| Error::NoAgent) } } diff --git a/src/agent/ops.rs b/src/agent/ops.rs index 13a4771..f26a128 100644 --- a/src/agent/ops.rs +++ b/src/agent/ops.rs @@ -25,17 +25,13 @@ pub trait OAuth2Operations { fn configure(&self, config: AgentConfiguration) -> Result<(), Error>; /// Start a login flow with default options. - fn start_login(&self) -> Result<(), Error> { - self.start_login_opts(Default::default()) - } + fn start_login(&self) -> Result<(), Error>; /// Start a login flow. fn start_login_opts(&self, options: LoginOptions) -> Result<(), Error>; /// Trigger the logout with default options. - fn logout(&self) -> Result<(), Error> { - self.logout_opts(Default::default()) - } + fn logout(&self) -> Result<(), Error>; /// Trigger the logout. fn logout_opts(&self, options: LogoutOptions) -> Result<(), Error>; diff --git a/src/components/context/mod.rs b/src/components/context/mod.rs index 0576e3c..eed0850 100644 --- a/src/components/context/mod.rs +++ b/src/components/context/mod.rs @@ -5,7 +5,7 @@ mod agent; pub use agent::*; use crate::{ - agent::{AgentConfiguration, Client, LoginOptions, OAuth2Operations}, + agent::{AgentConfiguration, Client, LoginOptions, LogoutOptions, OAuth2Operations}, context::{LatestAccessToken, OAuth2Context}, }; use agent::Agent as AgentContext; @@ -36,9 +36,13 @@ pub struct OAuth2Properties { #[prop_or_default] pub children: Children, - /// Default [`LoginOptions`] that will be used for every request + /// Default [`LoginOptions`] that will be used unless more specific options have been requested. #[prop_or_default] - pub options: Option, + pub login_options: Option, + + /// Default [`LogoutOptions`] that will be used unless more specific options have been requested. + #[prop_or_default] + pub logout_options: Option, } impl PartialEq for OAuth2Properties { @@ -132,8 +136,9 @@ impl OAuth2 { config: props.config.clone(), scopes: props.scopes.clone(), grace_period: props.grace_period, - options: props.options.clone(), audience: props.audience.clone(), + default_login_options: props.login_options.clone(), + default_logout_options: props.logout_options.clone(), } } } From 68e8cfcd1d0aa3df188b55d3ab49da37ce8f7dd4 Mon Sep 17 00:00:00 2001 From: Jens Reimann Date: Fri, 26 Jan 2024 10:46:53 +0100 Subject: [PATCH 14/24] style: typos --- src/components/redirect/location.rs | 2 +- src/components/redirect/mod.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/redirect/location.rs b/src/components/redirect/location.rs index 3d2f438..9c84f0e 100644 --- a/src/components/redirect/location.rs +++ b/src/components/redirect/location.rs @@ -4,7 +4,7 @@ use super::{Redirect, Redirector, RedirectorProperties}; use gloo_utils::window; use yew::prelude::*; -/// A redirector using the browsers location. +/// A redirector using the browser's location. pub struct LocationRedirector {} impl Redirector for LocationRedirector { diff --git a/src/components/redirect/mod.rs b/src/components/redirect/mod.rs index 84535a5..cca455b 100644 --- a/src/components/redirect/mod.rs +++ b/src/components/redirect/mod.rs @@ -143,7 +143,7 @@ where } } _ => { - // expired or logged out explicitly, then redirect to logout page + // expired or logged out explicitly, then redirect to the logout page self.logout(ctx.props()); } } From 7c841a3d8515f8550817f5964aa5901d27bff64c Mon Sep 17 00:00:00 2001 From: Jens Reimann Date: Fri, 26 Jan 2024 10:52:05 +0100 Subject: [PATCH 15/24] docs: uptick version in docs --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 11e6f95..4e928e0 100644 --- a/README.md +++ b/README.md @@ -7,14 +7,14 @@ Add to your `Cargo.toml`: ```toml -yew-oauth2 = "0.9" +yew-oauth2 = "0.10" ``` By default, the `router` integration for [`yew-nested-router`](https://github.com/ctron/yew-nested-router) is disabled, you can enable it using: ```toml -yew-oauth2 = { version = "0.9", features = ["router"] } +yew-oauth2 = { version = "0.10", features = ["router"] } ``` ## OpenID Connect From 9247002f0b9bbd8b5f1c53fa6fd3430a0f5259ee Mon Sep 17 00:00:00 2001 From: Jens Reimann Date: Fri, 26 Jan 2024 11:13:08 +0100 Subject: [PATCH 16/24] ci: update actions to non-deprecated ones --- .github/workflows/ci.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b1300c6..942d979 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -21,14 +21,14 @@ jobs: steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install Rust ${{ matrix.toolchain }} run: | rustup toolchain install ${{ matrix.toolchain }} --component rustfmt,clippy --target wasm32-unknown-unknown rustup default ${{ matrix.toolchain }} - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: path: | ~/.cargo/registry/index/ @@ -67,9 +67,9 @@ jobs: steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: path: | ~/.cargo/registry/index/ From aa7eb5af8222f06660525e118a00f408171eab66 Mon Sep 17 00:00:00 2001 From: Jens Reimann Date: Fri, 26 Jan 2024 11:18:16 +0100 Subject: [PATCH 17/24] chore: fix up examples after refactoring --- yew-oauth2-example/src/app.rs | 16 ++++----- yew-oauth2-redirect-example/src/app.rs | 45 +++++++++++--------------- 2 files changed, 24 insertions(+), 37 deletions(-) diff --git a/yew-oauth2-example/src/app.rs b/yew-oauth2-example/src/app.rs index e8992c4..6bb771e 100644 --- a/yew-oauth2-example/src/app.rs +++ b/yew-oauth2-example/src/app.rs @@ -103,18 +103,14 @@ pub fn content() -> Html { #[function_component(Application)] pub fn app() -> Html { #[cfg(not(feature = "openid"))] - let config = Config { - client_id: "example".into(), - auth_url: "http://localhost:8081/realms/master/protocol/openid-connect/auth".into(), - token_url: "http://localhost:8081/realms/master/protocol/openid-connect/token".into(), - }; + let config = Config::new( + "example", + "http://localhost:8081/realms/master/protocol/openid-connect/auth", + "http://localhost:8081/realms/master/protocol/openid-connect/token", + ); #[cfg(feature = "openid")] - let config = Config { - client_id: "example".into(), - issuer_url: "http://localhost:8081/realms/master".into(), - additional: Default::default(), - }; + let config = Config::new("example", "http://localhost:8081/realms/master"); let mode = if cfg!(feature = "openid") { "OpenID Connect" diff --git a/yew-oauth2-redirect-example/src/app.rs b/yew-oauth2-redirect-example/src/app.rs index 5c5c555..a89818a 100644 --- a/yew-oauth2-redirect-example/src/app.rs +++ b/yew-oauth2-redirect-example/src/app.rs @@ -28,15 +28,12 @@ pub enum AuthenticatedRoute { pub fn content() -> Html { let agent = use_auth_agent().expect("Requires OAuth2Context component in parent hierarchy"); - let login = { - let agent = agent.clone(); - Callback::from(move |_: MouseEvent| { - if let Err(err) = agent.start_login() { - log::warn!("Failed to start login: {err}"); - } - }) - }; - let logout = Callback::from(move |_: MouseEvent| { + let login = use_callback(agent.clone(), |_, agent| { + if let Err(err) = agent.start_login() { + log::warn!("Failed to start login: {err}"); + } + }); + let logout = use_callback(agent, |_, agent| { if let Err(err) = agent.logout() { log::warn!("Failed to logout: {err}"); } @@ -119,26 +116,20 @@ pub fn content() -> Html { #[function_component(Application)] pub fn app() -> Html { #[cfg(not(feature = "openid"))] - let config = Config { - client_id: "example".into(), - auth_url: "http://localhost:8081/realms/master/protocol/openid-connect/auth".into(), - token_url: "http://localhost:8081/realms/master/protocol/openid-connect/token".into(), - }; + let config = Config::new( + "example", + "http://localhost:8081/realms/master/protocol/openid-connect/auth", + "http://localhost:8081/realms/master/protocol/openid-connect/token", + ); #[cfg(feature = "openid")] - let config = Config { - client_id: "example".into(), - issuer_url: "http://localhost:8081/realms/master".into(), - additional: Additional { - /* - Set the after logout URL to a public URL. Otherwise, the SSO server will redirect - back to the current page, which is detected as a new session, and will try to login - again, if the page requires this. - */ - after_logout_url: Some("/".into()), - ..Default::default() - }, - }; + let config = Config::new("example", "http://localhost:8081/realms/master") + /* + Set the after logout URL to a public URL. Otherwise, the SSO server will redirect + back to the current page, which is detected as a new session, and will try to log in + again, if the page requires this. + */ + .with_after_logout_url(Some("/".into())); let mode = if cfg!(feature = "openid") { "OpenID Connect" From e1564424f8911959a222a656b79696c9578e9553 Mon Sep 17 00:00:00 2001 From: Jens Reimann Date: Fri, 26 Jan 2024 11:20:10 +0100 Subject: [PATCH 18/24] chore: drop patches section, we're good for now --- yew-oauth2-example/Cargo.toml | 3 --- yew-oauth2-redirect-example/Cargo.toml | 3 --- 2 files changed, 6 deletions(-) diff --git a/yew-oauth2-example/Cargo.toml b/yew-oauth2-example/Cargo.toml index 6116807..95d42a0 100644 --- a/yew-oauth2-example/Cargo.toml +++ b/yew-oauth2-example/Cargo.toml @@ -21,6 +21,3 @@ openidconnect = { version = "3.0", optional = true } [features] default = ["openid"] openid = ["openidconnect", "yew-oauth2/openid"] - -[patch.crates-io] -#openidconnect = { git = "https://github.com/ramosbugs/openidconnect-rs", rev = "64be3c7636bc6f745a9b33f0702950bbacca318c" } diff --git a/yew-oauth2-redirect-example/Cargo.toml b/yew-oauth2-redirect-example/Cargo.toml index 6ef46a2..efa3e60 100644 --- a/yew-oauth2-redirect-example/Cargo.toml +++ b/yew-oauth2-redirect-example/Cargo.toml @@ -20,6 +20,3 @@ openidconnect = { version = "3.0", optional = true } [features] default = ["openid"] openid = ["openidconnect", "yew-oauth2/openid"] - -[patch.crates-io] -#openidconnect = { git = "https://github.com/ctron/openidconnect-rs", rev = "6ca4a9ab9de35600c44a8b830693137d4769edf4" } From 530cbb8e3723c539fdff1800288001ed03e18c63 Mon Sep 17 00:00:00 2001 From: Jens Reimann Date: Fri, 26 Jan 2024 11:49:43 +0100 Subject: [PATCH 19/24] chore: more docs and cleanups --- src/agent/client/mod.rs | 2 ++ src/agent/client/oauth2.rs | 3 ++- src/agent/client/openid.rs | 1 + src/agent/error.rs | 7 +++++++ src/agent/mod.rs | 28 +++++++++++++++++++++++++++- src/agent/ops.rs | 1 + src/components/redirect/location.rs | 14 ++++++++------ src/components/redirect/mod.rs | 4 ++-- src/components/redirect/router.rs | 11 ++++++----- src/lib.rs | 2 +- 10 files changed, 57 insertions(+), 16 deletions(-) diff --git a/src/agent/client/mod.rs b/src/agent/client/mod.rs index e270050..0aab190 100644 --- a/src/agent/client/mod.rs +++ b/src/agent/client/mod.rs @@ -1,3 +1,5 @@ +//! Client implementations + mod oauth2; #[cfg(feature = "openid")] mod openid; diff --git a/src/agent/client/oauth2.rs b/src/agent/client/oauth2.rs index 299bf4e..5f390d1 100644 --- a/src/agent/client/oauth2.rs +++ b/src/agent/client/oauth2.rs @@ -22,9 +22,10 @@ pub struct LoginState { pub pkce_verifier: String, } +/// An OAuth2 based client implementation #[derive(Clone, Debug)] pub struct OAuth2Client { - client: ::oauth2::basic::BasicClient, + client: BasicClient, } impl OAuth2Client { diff --git a/src/agent/client/openid.rs b/src/agent/client/openid.rs index 209b6c7..3dcfe27 100644 --- a/src/agent/client/openid.rs +++ b/src/agent/client/openid.rs @@ -33,6 +33,7 @@ pub struct OpenIdLoginState { const DEFAULT_POST_LOGOUT_DIRECT_NAME: &str = "post_logout_redirect_uri"; +/// An OpenID Connect based client implementation #[derive(Clone, Debug)] pub struct OpenIdClient { /// The client diff --git a/src/agent/error.rs b/src/agent/error.rs index 37b863e..b3acc7e 100644 --- a/src/agent/error.rs +++ b/src/agent/error.rs @@ -1,13 +1,20 @@ use crate::context::OAuth2Context; use core::fmt::{Display, Formatter}; +/// An error with the OAuth2 agent #[derive(Debug)] pub enum OAuth2Error { + /// Not initialized NotInitialized, + /// Configuration error Configuration(String), + /// Failed to start login StartLogin(String), + /// Failed to handle login result LoginResult(String), + /// Failing storing information Storage(String), + /// Internal error Internal(String), } diff --git a/src/agent/mod.rs b/src/agent/mod.rs index 386a1c1..dff8cdf 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -1,4 +1,4 @@ -//! The Yew agent, working in the background to manage the session and refresh tokens. +//! The agent, working in the background to manage the session and refresh tokens. pub mod client; mod config; @@ -42,6 +42,28 @@ use yew::Callback; /// # let url = Url::parse("https://example.com").unwrap(); /// let opts = LoginOptions::default().with_redirect_url(url); /// ``` +/// +/// ## Redirect & Post login redirect +/// +/// By default, the login process will ask the issuer to redirect back the page that was active when starting the login +/// process. In some cases, the issuer might require a more strict set of redirect URLs, and so can only redirect back +/// to a single page. This can be enabled set setting a specific URL as `redirect_url`. +/// +/// Once the user comes back from the login flow, which might actually be without any user interaction if the session +/// was still valid, the user might find himself on the redirect page. Therefore, it is possible to forward/redirect +/// the back to the original page, but only after the issuer redirected back the `redirect_url`. If, while starting the +/// login process, the currently active URL differs from the `redirect_url`, the agent will store the "current" URL and +/// redirect to it once the login process has completed. +/// +/// However, there can be different ways to redirect, and there is no common one. One might think just setting a new +/// location in the browser should work, but that would actually cause a page reload, and would then start the login +/// process again. +/// +/// Therefore, it is possible to set a "post login redirect callback", which will be triggered in such cases. Letting +/// the user of the crate implement this logic. Having the `yew-nested-router` feature enabled, it is possible to just +/// call [`LoginOptions::with_nested_router_redirect`] and let the router take care of this. +/// +/// **NOTE:** The default is to do nothing. So the user would simply end up on the page defined by `redirect_url`. #[derive(Debug, Clone, Default)] #[non_exhaustive] pub struct LoginOptions { @@ -135,6 +157,7 @@ impl LogoutOptions { } } +#[doc(hidden)] pub enum Msg where C: Client, @@ -145,6 +168,7 @@ where Refresh, } +/// The agent handling the OAuth2/OIDC state #[derive(Clone, Debug)] pub struct Agent where @@ -170,6 +194,7 @@ where } } +#[doc(hidden)] pub struct InnerAgent where C: Client, @@ -183,6 +208,7 @@ where timeout: Option, } +#[doc(hidden)] #[derive(Clone, Debug)] pub struct InnerConfig { scopes: Vec, diff --git a/src/agent/ops.rs b/src/agent/ops.rs index f26a128..5ec6599 100644 --- a/src/agent/ops.rs +++ b/src/agent/ops.rs @@ -1,6 +1,7 @@ use super::{AgentConfiguration, Client, LoginOptions, LogoutOptions}; use std::fmt::{Display, Formatter}; +/// Operation error #[derive(Clone, Debug)] pub enum Error { /// The agent cannot be reached. diff --git a/src/components/redirect/location.rs b/src/components/redirect/location.rs index 9c84f0e..55755ff 100644 --- a/src/components/redirect/location.rs +++ b/src/components/redirect/location.rs @@ -5,10 +5,10 @@ use gloo_utils::window; use yew::prelude::*; /// A redirector using the browser's location. -pub struct LocationRedirector {} +pub struct LocationRedirector; impl Redirector for LocationRedirector { - type Properties = LocationProps; + type Properties = LocationProperties; fn new(_: &Context) -> Self { Self {} @@ -21,15 +21,17 @@ impl Redirector for LocationRedirector { } #[derive(Clone, Debug, PartialEq, Properties)] -pub struct LocationProps { +pub struct LocationProperties { + /// The content to show when being logged in. #[prop_or_default] - pub children: Children, + pub children: Html, + /// The logout URL to redirect to pub logout_href: String, } -impl RedirectorProperties for LocationProps { - fn children(&self) -> &Children { +impl RedirectorProperties for LocationProperties { + fn children(&self) -> &Html { &self.children } } diff --git a/src/components/redirect/mod.rs b/src/components/redirect/mod.rs index cca455b..55b2cc4 100644 --- a/src/components/redirect/mod.rs +++ b/src/components/redirect/mod.rs @@ -19,7 +19,7 @@ pub trait Redirector: 'static { } pub trait RedirectorProperties: yew::Properties { - fn children(&self) -> &Children; + fn children(&self) -> &Html; } #[derive(Debug, Clone)] @@ -104,7 +104,7 @@ where fn view(&self, ctx: &Context) -> Html { match self.auth { None => missing_context(), - Some(OAuth2Context::Authenticated(..)) => html!({for ctx.props().children().iter()}), + Some(OAuth2Context::Authenticated(..)) => ctx.props().children().clone(), _ => html!(), } } diff --git a/src/components/redirect/router.rs b/src/components/redirect/router.rs index 4081b49..f7d164e 100644 --- a/src/components/redirect/router.rs +++ b/src/components/redirect/router.rs @@ -17,7 +17,7 @@ impl Redirector for RouterRedirector where R: Target + 'static, { - type Properties = RouterProps; + type Properties = RouterProperties; fn new(ctx: &Context) -> Self { // while the "route" can change, the "router" itself does not. @@ -43,21 +43,22 @@ where } } +/// Properties for the [`RouterRedirector`] component. #[derive(Clone, Debug, PartialEq, Properties)] -pub struct RouterProps +pub struct RouterProperties where R: Target + 'static, { #[prop_or_default] - pub children: Children, + pub children: Html, pub logout: R, } -impl RedirectorProperties for RouterProps +impl RedirectorProperties for RouterProperties where R: Target + 'static, { - fn children(&self) -> &Children { + fn children(&self) -> &Html { &self.children } } diff --git a/src/lib.rs b/src/lib.rs index 0a05685..2bae789 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,7 @@ //! //! ## Example //! -//! **NOTE:** Also see the readme for more examples. +//! **NOTE:** Also see the [readme](https://github.com/ctron/yew-oauth2/blob/main/README.md#examples) for more examples. //! //! The following is a basic example: //! From 28495f055cbbd07d7715d1896197d522f6bcce05 Mon Sep 17 00:00:00 2001 From: Jens Reimann Date: Fri, 26 Jan 2024 12:01:26 +0100 Subject: [PATCH 20/24] ci: try fixing compiler issue --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 942d979..4855b73 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -17,7 +17,7 @@ jobs: toolchain: - stable # minimum version: because of "clap_lex" - - "1.70" + - "1.71.1" steps: From 78440f62a8531175c6c0d541863c0c2c994de5ba Mon Sep 17 00:00:00 2001 From: Jens Reimann Date: Fri, 26 Jan 2024 12:05:07 +0100 Subject: [PATCH 21/24] ci: explicitly set toolchain --- .github/workflows/ci.yaml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4855b73..1044692 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -39,7 +39,7 @@ jobs: - name: Run cargo fmt run: | - cargo fmt --all -- --check + cargo +${{ matrix.toolchain }} fmt --all -- --check - name: Install binstall run: | @@ -51,15 +51,19 @@ jobs: - name: Run cargo check run: | - cargo check-all-features --target wasm32-unknown-unknown + cargo +${{ matrix.toolchain }} check-all-features --target wasm32-unknown-unknown - name: Run cargo test run: | - cargo test-all-features + cargo +${{ matrix.toolchain }} test-all-features - name: Run cargo clippy run: | - cargo clippy --target wasm32-unknown-unknown -- -D warnings + cargo +${{ matrix.toolchain }} clippy --target wasm32-unknown-unknown -- -D warnings + + - name: Run cargo clippy (all features) + run: | + cargo +${{ matrix.toolchain }} clippy --all-features --target wasm32-unknown-unknown -- -D warnings examples: From e0f6924896bdc641385d3e5e34a29a578de3aa21 Mon Sep 17 00:00:00 2001 From: Jens Reimann Date: Fri, 26 Jan 2024 14:42:12 +0100 Subject: [PATCH 22/24] chore: align visibility with login options --- src/agent/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agent/mod.rs b/src/agent/mod.rs index dff8cdf..9a7be16 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -143,7 +143,7 @@ pub struct LogoutOptions { /// An optional target to navigate to after the user was logged out. /// /// This would override any settings from the client configuration. - pub(crate) target: Option, + pub target: Option, } impl LogoutOptions { From 5b9702d245b0acd2d7e45a4cc05a3ef42000fbea Mon Sep 17 00:00:00 2001 From: Jens Reimann Date: Fri, 26 Jan 2024 14:44:56 +0100 Subject: [PATCH 23/24] chore: rename feature "router" to "nested-router" BREAKING-CHANGE: The feature named "router" got renamed to "nested-router" --- Cargo.toml | 2 +- README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cb89051..ce080d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ yew-nested-router = { version = "0.6.3", optional = true } [features] # Enable for Yew nested router support -router = ["yew-nested-router"] +nested-router = ["yew-nested-router"] # Enable for OpenID Connect support openid = ["openidconnect"] diff --git a/README.md b/README.md index 4e928e0..7d32a7a 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,11 @@ Add to your `Cargo.toml`: yew-oauth2 = "0.10" ``` -By default, the `router` integration for [`yew-nested-router`](https://github.com/ctron/yew-nested-router) is disabled, +By default, the `nested-router` integration for [`yew-nested-router`](https://github.com/ctron/yew-nested-router) is disabled, you can enable it using: ```toml -yew-oauth2 = { version = "0.10", features = ["router"] } +yew-oauth2 = { version = "0.10", features = ["nested-router"] } ``` ## OpenID Connect From 8d2bc39697cb74c3033a195316103b816daa7051 Mon Sep 17 00:00:00 2001 From: Jens Reimann Date: Fri, 26 Jan 2024 15:19:54 +0100 Subject: [PATCH 24/24] refactor: allow retrieving the login state from the session --- src/agent/mod.rs | 25 +++++-------------------- src/agent/state.rs | 39 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 21 deletions(-) diff --git a/src/agent/mod.rs b/src/agent/mod.rs index 9a7be16..be9bb4c 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -9,11 +9,12 @@ mod state; pub use client::*; pub use error::*; pub use ops::*; +pub use state::LoginState; pub(crate) use config::*; use crate::context::{Authentication, OAuth2Context, Reason}; -use gloo_storage::{errors::StorageError, SessionStorage, Storage}; +use gloo_storage::{SessionStorage, Storage}; use gloo_timers::callback::Timeout; use gloo_utils::{history, window}; use js_sys::Date; @@ -21,7 +22,6 @@ use log::error; use num_traits::cast::ToPrimitive; use reqwest::Url; use state::*; -use std::fmt::Display; use std::{collections::HashMap, fmt::Debug, time::Duration}; use tokio::sync::mpsc::{channel, Receiver, Sender}; use wasm_bindgen::JsValue; @@ -412,7 +412,7 @@ where )) } Some(state) => { - let stored_state = Self::get_from_store(STORAGE_KEY_CSRF_TOKEN)?; + let stored_state = get_from_store(STORAGE_KEY_CSRF_TOKEN)?; if state != stored_state { return Err(OAuth2Error::LoginResult("State mismatch".to_string())); @@ -427,7 +427,7 @@ where log::debug!("Login state: {state:?}"); - let redirect_url = Self::get_from_store(STORAGE_KEY_REDIRECT_URL)?; + let redirect_url = get_from_store(STORAGE_KEY_REDIRECT_URL)?; log::debug!("Redirect URL: {redirect_url}"); let redirect_url = Url::parse(&redirect_url).map_err(|err| { OAuth2Error::LoginResult(format!("Failed to parse redirect URL: {err}")) @@ -454,7 +454,7 @@ where else { return Ok(()); }; - let Some(url) = Self::get_from_store_optional(STORAGE_KEY_POST_LOGIN_URL)? else { + let Some(url) = get_from_store_optional(STORAGE_KEY_POST_LOGIN_URL)? else { return Ok(()); }; SessionStorage::delete(STORAGE_KEY_POST_LOGIN_URL); @@ -506,21 +506,6 @@ where } } - fn get_from_store + Display>(key: K) -> Result { - Self::get_from_store_optional(&key)?.ok_or_else(|| OAuth2Error::storage_key_empty(key)) - } - - fn get_from_store_optional + Display>( - key: K, - ) -> Result, OAuth2Error> { - match SessionStorage::get::(key.as_ref()) { - Err(StorageError::KeyNotFound(_)) => Ok(None), - Err(err) => Err(OAuth2Error::Storage(err.to_string())), - Ok(value) if value.is_empty() => Err(OAuth2Error::storage_key_empty(key)), - Ok(value) => Ok(Some(value)), - } - } - /// Extract the state from the query. fn find_query_state() -> Option { if let Ok(url) = Self::current_url() { diff --git a/src/agent/state.rs b/src/agent/state.rs index c265b6c..595dc99 100644 --- a/src/agent/state.rs +++ b/src/agent/state.rs @@ -1,11 +1,48 @@ +use super::OAuth2Error; +use gloo_storage::errors::StorageError; +use gloo_storage::{SessionStorage, Storage}; +use std::fmt::Display; + pub(crate) const STORAGE_KEY_CSRF_TOKEN: &str = "ctron/oauth2/csrfToken"; pub(crate) const STORAGE_KEY_LOGIN_STATE: &str = "ctron/oauth2/loginState"; pub(crate) const STORAGE_KEY_REDIRECT_URL: &str = "ctron/oauth2/redirectUrl"; pub(crate) const STORAGE_KEY_POST_LOGIN_URL: &str = "ctron/oauth2/postLoginUrl"; #[derive(Debug)] -pub struct State { +pub(crate) struct State { pub code: Option, pub state: Option, pub error: Option, } + +pub(crate) fn get_from_store + Display>(key: K) -> Result { + get_from_store_optional(&key)?.ok_or_else(|| OAuth2Error::storage_key_empty(key)) +} + +pub(crate) fn get_from_store_optional + Display>( + key: K, +) -> Result, OAuth2Error> { + match SessionStorage::get::(key.as_ref()) { + Err(StorageError::KeyNotFound(_)) => Ok(None), + Err(err) => Err(OAuth2Error::Storage(err.to_string())), + Ok(value) if value.is_empty() => Err(OAuth2Error::storage_key_empty(key)), + Ok(value) => Ok(Some(value)), + } +} + +/// Login state, stored in the session +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LoginState { + pub redirect_url: Option, + pub post_login_url: Option, +} + +impl LoginState { + /// Read the state from the session + pub fn from_storage() -> Result { + Ok(Self { + redirect_url: get_from_store_optional(STORAGE_KEY_REDIRECT_URL)?, + post_login_url: get_from_store_optional(STORAGE_KEY_POST_LOGIN_URL)?, + }) + } +}