Compare commits
4 Commits
51e52e5ed7
...
88e34902c0
Author | SHA1 | Date | |
---|---|---|---|
88e34902c0 | |||
5633aae029 | |||
b10215ae9c | |||
c4bc559b4d |
@ -1,2 +1,3 @@
|
||||
pub mod users_actor;
|
||||
pub mod bruteforce_actor;
|
||||
pub mod bruteforce_actor;
|
||||
pub mod openid_sessions_actor;
|
68
src/actors/openid_sessions_actor.rs
Normal file
68
src/actors/openid_sessions_actor.rs
Normal file
@ -0,0 +1,68 @@
|
||||
use actix::{Actor, AsyncContext, Context, Handler};
|
||||
use actix::Message;
|
||||
|
||||
use crate::constants::*;
|
||||
use crate::data::client::ClientID;
|
||||
use crate::data::user::UserID;
|
||||
use crate::utils::time::time;
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq)]
|
||||
pub struct SessionID(pub String);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Session {
|
||||
pub session_id: SessionID,
|
||||
pub client: ClientID,
|
||||
pub user: UserID,
|
||||
pub redirect_uri: String,
|
||||
|
||||
pub authorization_code: String,
|
||||
pub code_expire_on: u64,
|
||||
|
||||
pub token: String,
|
||||
pub token_expire_at: u64,
|
||||
|
||||
pub nonce: Option<String>,
|
||||
pub code_challenge: Option<(String, String)>,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub fn is_expired(&self) -> bool {
|
||||
self.code_expire_on < time() && self.token_expire_at < time()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "()")]
|
||||
pub struct PushNewSession(pub Session);
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct OpenIDSessionsActor {
|
||||
session: Vec<Session>,
|
||||
}
|
||||
|
||||
impl OpenIDSessionsActor {
|
||||
pub fn clean_old_sessions(&mut self) {
|
||||
self.session.retain(|s| !s.is_expired());
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for OpenIDSessionsActor {
|
||||
type Context = Context<Self>;
|
||||
|
||||
fn started(&mut self, ctx: &mut Self::Context) {
|
||||
// Clean up at a regular interval failed attempts
|
||||
ctx.run_interval(OPEN_ID_SESSION_CLEANUP_INTERVAL, |act, _ctx| {
|
||||
log::trace!("Cleaning up old login sessions");
|
||||
act.clean_old_sessions();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<PushNewSession> for OpenIDSessionsActor {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: PushNewSession, _ctx: &mut Self::Context) -> Self::Result {
|
||||
self.session.push(msg.0)
|
||||
}
|
||||
}
|
@ -40,4 +40,15 @@ pub const MAX_FAILED_LOGIN_ATTEMPTS: usize = 15;
|
||||
pub const FAIL_LOGIN_ATTEMPT_CLEANUP_INTERVAL: Duration = Duration::from_secs(60);
|
||||
|
||||
/// Temporary password length
|
||||
pub const TEMPORARY_PASSWORDS_LEN: usize = 20;
|
||||
pub const TEMPORARY_PASSWORDS_LEN: usize = 20;
|
||||
|
||||
/// Open ID routes
|
||||
pub const AUTHORIZE_URI: &str = "/openid/authorize";
|
||||
|
||||
/// Open ID constants
|
||||
pub const OPEN_ID_SESSION_CLEANUP_INTERVAL: Duration = Duration::from_secs(60);
|
||||
pub const OPEN_ID_SESSION_LEN: usize = 40;
|
||||
pub const OPEN_ID_AUTHORIZATION_CODE_LEN: usize = 120;
|
||||
pub const OPEN_ID_AUTHORIZATION_CODE_TIMEOUT: u64 = 300;
|
||||
pub const OPEN_ID_TOKEN_LEN: usize = 120;
|
||||
pub const OPEN_ID_TOKEN_TIMEOUT: u64 = 3600;
|
@ -1,12 +1,22 @@
|
||||
use actix::Addr;
|
||||
use actix_web::{HttpResponse, Responder, web};
|
||||
use askama::Template;
|
||||
|
||||
use crate::actors::openid_sessions_actor;
|
||||
use crate::actors::openid_sessions_actor::{OpenIDSessionsActor, Session, SessionID};
|
||||
use crate::constants::{AUTHORIZE_URI, OPEN_ID_AUTHORIZATION_CODE_LEN, OPEN_ID_AUTHORIZATION_CODE_TIMEOUT, OPEN_ID_SESSION_LEN, OPEN_ID_TOKEN_LEN, OPEN_ID_TOKEN_TIMEOUT};
|
||||
use crate::controllers::base_controller::FatalErrorPage;
|
||||
use crate::data::app_config::AppConfig;
|
||||
use crate::data::client::{ClientID, ClientManager};
|
||||
use crate::data::current_user::CurrentUser;
|
||||
use crate::data::openid_config::OpenIDConfig;
|
||||
use crate::utils::string_utils::rand_str;
|
||||
use crate::utils::time::time;
|
||||
|
||||
pub async fn get_configuration(app_conf: web::Data<AppConfig>) -> impl Responder {
|
||||
HttpResponse::Ok().json(OpenIDConfig {
|
||||
issuer: app_conf.full_url("/"),
|
||||
authorization_endpoint: app_conf.full_url("openid/authorize"),
|
||||
authorization_endpoint: app_conf.full_url(AUTHORIZE_URI),
|
||||
token_endpoint: app_conf.full_url("openid/token"),
|
||||
userinfo_endpoint: app_conf.full_url("openid/userinfo"),
|
||||
jwks_uri: app_conf.full_url("openid/jwks_uri"),
|
||||
@ -14,6 +24,120 @@ pub async fn get_configuration(app_conf: web::Data<AppConfig>) -> impl Responder
|
||||
response_types_supported: vec!["code", "id_token", "token id_token"],
|
||||
subject_types_supported: vec!["public"],
|
||||
id_token_signing_alg_values_supported: vec!["RS256"],
|
||||
claims_supported: vec!["sub", "exp", "name", "given_name", "family_name", "email"]
|
||||
claims_supported: vec!["sub", "exp", "name", "given_name", "family_name", "email"],
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
pub struct AuthorizeQuery {
|
||||
/// REQUIRED. OpenID Connect requests MUST contain the openid scope value. If the openid scope value is not present, the behavior is entirely unspecified. Other scope values MAY be present. Scope values used that are not understood by an implementation SHOULD be ignored. See Sections 5.4 and 11 for additional scope values defined by this specification.
|
||||
scope: String,
|
||||
|
||||
/// REQUIRED. OAuth 2.0 Response Type value that determines the authorization processing flow to be used, including what parameters are returned from the endpoints used. When using the Authorization Code Flow, this value is code.
|
||||
response_type: String,
|
||||
|
||||
/// REQUIRED. OAuth 2.0 Client Identifier valid at the Authorization Server.
|
||||
client_id: ClientID,
|
||||
|
||||
/// REQUIRED. Redirection URI to which the response will be sent. This URI MUST exactly match one of the Redirection URI values for the Client pre-registered at the OpenID Provider, with the matching performed as described in Section 6.2.1 of [RFC3986] (Simple String Comparison). When using this flow, the Redirection URI SHOULD use the https scheme; however, it MAY use the http scheme, provided that the Client Type is confidential, as defined in Section 2.1 of OAuth 2.0, and provided the OP allows the use of http Redirection URIs in this case. The Redirection URI MAY use an alternate scheme, such as one that is intended to identify a callback into a native application.
|
||||
redirect_uri: String,
|
||||
|
||||
/// RECOMMENDED. Opaque value used to maintain state between the request and the callback. Typically, Cross-Site Request Forgery (CSRF, XSRF) mitigation is done by cryptographically binding the value of this parameter with a browser cookie.
|
||||
state: String,
|
||||
|
||||
/// OPTIONAL. String value used to associate a Client session with an ID Token, and to mitigate replay attacks. The value is passed through unmodified from the Authentication Request to the ID Token. Sufficient entropy MUST be present in the nonce values used to prevent attackers from guessing values.
|
||||
nonce: Option<String>,
|
||||
|
||||
/// OPTIONAL - https://ldapwiki.com/wiki/Code_challenge_method
|
||||
code_challenge: Option<String>,
|
||||
code_challenge_method: Option<String>,
|
||||
}
|
||||
|
||||
fn error_redirect(query: &AuthorizeQuery, error: &str, description: &str) -> HttpResponse {
|
||||
log::warn!("Failed to process sign in request ({} => {}): {:?}", error, description, query);
|
||||
HttpResponse::Found()
|
||||
.append_header(
|
||||
("Location", format!(
|
||||
"{}?error={}?error_description={}&state={}",
|
||||
query.redirect_uri,
|
||||
urlencoding::encode(error),
|
||||
urlencoding::encode(description),
|
||||
urlencoding::encode(&query.state)
|
||||
))
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
|
||||
pub async fn authorize(user: CurrentUser, query: web::Query<AuthorizeQuery>,
|
||||
clients: web::Data<ClientManager>,
|
||||
sessions: web::Data<Addr<OpenIDSessionsActor>>) -> impl Responder {
|
||||
let client = match clients.find_by_id(&query.client_id) {
|
||||
None => {
|
||||
return HttpResponse::BadRequest().body(FatalErrorPage {
|
||||
message: "Client is invalid!"
|
||||
}.render().unwrap());
|
||||
}
|
||||
Some(c) => c
|
||||
};
|
||||
|
||||
let redirect_uri = query.redirect_uri.trim().to_string();
|
||||
if client.redirect_uri != redirect_uri {
|
||||
return HttpResponse::BadRequest().body(FatalErrorPage {
|
||||
message: "Redirect URI is invalid!"
|
||||
}.render().unwrap());
|
||||
}
|
||||
|
||||
if !query.scope.split(' ').any(|x| x == "openid") {
|
||||
return error_redirect(&query, "invalid_request", "openid scope missing!");
|
||||
}
|
||||
|
||||
if !query.response_type.eq("code") {
|
||||
return error_redirect(&query, "invalid_request", "Only code response type is supported!");
|
||||
}
|
||||
|
||||
if query.state.is_empty() {
|
||||
return error_redirect(&query, "invalid_request", "State is empty!");
|
||||
}
|
||||
|
||||
let code_challenge = match (query.0.code_challenge.clone(), query.0.code_challenge_method.clone()) {
|
||||
(Some(chal), Some(meth)) => {
|
||||
if !meth.eq("S256") {
|
||||
return error_redirect(&query, "invalid_request",
|
||||
"Only S256 code challenge is supported!");
|
||||
}
|
||||
Some((chal, meth))
|
||||
}
|
||||
(_, _) => None
|
||||
};
|
||||
|
||||
// Check if user is authorized to access the application
|
||||
if !user.can_access_app(&client.id) {
|
||||
return error_redirect(&query, "invalid_request",
|
||||
"User is not authorized to access this application!");
|
||||
}
|
||||
|
||||
// Save all authentication information in memory
|
||||
let session = Session {
|
||||
session_id: SessionID(rand_str(OPEN_ID_SESSION_LEN)),
|
||||
client: client.id,
|
||||
user: user.uid.clone(),
|
||||
redirect_uri,
|
||||
authorization_code: rand_str(OPEN_ID_AUTHORIZATION_CODE_LEN),
|
||||
code_expire_on: time() + OPEN_ID_AUTHORIZATION_CODE_TIMEOUT,
|
||||
token: rand_str(OPEN_ID_TOKEN_LEN),
|
||||
token_expire_at: time() + OPEN_ID_TOKEN_TIMEOUT,
|
||||
nonce: query.0.nonce,
|
||||
code_challenge,
|
||||
};
|
||||
sessions.send(openid_sessions_actor::PushNewSession(session.clone())).await.unwrap();
|
||||
|
||||
log::trace!("New OpenID session: {:#?}", session);
|
||||
|
||||
HttpResponse::Found()
|
||||
.append_header(("Location", format!(
|
||||
"{}?state={}&session_sate=&code={}",
|
||||
session.redirect_uri,
|
||||
urlencoding::encode(&query.0.state),
|
||||
urlencoding::encode(&session.authorization_code)
|
||||
))).finish()
|
||||
}
|
@ -8,6 +8,8 @@ pub struct Client {
|
||||
pub id: ClientID,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub secret: String,
|
||||
pub redirect_uri: String,
|
||||
}
|
||||
|
||||
impl PartialEq for Client {
|
||||
|
10
src/main.rs
10
src/main.rs
@ -7,6 +7,7 @@ use actix_web::middleware::Logger;
|
||||
use clap::Parser;
|
||||
|
||||
use basic_oidc::actors::bruteforce_actor::BruteForceActor;
|
||||
use basic_oidc::actors::openid_sessions_actor::OpenIDSessionsActor;
|
||||
use basic_oidc::actors::users_actor::UsersActor;
|
||||
use basic_oidc::constants::*;
|
||||
use basic_oidc::controllers::*;
|
||||
@ -64,6 +65,7 @@ async fn main() -> std::io::Result<()> {
|
||||
|
||||
let users_actor = UsersActor::new(users).start();
|
||||
let bruteforce_actor = BruteForceActor::default().start();
|
||||
let openid_sessions_actor = OpenIDSessionsActor::default().start();
|
||||
|
||||
log::info!("Server will listen on {}", config.listen_address);
|
||||
let listen_address = config.listen_address.to_string();
|
||||
@ -77,11 +79,12 @@ async fn main() -> std::io::Result<()> {
|
||||
.secure(config.secure_cookie())
|
||||
.visit_deadline(Duration::seconds(MAX_INACTIVITY_DURATION))
|
||||
.login_deadline(Duration::seconds(MAX_SESSION_DURATION))
|
||||
.same_site(SameSite::Strict);
|
||||
.same_site(SameSite::Lax);
|
||||
|
||||
App::new()
|
||||
.app_data(web::Data::new(users_actor.clone()))
|
||||
.app_data(web::Data::new(bruteforce_actor.clone()))
|
||||
.app_data(web::Data::new(openid_sessions_actor.clone()))
|
||||
.app_data(web::Data::new(config.clone()))
|
||||
.app_data(web::Data::new(clients))
|
||||
|
||||
@ -124,8 +127,9 @@ async fn main() -> std::io::Result<()> {
|
||||
.route("/admin/api/find_username", web::post().to(admin_api::find_username))
|
||||
.route("/admin/api/delete_user", web::post().to(admin_api::delete_user))
|
||||
|
||||
// OpenID specs
|
||||
.route(".well-known/openid-configuration", web::get().to(openid_controller::get_configuration))
|
||||
// OpenID routes
|
||||
.route("/.well-known/openid-configuration", web::get().to(openid_controller::get_configuration))
|
||||
.route("/openid/authorize", web::get().to(openid_controller::authorize))
|
||||
})
|
||||
.bind(listen_address)?
|
||||
.run()
|
||||
|
@ -13,7 +13,7 @@ use actix_web::body::EitherBody;
|
||||
use actix_web::http::{header, Method};
|
||||
use askama::Template;
|
||||
|
||||
use crate::constants::{ADMIN_ROUTES, AUTHENTICATED_ROUTES};
|
||||
use crate::constants::{ADMIN_ROUTES, AUTHENTICATED_ROUTES, AUTHORIZE_URI};
|
||||
use crate::controllers::base_controller::{FatalErrorPage, redirect_user_for_login};
|
||||
use crate::data::app_config::AppConfig;
|
||||
use crate::data::session_identity::{SessionIdentity, SessionIdentityData, SessionStatus};
|
||||
@ -131,9 +131,9 @@ impl<S, B> Service<ServiceRequest> for AuthInnerMiddleware<S>
|
||||
// Redirect user to login page
|
||||
if !session.is_auth()
|
||||
&& (req.path().starts_with(ADMIN_ROUTES)
|
||||
|| req.path().starts_with(AUTHENTICATED_ROUTES))
|
||||
|| req.path().starts_with(AUTHENTICATED_ROUTES) || req.path().eq(AUTHORIZE_URI))
|
||||
{
|
||||
let path = req.path().to_string();
|
||||
let path = req.uri().to_string();
|
||||
return Ok(req
|
||||
.into_response(redirect_user_for_login(path))
|
||||
.map_into_right_body());
|
||||
|
Loading…
x
Reference in New Issue
Block a user