Compare commits

...

2 Commits

Author SHA1 Message Date
b26e283f7d Handle errors cases when retrieving login token & rate limiting
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-26 14:11:44 +02:00
bee794a589 Retrieve access token from provider 2023-04-26 12:22:22 +02:00
7 changed files with 157 additions and 26 deletions

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

@@ -4,9 +4,10 @@ use actix::Addr;
use actix_web::{web, HttpResponse, Responder}; use actix_web::{web, HttpResponse, Responder};
use askama::Template; use askama::Template;
use crate::actors::providers_states_actor; use crate::actors::bruteforce_actor::BruteForceActor;
use crate::actors::providers_states_actor::{ProviderLoginState, ProvidersStatesActor}; use crate::actors::providers_states_actor::{ProviderLoginState, ProvidersStatesActor};
use crate::constants::APP_NAME; use crate::actors::{bruteforce_actor, providers_states_actor};
use crate::constants::{APP_NAME, MAX_FAILED_LOGIN_ATTEMPTS};
use crate::controllers::base_controller::{build_fatal_error_page, redirect_user}; use crate::controllers::base_controller::{build_fatal_error_page, redirect_user};
use crate::controllers::login_controller::BaseLoginPage; use crate::controllers::login_controller::BaseLoginPage;
use crate::data::action_logger::{Action, ActionLogger}; use crate::data::action_logger::{Action, ActionLogger};
@@ -127,6 +128,7 @@ pub async fn finish_login(
remote_ip: RemoteIP, remote_ip: RemoteIP,
providers: web::Data<Arc<ProvidersManager>>, providers: web::Data<Arc<ProvidersManager>>,
states: web::Data<Addr<ProvidersStatesActor>>, states: web::Data<Addr<ProvidersStatesActor>>,
bruteforce: web::Data<Addr<BruteForceActor>>,
query: web::Query<FinishLoginQuery>, query: web::Query<FinishLoginQuery>,
logger: ActionLogger, logger: ActionLogger,
) -> impl Responder { ) -> impl Responder {
@@ -167,8 +169,64 @@ pub async fn finish_login(
} }
}; };
// TODO : rate limiting // We perform rate limiting before attempting to use authorization code
// TODO : finish login, get user information let failed_attempts = bruteforce
.send(bruteforce_actor::CountFailedAttempt {
ip: remote_ip.into(),
})
.await
.unwrap();
if failed_attempts > MAX_FAILED_LOGIN_ATTEMPTS {
logger.log(Action::ProviderRateLimited);
return HttpResponse::TooManyRequests().body(build_fatal_error_page(
"Too many failed login attempts, please try again later!",
));
}
// 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!",
));
}
};
// Get access token & user information
let token = provider_config.get_token(&provider, &query.code).await;
let token = match token {
Ok(t) => t,
Err(e) => {
log::error!("Failed to retrieve login token! {:?}", e);
bruteforce
.send(bruteforce_actor::RecordFailedAttempt {
ip: remote_ip.into(),
})
.await
.unwrap();
logger.log(Action::ProviderFailedGetToken {
state: &state,
code: query.code.as_str(),
});
return ProviderLoginError::get(
"Failed to retrieve login token from identity provider!",
&state.redirect,
);
}
};
println!("go on {:?}", 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

@@ -7,6 +7,7 @@ use actix_identity::Identity;
use actix_web::dev::Payload; use actix_web::dev::Payload;
use actix_web::{web, Error, FromRequest, HttpRequest}; use actix_web::{web, Error, FromRequest, HttpRequest};
use crate::actors::providers_states_actor::ProviderLoginState;
use crate::actors::users_actor; use crate::actors::users_actor;
use crate::actors::users_actor::{AuthorizedAuthenticationSources, UsersActor}; use crate::actors::users_actor::{AuthorizedAuthenticationSources, UsersActor};
use crate::data::client::Client; use crate::data::client::Client;
@@ -38,6 +39,11 @@ pub enum Action<'a> {
ProviderCBInvalidState { ProviderCBInvalidState {
state: &'a str, state: &'a str,
}, },
ProviderRateLimited,
ProviderFailedGetToken {
state: &'a ProviderLoginState,
code: &'a str,
},
Signout, Signout,
UserNeed2FAOnLogin(&'a User), UserNeed2FAOnLogin(&'a User),
UserSuccessfullyAuthenticated(&'a User), UserSuccessfullyAuthenticated(&'a User),
@@ -108,6 +114,8 @@ impl<'a> Action<'a> {
format!("failed provider authentication with message '{message}'"), format!("failed provider authentication with message '{message}'"),
Action::ProviderCBInvalidState { state } => Action::ProviderCBInvalidState { state } =>
format!("provided invalid callback state after provider authentication: '{state}'"), format!("provided invalid callback state after provider authentication: '{state}'"),
Action::ProviderRateLimited => "could not complete OpenID login because it has reached failed attempts rate limit!".to_string(),
Action::ProviderFailedGetToken {state, code} => format!("could not complete login from provider because the id_token could not be retrieved! (state={:?} code = {code})",state),
Action::Signout => "signed out".to_string(), Action::Signout => "signed out".to_string(),
Action::UserNeed2FAOnLogin(user) => { Action::UserNeed2FAOnLogin(user) => {
format!( format!(

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! {