From 5f5bb48c1a4ddd3c3e1c3521ff6e84f75743d385 Mon Sep 17 00:00:00 2001 From: Patrick Schneider Date: Sun, 16 Jan 2022 00:42:43 +0100 Subject: [PATCH] Swapped out oauth2 for oidcconnect library --- Cargo.toml | 4 +- src/controller/auth_manager/client_data.rs | 125 +++++++------- src/controller/auth_manager/mod.rs | 182 +++++++++++++++------ src/controller/auth_manager/pkce.rs | 58 ++++--- src/controller/framework/mod.rs | 14 +- 5 files changed, 245 insertions(+), 138 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b3ffaed..fbac613 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,9 +25,9 @@ features = [ "Location" ] -[dependencies.oauth2] +[dependencies.openidconnect] features = ["reqwest"] -version = "4.1.0" +version = "2.2.0" [dev-dependencies] wasm-bindgen-test = "0.3.28" diff --git a/src/controller/auth_manager/client_data.rs b/src/controller/auth_manager/client_data.rs index 6acdec8..f53afac 100644 --- a/src/controller/auth_manager/client_data.rs +++ b/src/controller/auth_manager/client_data.rs @@ -4,130 +4,141 @@ /// 2022, Patrick Schneider use wasm_bindgen::prelude::*; -use oauth2::{ +use wasm_bindgen::throw_str; +use openidconnect::{ ClientId, - AuthUrl, - RedirectUrl, - TokenUrl + IssuerUrl, + RedirectUrl }; -use oauth2::basic::BasicClient; +use openidconnect::core::{ + CoreClient, + CoreProviderMetadata +}; +use openidconnect::reqwest::async_http_client; + use super::auth_error::AuthError; -/// The ClientData struct stores the relevant authentication provider data used in the authentication process. +/// The OIDCClientData struct stores the relevant authentication provider data used in the authentication process. /// #[wasm_bindgen] -pub struct ClientData { +pub struct OIDCClientData { /// The URL to redirect to. /// Must be known to the authentication provider redirect_url: RedirectUrl, - /// The URL of the authentication provider. - auth_url: AuthUrl, - - // The URL to fetch the token of the authentication provider. - token_url: TokenUrl, - /// The client id registered at the authentication provider. - client_id: ClientId + client_id: ClientId, + + metadata: CoreProviderMetadata } #[wasm_bindgen] -impl ClientData { +impl OIDCClientData { - /// Create a new ClientData instance with the given values + /// Create a new OIDCClientData instance with the given values. /// /// # Arguments /// - /// * `auth_url` - The endpoint of the used authentication provider - /// * `token_url` - The endpoint used to fetch tokens on + /// * `issuer_url` - The url of the used authentication provider /// * `client_id` - The at the authentication provider registered client id /// * `redirect_url`- The at the authentication provider registered redirection url /// + /// # Returns + /// + /// * `OIDCClientData` + /// + /// # Throws + /// If any of the given values are not valid, e.g not a url provided + /// /// # Example /// ```rust - /// let auth_url = String::from("https://auth_provider.org/auth"); - /// let token_url = String::from("https://auth_provider.org/token"); + /// let issuer_url = String::from("https://auth_provider.org/"); /// let client_id = String::from("my-client-id"); /// let redirect_url = String::from("https://my.site"); - /// let client: ClientData = ClientData::new(auth_url, token_url, client_id, redirect_url); + /// let client: OIDCClientData = OIDCClientData::new(auth_url, token_url, client_id, redirect_url); /// ``` - pub fn from( - auth_url: String, + pub async fn from( + issuer_url: String, token_url: String, client_id: String, - redirect_url: String) -> Result { + redirect_url: String) -> OIDCClientData { - match ( - AuthUrl::new(auth_url), - TokenUrl::new(token_url), + let (issuer, client, redirect) = match ( + IssuerUrl::new(issuer_url), ClientId::new(client_id), RedirectUrl::new(redirect_url) ) { - (Ok(auth_url), Ok(token_url), client_id, Ok(redirect_url)) => Ok( - ClientData::new( - auth_url, - token_url, - client_id, - redirect_url - ) - ), - _ => Err(JsValue::from(AuthError::from("The provided data is not correct!"))) + (Ok(issuer_url), client_id, Ok(redirect_url)) => (issuer_url, client_id, redirect_url), + (Err(err), _, _) | + (_, _, Err(err)) => throw_str(&format!("{}", err)) + }; + + match OIDCClientData::new(issuer, client, redirect).await { + Ok(client_data) => client_data, + Err(err) => throw_str(&format!("{}", err)) } } } -impl ClientData { +impl OIDCClientData { - /// Create a new ClientData instance with the given values + /// Create a new OIDCClientData instance with the given values. + /// The relevant endpoints are discovered through the provided issuer url. /// /// # Arguments /// - /// * `auth_url` - The endpoint of the used authentication provider + /// * `issuer_url` - The The url of the used authentication provider /// * `client_id` - The at the authentication provider registered client id /// * `redirect_url`- The at the authentication provider registered redirection url /// + /// # Returns + /// `Ok(OIDCClientData)` - If the issuer url could be accessed + /// `Err(AuthErr)` - If the issuer url could not be accessed + /// /// # Example /// ```rust - /// let auth_url = AuthUrl::new(String::from("https://auth_provider.org/auth")); + /// let issuer = IssuerUrl::new(String::from("https://auth_provider.org/")); /// let client_id = ClientId::new(String::from("my-client-id")); /// let redirect_url = RedirectUrl::new(String::from("https://my.site")); - /// let client: ClientData = ClientData::new(auth_url, client_id, redirect_url); + /// let client: OIDCClientData = OIDCClientData::new(auth_url, client_id, redirect_url); /// ``` - pub fn new( - auth_url: AuthUrl, - token_url: TokenUrl, + pub async fn new( + issuer_url: IssuerUrl, client_id: ClientId, - redirect_url: RedirectUrl) -> Self { + redirect_url: RedirectUrl) -> Result { - ClientData { - auth_url, - token_url, + let metadata = match CoreProviderMetadata::discover_async(issuer_url, async_http_client).await { + Ok(metadata) => metadata, + Err(err) => return Err(AuthError::from(err.to_string())) + }; + + Ok(OIDCClientData { client_id, - redirect_url - } + redirect_url, + metadata + }) } /// Create the client represented by the data of this instance. /// Consumes this instance! /// /// # Returns - /// [`BasicClient`](oauth2::basic::BasicClient) + /// [`CoreClient`](openidconnect::core::CoreClient) /// /// # Example /// ```rust - /// let data = BasicClient::new(/** */) - /// let client: BasicClient = data.create(); + /// let data: OIDCClientData; // Provided elsewhere + /// let client: CoreClient = data.create(); /// // data cannot be used anymore! /// // do stuff with client /// ``` - pub fn create(self) -> BasicClient { + pub fn create(self) -> CoreClient { - BasicClient::new( + CoreClient::from_provider_metadata( + self.metadata, self.client_id, None, - self.auth_url, - Some(self.token_url) ).set_redirect_uri(self.redirect_url) } } diff --git a/src/controller/auth_manager/mod.rs b/src/controller/auth_manager/mod.rs index 97d88f7..2da46d9 100644 --- a/src/controller/auth_manager/mod.rs +++ b/src/controller/auth_manager/mod.rs @@ -9,35 +9,53 @@ mod pkce; pub use pkce::PKCE; mod client_data; -pub use client_data::ClientData; +pub use client_data::OIDCClientData; mod auth_error; pub use auth_error::AuthError; use wasm_bindgen::prelude::*; -use wasm_bindgen_test::console_log; use web_sys::Storage; -use oauth2::{ +use openidconnect::{ PkceCodeChallenge, CsrfToken, + Nonce, AuthorizationCode, - StandardTokenResponse, - EmptyExtraTokenFields, - TokenResponse + TokenResponse, + IdToken, + IdTokenClaims, + AccessToken, + RefreshToken, + EmptyAdditionalClaims, + AccessTokenHash, + OAuth2TokenResponse }; -use oauth2::basic::{ - BasicClient, - BasicTokenType +use openidconnect::core::{ + CoreAuthenticationFlow, + CoreClient, + CoreGenderClaim, + CoreJweContentEncryptionAlgorithm, + CoreJwsSigningAlgorithm, + CoreJsonWebKeyType, }; -use oauth2::url::Url; -use oauth2::reqwest::async_http_client; +use openidconnect::url::Url; +use openidconnect::reqwest::async_http_client; use std::collections::HashMap; pub struct AuthManager { pkce: Option, - client: BasicClient, - tokens: Option> + client: Option, + id_token: Option>, + access_token: Option, + refresh_token: Option, + claims: Option> } impl AuthManager { @@ -49,14 +67,17 @@ impl AuthManager { /// /// # Example /// ```rust - /// let client_data = ClientData::new(/** */); + /// let client_data: OIDCClientData // Already elsewhere provided; /// let auth: AuthManager = AuthManager::new(client); /// ``` - pub fn new(client_data: ClientData) -> Self { + pub fn new(client_data: OIDCClientData) -> Self { AuthManager { pkce: None, - client: client_data.create(), - tokens: None + client: Some(client_data.create()), + id_token: None, + access_token: None, + refresh_token: None, + claims: None } } @@ -152,17 +173,19 @@ impl AuthManager { let (challenge, verifier) = PkceCodeChallenge::new_random_sha256(); // Generate the full authorization URL and the csrf token - let (redirect, csrf) = self.client - .authorize_url(CsrfToken::new_random) - // Set the desired scopes. - // .add_scope(Scope::new("read".to_string())) - // .add_scope(Scope::new("write".to_string())) - // Set the PKCE code challenge. - .set_pkce_challenge(challenge) - .url(); - - // Store the verifier and the csrf token to verify server response - self.pkce = Some(PKCE::new(verifier, csrf)); + let (redirect, csrf, nonce) = match &self.client { + Some(client) => client.authorize_url( + CoreAuthenticationFlow::AuthorizationCode, + CsrfToken::new_random, + Nonce::new_random + ) + .set_pkce_challenge(challenge) + .url(), + None => return Err(JsValue::from(AuthError::from("No client was setup!"))) + }; + + // Store the verifier, csrf token and nonce to verify server response + self.pkce = Some(PKCE::new(verifier, csrf, nonce)); self.store(storage)?; Ok(redirect) @@ -170,6 +193,7 @@ impl AuthManager { /// Exchange the given authorization code for the tokens at the authentication provider. /// Check for security issues (Cross-Site Request Forgery) by providing the state answer. + /// After this method returned successfully, the user was successfully authorized and the claims are available. /// /// # Params /// @@ -184,10 +208,10 @@ impl AuthManager { /// /// # Example /// ```rust - /// let auth = AuthManager::new(/** */); - /// let redirect = auth.init_authentication(/** */); + /// let auth: AuthManager; // Already elsewhere provided + /// let redirect: Url; // Already elsewhere provided /// let storage: Storage; // already provided elsewhere - /// /* Authenticate and retreive code and state */ + /// // Authenticate and retreive code and state /// let (auth, result) = auth.exchange_token(code, state, Some(&storage)); /// if let Err(err) = result { /// // Handle Error @@ -201,6 +225,7 @@ impl AuthManager { storage: Option<&Storage> ) -> (Self, Result<(), AuthError>) { + // If no pkce data is present, try to load the data from the provided storage if let None = self.pkce { if let Some(store) = storage { if let Err(_) = self.load(&store) { @@ -216,10 +241,22 @@ impl AuthManager { ); } } + + let client = match self.client { + Some(client) => { + self.client = None; + client + }, + None => return ( + self, + Err(AuthError::from("No client was setup to authenticate!")) + ) + }; - let (verifier, csrf) = self.pkce.unwrap().destructure(); + let (verifier, csrf, nonce) = self.pkce.unwrap().destructure(); self.pkce = None; + // check for csrf errors if csrf.secret() != state.secret() { return ( self, @@ -228,14 +265,16 @@ impl AuthManager { ) ); } - let token_result = self.client + + // retrieve the tokens + let token_result = client .exchange_code(code) .set_pkce_verifier(verifier) .request_async(async_http_client) .await; - self.tokens = match token_result { - Ok(tokens) => Some(tokens), + let tokens = match token_result { + Ok(tokens) => tokens, Err(err) => { return ( self, @@ -244,9 +283,62 @@ impl AuthManager { } }; - console_log!("{:?}", self.tokens); - print!("{:?}", self.tokens); - + // Extract the id_token and the claims containing the user information + let id_token = match tokens.id_token() { + Some(id) => id, + None => return ( + self, + Err(AuthError::from("The server did not respond with an id token!")) + ) + }; + + let claims = match id_token.claims(&client.id_token_verifier(), &nonce) { + Ok(claims) => claims, + Err(err) => return ( + self, + Err(AuthError::from(err.to_string())) + ) + }; + + // Check if the access_token got tampered and signed differently + // and throw errors if anything does not line up. + if let Some(expected_hash_algorithm) = claims.access_token_hash() { + + let siging_alg = match id_token.signing_alg() { + Ok(alg) => alg, + Err(err) => return ( + self, + Err(AuthError::from(err.to_string())) + ) + }; + let actual_hash_algorithm = match + AccessTokenHash::from_token( + tokens.access_token(), + &siging_alg + ) { + Ok(alg) => alg, + Err(err) => return ( + self, + Err(AuthError::from(err.to_string())) + ) + }; + if *expected_hash_algorithm != actual_hash_algorithm { + return ( + self, + Err(AuthError::from("The used and expected hash algorithms are not the same!")) + ) + } + } + + // save the extracted id, access and refresh tokens as well as the claims for later usage + self.id_token = Some(id_token.clone()); + self.access_token = Some(tokens.access_token().clone()); + self.refresh_token = match tokens.refresh_token() { + Some(refresh) => Some(refresh.clone()), + None => None + }; + self.claims = Some(claims.clone()); + (self, Ok(())) } @@ -298,20 +390,6 @@ impl AuthManager { Ok((auth_code, state)) } - // TODO: Remove this function since it is disabling any security regarding the access token - // Debugging only! - // - // pub fn access_token(&self) -> Result<&String, AuthError> { - // match &self.tokens { - // Some(tokens) => { - // Ok(tokens.access_token().secret()) - // } - // None => { - // Err(AuthError::new(String::from("No access token available!"))) - // } - // } - // } - } // ********************** Unit Tests ************************* diff --git a/src/controller/auth_manager/pkce.rs b/src/controller/auth_manager/pkce.rs index 3f9bb27..c49afd0 100644 --- a/src/controller/auth_manager/pkce.rs +++ b/src/controller/auth_manager/pkce.rs @@ -5,9 +5,10 @@ use wasm_bindgen::prelude::*; use web_sys::Storage; -use oauth2::{ +use openidconnect::{ CsrfToken, - PkceCodeVerifier + PkceCodeVerifier, + Nonce }; use super::AuthError; @@ -20,26 +21,37 @@ pub struct PKCE { verifier: PkceCodeVerifier, /// The csrf token involved in the authentication process - csrf: CsrfToken + csrf: CsrfToken, + + /// The nonce involved to verify the response of the authentication process + nonce: Nonce } impl PKCE { const ID_VERIFIER: &'static str = "verifier"; const ID_CSRF: &'static str = "csrf"; + const ID_NONCE: &'static str = "nonce"; } impl PKCE { - /// Create a new pkce instance with default values + /// Create a new pkce instance with the given pkce verifier, csrf token and nonce + /// + /// # Arguments + /// + /// * `verifier` - The [`PkceCodeVerifier`](PkceCodeVerifier) used to verify the response + /// * `csrf` - The [`CsrfToken`](CsrfToken) used to validate CSRF + /// * `nonce` - The created [`Nonce`](Nonce) /// /// # Example /// ```rust /// let pkce: PKCE = PKCE::new() /// ``` - pub fn new(verifier: PkceCodeVerifier, csrf: CsrfToken) -> Self { + pub fn new(verifier: PkceCodeVerifier, csrf: CsrfToken, nonce: Nonce) -> Self { PKCE { verifier, - csrf + csrf, + nonce } } @@ -68,6 +80,7 @@ impl PKCE { storage.set(PKCE::ID_VERIFIER, &self.verifier.secret())?; storage.set(PKCE::ID_CSRF, &self.csrf.secret())?; + storage.set(PKCE::ID_NONCE, &self.nonce.secret())?; Ok(()) } @@ -96,17 +109,22 @@ impl PKCE { /// ``` pub fn load_from(storage: &Storage) -> Result { - let (verifier, csrf) = match ( + let (verifier, csrf, nonce) = match ( storage.get(PKCE::ID_VERIFIER), - storage.get(PKCE::ID_CSRF) + storage.get(PKCE::ID_CSRF), + storage.get(PKCE::ID_NONCE) ) { - (Ok(Some(verifier)), Ok(Some(csrf))) => { - (PkceCodeVerifier::new(verifier), CsrfToken::new(csrf)) + (Ok(Some(verifier)), Ok(Some(csrf)), Ok(Some(nonce))) => { + (PkceCodeVerifier::new(verifier), CsrfToken::new(csrf), Nonce::new(nonce)) }, - (Ok(None), _) | (_, Ok(None)) => return Err(JsValue::from(AuthError::from("No authentication data in storage found!"))), - (Err(e), _) | (_, Err(e)) => return Err(e) + (Ok(None), _, _) | + (_, Ok(None), _) | + (_, _, Ok(None)) => return Err(JsValue::from(AuthError::from("No authentication data in storage found!"))), + (Err(e), _, _) | + (_, Err(e), _) | + (_, _, Err(e)) => return Err(e) }; - Ok(PKCE::new(verifier, csrf)) + Ok(PKCE::new(verifier, csrf, nonce)) } /// Destructure this pkce data into its components to use. @@ -114,20 +132,20 @@ impl PKCE { /// /// # Returns /// - /// * `(PkceCodeVerifier, CsrfToken)` - The used verifier and csrf token. + /// * `(PkceCodeVerifier, CsrfToken, Nonce)` - The used verifier, csrf token and nonce. /// /// # Example /// ```rust - /// let pkce = PKCE::new(verifier, csrf); + /// let pkce = PKCE::new(verifier, csrf, nonce); /// - /// // Cannot use verifier and csrf here due to move + /// // Cannot use verifier, csrf, nonce here due to move /// - /// let (verifier, csrf) = pkce.destructure; + /// let (verifier, csrf, nonce) = pkce.destructure; /// - /// // Can use verifer and csrf here, but not pkce anymore + /// // Can use verifer, csrf, nonce here, but not pkce anymore /// ``` - pub fn destructure(self) -> (PkceCodeVerifier, CsrfToken) { - (self.verifier, self.csrf) + pub fn destructure(self) -> (PkceCodeVerifier, CsrfToken, Nonce) { + (self.verifier, self.csrf, self.nonce) } } diff --git a/src/controller/framework/mod.rs b/src/controller/framework/mod.rs index ce071bb..30a14fe 100644 --- a/src/controller/framework/mod.rs +++ b/src/controller/framework/mod.rs @@ -8,10 +8,10 @@ use wasm_bindgen::throw_str; use web_sys::Storage; use super::AuthManager; use super::auth_manager::{ - ClientData, + OIDCClientData, }; -use oauth2::url::Url; +use openidconnect::url::Url; #[wasm_bindgen] pub struct Framework { @@ -27,7 +27,7 @@ impl Framework { /// /// # Arguments /// - /// * `client_data` - See [`ClientData`](ClientData) + /// * `client_data` - See [`OIDCClientData`](OIDCClientData) /// * `storage` - A [`Storage`](Storage) /// /// # Returns @@ -36,12 +36,12 @@ impl Framework { /// /// # Example /// ```rust - /// let client_data = ClientData::from(/* */); - /// let storage: Storage = /* */; + /// let client_data: OIDCClientData; // Already elsewhere provided + /// let storage: Storage; // Already elsewhere provided /// let framework: Framework = Framework::new(client_data, storage); /// ``` pub fn new( - client_data: ClientData, + client_data: OIDCClientData, storage: Storage ) -> Framework { Framework { @@ -68,7 +68,7 @@ impl Framework { match self.auth.init_authentication(&self.session) { Ok(url) => url.to_string(), - Err(err) => throw_str(format!("{:?}", err)) + Err(err) => throw_str(&format!("{:?}", err)) } }