diff --git a/src/controllers/openid_controller.rs b/src/controllers/openid_controller.rs index 4c6c495..d1b5daf 100644 --- a/src/controllers/openid_controller.rs +++ b/src/controllers/openid_controller.rs @@ -21,7 +21,7 @@ use crate::data::current_user::CurrentUser; use crate::data::id_token::IdToken; use crate::data::jwt_signer::{JWTSigner, JsonWebKey}; use crate::data::open_id_user_info::OpenIDUserInfo; -use crate::data::openid_config::OpenIDConfig; +use crate::data::openid_primitive::{OpenIDConfig, TokenResponse}; use crate::data::session_identity::SessionIdentity; use crate::data::user::User; use crate::utils::string_utils::rand_str; @@ -255,16 +255,6 @@ pub struct TokenQuery { refresh_token_query: Option, } -#[derive(Debug, serde::Serialize)] -pub struct TokenResponse { - access_token: String, - token_type: &'static str, - refresh_token: String, - expires_in: u64, - #[serde(skip_serializing_if = "Option::is_none")] - id_token: Option, -} - pub async fn token( req: HttpRequest, query: web::Form, @@ -451,9 +441,9 @@ pub async fn token( TokenResponse { access_token: session.access_token.expect("Missing access token!"), - token_type: "Bearer", - refresh_token: session.refresh_token, - expires_in: session.access_token_expire_at - time(), + token_type: "Bearer".to_string(), + refresh_token: Some(session.refresh_token), + expires_in: Some(session.access_token_expire_at - time()), id_token: Some(jwt_signer.sign_token(id_token.to_jwt_claims())?), } } @@ -501,9 +491,9 @@ pub async fn token( TokenResponse { access_token: session.access_token.expect("Missing access token!"), - token_type: "Bearer", - refresh_token: session.refresh_token, - expires_in: session.access_token_expire_at - time(), + token_type: "Bearer".to_string(), + refresh_token: Some(session.refresh_token), + expires_in: Some(session.access_token_expire_at - time()), id_token: None, } } diff --git a/src/controllers/providers_controller.rs b/src/controllers/providers_controller.rs index 22ae6ed..32302b0 100644 --- a/src/controllers/providers_controller.rs +++ b/src/controllers/providers_controller.rs @@ -167,8 +167,27 @@ pub async fn finish_login( } }; + // Retrieve provider information & configuration + let provider = providers + .find_by_id(&state.provider_id) + .expect("Unable to retrieve provider information!"); + + let provider_config = match ProviderConfigurationHelper::get_configuration(&provider).await { + Ok(c) => c, + Err(e) => { + log::error!("Failed to load provider configuration! {}", e); + return HttpResponse::InternalServerError().body(build_fatal_error_page( + "Failed to load provider configuration!", + )); + } + }; + // TODO : rate limiting - // TODO : finish login, get user information + + // Get access token & user information + let token = provider_config.get_token(&provider, &query.code).await; + log::debug!("{:#?}", token); + // TODO : check token signature // TODO : check if user is authorized to access application // TODO : check if 2FA is enabled diff --git a/src/data/app_config.rs b/src/data/app_config.rs index 791a46a..a70309d 100644 --- a/src/data/app_config.rs +++ b/src/data/app_config.rs @@ -2,7 +2,9 @@ use std::path::{Path, PathBuf}; use clap::Parser; -use crate::constants::{APP_NAME, CLIENTS_LIST_FILE, PROVIDERS_LIST_FILE, USERS_LIST_FILE}; +use crate::constants::{ + APP_NAME, CLIENTS_LIST_FILE, OIDC_PROVIDER_CB_URI, PROVIDERS_LIST_FILE, USERS_LIST_FILE, +}; /// Basic OIDC provider #[derive(Parser, Debug, Clone)] @@ -84,6 +86,12 @@ impl AppConfig { } } + /// Get the URL where a upstream OpenID provider should redirect + /// the user after an authentication + pub fn oidc_provider_redirect_url(&self) -> String { + AppConfig::get().full_url(OIDC_PROVIDER_CB_URI) + } + pub fn domain_name(&self) -> &str { self.website_origin.split('/').nth(2).unwrap_or(APP_NAME) } diff --git a/src/data/mod.rs b/src/data/mod.rs index 2d3c913..c45df9a 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -10,7 +10,7 @@ pub mod id_token; pub mod jwt_signer; pub mod login_redirect; pub mod open_id_user_info; -pub mod openid_config; +pub mod openid_primitive; pub mod provider; pub mod provider_configuration; pub mod remote_ip; diff --git a/src/data/openid_config.rs b/src/data/openid_primitive.rs similarity index 69% rename from src/data/openid_config.rs rename to src/data/openid_primitive.rs index b3e6df0..158633c 100644 --- a/src/data/openid_config.rs +++ b/src/data/openid_primitive.rs @@ -1,3 +1,6 @@ +//! # OpenID primitives + +/// OpenID discovery information #[derive(Debug, Clone, serde::Serialize)] pub struct OpenIDConfig { /// URL using the https scheme with no query or fragment component that the OP asserts as its Issuer Identifier. If Issuer discovery is supported (see Section 2), this value MUST be identical to the issuer value returned by WebFinger. This also MUST be identical to the iss Claim value in ID Tokens issued from this Issuer @@ -35,3 +38,37 @@ pub struct OpenIDConfig { pub code_challenge_methods_supported: Vec<&'static str>, } + +/// OpenID token response +/// +/// The content of this field is specified in +/// * OAuth specifications: https://datatracker.ietf.org/doc/html/rfc6749#section-5.1 +/// * OpenID Core specifications: https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct TokenResponse { + /// REQUIRED. The access token issued by the authorization server. + pub access_token: String, + + /// REQUIRED. The type of the token issued. It MUST be "Bearer" + pub token_type: String, + + /// OPTIONAL. The refresh token, which can be used to obtain new + /// access tokens using the same authorization grant + #[serde(skip_serializing_if = "Option::is_none")] + pub refresh_token: Option, + + /// RECOMMENDED. The lifetime in seconds of the access token. For + /// example, the value "3600" denotes that the access token will + /// expire in one hour from the time the response was generated. + /// If omitted, the authorization server SHOULD provide the + /// expiration time via other means or document the default value. + #[serde(skip_serializing_if = "Option::is_none")] + pub expires_in: Option, + + /// REQUIRED. ID Token value associated with the authenticated session. + /// + /// Note: this field is marked as optionnal because it is excluded in case + /// of request of refresh token. + #[serde(skip_serializing_if = "Option::is_none")] + pub id_token: Option, +} diff --git a/src/data/provider_configuration.rs b/src/data/provider_configuration.rs index 2fe1f92..e0a5d95 100644 --- a/src/data/provider_configuration.rs +++ b/src/data/provider_configuration.rs @@ -1,10 +1,14 @@ -use crate::actors::providers_states_actor::ProviderLoginState; use std::cell::RefCell; use std::collections::HashMap; -use crate::constants::{OIDC_PROVIDERS_LIFETIME, OIDC_PROVIDER_CB_URI}; +use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; +use base64::Engine as _; + +use crate::actors::providers_states_actor::ProviderLoginState; +use crate::constants::OIDC_PROVIDERS_LIFETIME; use crate::data::app_config::AppConfig; use crate::data::jwt_signer::JsonWebKey; +use crate::data::openid_primitive::TokenResponse; use crate::data::provider::Provider; use crate::utils::err::Res; use crate::utils::time::time; @@ -38,10 +42,36 @@ impl ProviderConfiguration { let authorization_url = &self.discovery.authorization_endpoint; let client_id = urlencoding::encode(&provider.client_id).to_string(); let state = urlencoding::encode(&state.state_id).to_string(); - let callback_url = AppConfig::get().full_url(OIDC_PROVIDER_CB_URI); + let callback_url = AppConfig::get().oidc_provider_redirect_url(); format!("{authorization_url}?response_type=code&scope=openid%20profile%20email&client_id={client_id}&state={state}&redirect_uri={callback_url}") } + + /// Retrieve the authorization token after a successful authentication, using an authorization code + pub async fn get_token( + &self, + provider: &Provider, + authorization_code: &str, + ) -> Res { + let authorization = + BASE64_STANDARD.encode(format!("{}:{}", provider.client_id, provider.client_secret)); + + let redirect_url = AppConfig::get().oidc_provider_redirect_url(); + + let mut params = HashMap::new(); + params.insert("grant_type", "authorization_code"); + params.insert("code", authorization_code); + params.insert("redirect_uri", redirect_url.as_str()); + + Ok(reqwest::Client::new() + .post(&self.discovery.token_endpoint) + .header("Authorization", format!("Basic {authorization}")) + .form(¶ms) + .send() + .await? + .json() + .await?) + } } thread_local! {