Add authentication from upstream providers #107

Merged
pierre merged 25 commits from feat-upstream-providers into master 2023-04-27 10:10:29 +00:00
6 changed files with 107 additions and 23 deletions
Showing only changes of commit bee794a589 - Show all commits

View File

@ -21,7 +21,7 @@ use crate::data::current_user::CurrentUser;
use crate::data::id_token::IdToken; use crate::data::id_token::IdToken;
use crate::data::jwt_signer::{JWTSigner, JsonWebKey}; use crate::data::jwt_signer::{JWTSigner, JsonWebKey};
use crate::data::open_id_user_info::OpenIDUserInfo; 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::session_identity::SessionIdentity;
use crate::data::user::User; use crate::data::user::User;
use crate::utils::string_utils::rand_str; use crate::utils::string_utils::rand_str;
@ -255,16 +255,6 @@ pub struct TokenQuery {
refresh_token_query: Option<TokenRefreshTokenQuery>, refresh_token_query: Option<TokenRefreshTokenQuery>,
} }
#[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<String>,
}
pub async fn token( pub async fn token(
req: HttpRequest, req: HttpRequest,
query: web::Form<TokenQuery>, query: web::Form<TokenQuery>,
@ -451,9 +441,9 @@ pub async fn token(
TokenResponse { TokenResponse {
access_token: session.access_token.expect("Missing access token!"), access_token: session.access_token.expect("Missing access token!"),
token_type: "Bearer", token_type: "Bearer".to_string(),
refresh_token: session.refresh_token, refresh_token: Some(session.refresh_token),
expires_in: session.access_token_expire_at - time(), expires_in: Some(session.access_token_expire_at - time()),
id_token: Some(jwt_signer.sign_token(id_token.to_jwt_claims())?), id_token: Some(jwt_signer.sign_token(id_token.to_jwt_claims())?),
} }
} }
@ -501,9 +491,9 @@ pub async fn token(
TokenResponse { TokenResponse {
access_token: session.access_token.expect("Missing access token!"), access_token: session.access_token.expect("Missing access token!"),
token_type: "Bearer", token_type: "Bearer".to_string(),
refresh_token: session.refresh_token, refresh_token: Some(session.refresh_token),
expires_in: session.access_token_expire_at - time(), expires_in: Some(session.access_token_expire_at - time()),
id_token: None, id_token: None,
} }
} }

View File

@ -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 : 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 token signature
// TODO : check if user is authorized to access application // TODO : check if user is authorized to access application
// TODO : check if 2FA is enabled // TODO : check if 2FA is enabled

View File

@ -2,7 +2,9 @@ use std::path::{Path, PathBuf};
use clap::Parser; 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 /// Basic OIDC provider
#[derive(Parser, Debug, Clone)] #[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 { pub fn domain_name(&self) -> &str {
self.website_origin.split('/').nth(2).unwrap_or(APP_NAME) self.website_origin.split('/').nth(2).unwrap_or(APP_NAME)
} }

View File

@ -10,7 +10,7 @@ pub mod id_token;
pub mod jwt_signer; pub mod jwt_signer;
pub mod login_redirect; pub mod login_redirect;
pub mod open_id_user_info; pub mod open_id_user_info;
pub mod openid_config; pub mod openid_primitive;
pub mod provider; pub mod provider;
pub mod provider_configuration; pub mod provider_configuration;
pub mod remote_ip; pub mod remote_ip;

View File

@ -1,3 +1,6 @@
//! # OpenID primitives
/// OpenID discovery information
#[derive(Debug, Clone, serde::Serialize)] #[derive(Debug, Clone, serde::Serialize)]
pub struct OpenIDConfig { 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 /// 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>, 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<String>,
/// 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<u64>,
/// 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<String>,
}

View File

@ -1,10 +1,14 @@
use crate::actors::providers_states_actor::ProviderLoginState;
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::HashMap; 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::app_config::AppConfig;
use crate::data::jwt_signer::JsonWebKey; use crate::data::jwt_signer::JsonWebKey;
use crate::data::openid_primitive::TokenResponse;
use crate::data::provider::Provider; use crate::data::provider::Provider;
use crate::utils::err::Res; use crate::utils::err::Res;
use crate::utils::time::time; use crate::utils::time::time;
@ -38,10 +42,36 @@ impl ProviderConfiguration {
let authorization_url = &self.discovery.authorization_endpoint; let authorization_url = &self.discovery.authorization_endpoint;
let client_id = urlencoding::encode(&provider.client_id).to_string(); let client_id = urlencoding::encode(&provider.client_id).to_string();
let state = urlencoding::encode(&state.state_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}") 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<TokenResponse> {
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(&params)
.send()
.await?
.json()
.await?)
}
} }
thread_local! { thread_local! {