Compare commits

...

5 Commits

7 changed files with 96 additions and 49 deletions

View File

@ -2,9 +2,14 @@ use actix::{Actor, AsyncContext, Context, Handler};
use actix::Message;
use crate::constants::*;
use crate::data::access_token::AccessToken;
use crate::data::app_config::AppConfig;
use crate::data::client::ClientID;
use crate::data::code_challenge::CodeChallenge;
use crate::data::jwt_signer::JWTSigner;
use crate::data::user::UserID;
use crate::utils::err::Res;
use crate::utils::string_utils::rand_str;
use crate::utils::time::time;
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq)]
@ -20,9 +25,8 @@ pub struct Session {
pub authorization_code: String,
pub authorization_code_expire_at: u64,
pub authorization_code_used: bool,
pub access_token: String,
pub access_token: Option<String>,
pub access_token_expire_at: u64,
pub refresh_token: String,
pub refresh_token_expire_at: u64,
@ -36,6 +40,26 @@ impl Session {
self.authorization_code_expire_at < time() && self.access_token_expire_at < time()
&& self.refresh_token_expire_at < time()
}
pub fn regenerate_access_and_refresh_tokens(&mut self,
app_config: &AppConfig,
jwt_signer: &JWTSigner) -> Res {
let access_token = AccessToken {
issuer: app_config.website_origin.to_string(),
subject_identifier: self.user.clone(),
issued_at: time(),
exp_time: time() + OPEN_ID_ACCESS_TOKEN_TIMEOUT,
rand_val: rand_str(OPEN_ID_ACCESS_TOKEN_LEN),
nonce: self.nonce.clone(),
};
self.access_token_expire_at = access_token.exp_time;
self.access_token = Some(jwt_signer.sign_token(access_token.to_jwt_claims())?);
self.refresh_token = rand_str(OPEN_ID_REFRESH_TOKEN_LEN);
self.refresh_token_expire_at = OPEN_ID_REFRESH_TOKEN_TIMEOUT + time();
Ok(())
}
}
#[derive(Message)]
@ -54,10 +78,6 @@ pub struct FindSessionByRefreshToken(pub String);
#[rtype(result = "Option<Session>")]
pub struct FindSessionByAccessToken(pub String);
#[derive(Message)]
#[rtype(result = "()")]
pub struct MarkAuthorizationCodeUsed(pub String);
#[derive(Message)]
#[rtype(result = "()")]
pub struct UpdateSession(pub Session);
@ -121,23 +141,11 @@ impl Handler<FindSessionByAccessToken> for OpenIDSessionsActor {
fn handle(&mut self, msg: FindSessionByAccessToken, _ctx: &mut Self::Context) -> Self::Result {
self.session
.iter()
.find(|f| f.access_token.eq(&msg.0))
.find(|f| f.access_token.as_ref().map(|t| t.eq(&msg.0)).unwrap_or(false))
.cloned()
}
}
impl Handler<MarkAuthorizationCodeUsed> for OpenIDSessionsActor {
type Result = ();
fn handle(&mut self, msg: MarkAuthorizationCodeUsed, _ctx: &mut Self::Context) -> Self::Result {
if let Some(r) = self.session
.iter_mut()
.find(|f| f.authorization_code.eq(&msg.0)) {
r.authorization_code_used = true;
}
}
}
impl Handler<UpdateSession> for OpenIDSessionsActor {
type Result = ();

View File

@ -53,7 +53,7 @@ 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_ACCESS_TOKEN_LEN: usize = 120;
pub const OPEN_ID_ACCESS_TOKEN_LEN: usize = 50;
pub const OPEN_ID_ACCESS_TOKEN_TIMEOUT: u64 = 3600;
pub const OPEN_ID_REFRESH_TOKEN_LEN: usize = 120;
pub const OPEN_ID_REFRESH_TOKEN_TIMEOUT: u64 = 360000;

View File

@ -9,7 +9,7 @@ use askama::Template;
use crate::actors::{openid_sessions_actor, users_actor};
use crate::actors::openid_sessions_actor::{OpenIDSessionsActor, Session, SessionID};
use crate::actors::users_actor::UsersActor;
use crate::constants::{AUTHORIZE_URI, CERT_URI, OPEN_ID_ACCESS_TOKEN_LEN, OPEN_ID_ACCESS_TOKEN_TIMEOUT, OPEN_ID_AUTHORIZATION_CODE_LEN, OPEN_ID_AUTHORIZATION_CODE_TIMEOUT, OPEN_ID_REFRESH_TOKEN_LEN, OPEN_ID_REFRESH_TOKEN_TIMEOUT, OPEN_ID_SESSION_LEN, TOKEN_URI, USERINFO_URI};
use crate::constants::*;
use crate::controllers::base_controller::FatalErrorPage;
use crate::data::app_config::AppConfig;
use crate::data::client::{ClientID, ClientManager};
@ -36,7 +36,8 @@ pub async fn get_configuration(app_conf: web::Data<AppConfig>) -> impl Responder
subject_types_supported: vec!["public"],
id_token_signing_alg_values_supported: vec!["RS256"],
token_endpoint_auth_methods_supported: vec!["client_secret_post", "client_secret_basic"],
claims_supported: vec!["sub", "exp", "name", "given_name", "family_name", "email"],
claims_supported: vec!["sub", "name", "given_name", "family_name", "email"],
code_challenge_methods_supported: vec!["plain", "S256"],
})
}
@ -138,11 +139,10 @@ pub async fn authorize(user: CurrentUser, id: Identity, query: web::Query<Author
redirect_uri,
authorization_code: rand_str(OPEN_ID_AUTHORIZATION_CODE_LEN),
authorization_code_expire_at: time() + OPEN_ID_AUTHORIZATION_CODE_TIMEOUT,
authorization_code_used: false,
access_token: rand_str(OPEN_ID_ACCESS_TOKEN_LEN),
access_token: None,
access_token_expire_at: time() + OPEN_ID_ACCESS_TOKEN_TIMEOUT,
refresh_token: rand_str(OPEN_ID_REFRESH_TOKEN_LEN),
refresh_token_expire_at: time() + OPEN_ID_REFRESH_TOKEN_TIMEOUT,
refresh_token: "".to_string(),
refresh_token_expire_at: 0,
nonce: query.0.nonce,
code_challenge,
};
@ -152,9 +152,10 @@ pub async fn authorize(user: CurrentUser, id: Identity, query: web::Query<Author
HttpResponse::Found()
.append_header(("Location", format!(
"{}?state={}&session_sate=&code={}",
"{}?state={}&session_state={}&code={}",
session.redirect_uri,
urlencoding::encode(&query.0.state),
urlencoding::encode(&session.session_id.0),
urlencoding::encode(&session.authorization_code)
))).finish()
}
@ -270,7 +271,7 @@ pub async fn token(req: HttpRequest,
&query.authorization_code_query,
&query.refresh_token_query) {
("authorization_code", Some(q), _) => {
let session: Session = match sessions
let mut session: Session = match sessions
.send(openid_sessions_actor::FindSessionByAuthorizationCode(q.code.clone()))
.await.unwrap()
{
@ -293,30 +294,33 @@ pub async fn token(req: HttpRequest,
}
// Check code challenge, if needed
if let Some(chall) = &session.code_challenge {
let code_verifier = match &q.code_verifier {
None => {
return Ok(error_response(&query, "access_denied", "Code verifier missing"));
}
Some(s) => s
};
if !client.disable_code_verifier.unwrap_or(false) {
if let Some(chall) = &session.code_challenge {
let code_verifier = match &q.code_verifier {
None => {
return Ok(error_response(&query, "access_denied", "Code verifier missing"));
}
Some(s) => s
};
if !chall.verify_code(code_verifier) {
return Ok(error_response(&query, "invalid_grant", "Invalid code verifier"));
if !chall.verify_code(code_verifier) {
return Ok(error_response(&query, "invalid_grant", "Invalid code verifier"));
}
}
}
if session.authorization_code_used {
if session.access_token.is_some() {
return Ok(error_response(&query, "invalid_request", "Authorization code already used!"));
}
// Mark session as used
sessions.send(openid_sessions_actor::MarkAuthorizationCodeUsed(session.authorization_code))
session.regenerate_access_and_refresh_tokens(&app_config, &jwt_signer)?;
sessions.send(openid_sessions_actor::UpdateSession(session.clone()))
.await.unwrap();
// Generate id token
let token = IdToken {
let id_token = IdToken {
issuer: app_config.website_origin.to_string(),
subject_identifier: session.user,
audience: session.client.0.to_string(),
@ -327,11 +331,11 @@ pub async fn token(req: HttpRequest,
};
TokenResponse {
access_token: session.access_token,
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(),
id_token: Some(jwt_signer.sign_token(token.to_jwt_claims())?),
id_token: Some(jwt_signer.sign_token(id_token.to_jwt_claims())?),
}
}
@ -354,17 +358,14 @@ pub async fn token(req: HttpRequest,
return Ok(error_response(&query, "access_denied", "Refresh token has expired!"));
}
session.refresh_token = rand_str(OPEN_ID_REFRESH_TOKEN_LEN);
session.refresh_token_expire_at = OPEN_ID_REFRESH_TOKEN_TIMEOUT + time();
session.access_token = rand_str(OPEN_ID_ACCESS_TOKEN_LEN);
session.access_token_expire_at = OPEN_ID_ACCESS_TOKEN_TIMEOUT + time();
session.regenerate_access_and_refresh_tokens(&app_config, &jwt_signer)?;
sessions
.send(openid_sessions_actor::UpdateSession(session.clone()))
.await.unwrap();
TokenResponse {
access_token: session.access_token,
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(),

34
src/data/access_token.rs Normal file
View File

@ -0,0 +1,34 @@
use jwt_simple::claims::JWTClaims;
use jwt_simple::prelude::Duration;
pub struct AccessToken {
pub issuer: String,
pub subject_identifier: String,
pub issued_at: u64,
pub exp_time: u64,
pub rand_val: String,
pub nonce: Option<String>,
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct CustomAccessTokenClaims {
rand_val: String,
}
impl AccessToken {
pub fn to_jwt_claims(self) -> JWTClaims<CustomAccessTokenClaims> {
JWTClaims {
issued_at: Some(Duration::from_secs(self.issued_at)),
expires_at: Some(Duration::from_secs(self.exp_time)),
invalid_before: None,
issuer: Some(self.issuer),
subject: Some(self.subject_identifier),
audiences: None,
jwt_id: None,
nonce: self.nonce,
custom: CustomAccessTokenClaims {
rand_val: self.rand_val
},
}
}
}

View File

@ -10,6 +10,7 @@ pub struct Client {
pub description: String,
pub secret: String,
pub redirect_uri: String,
pub disable_code_verifier: Option<bool>,
}
impl PartialEq for Client {

View File

@ -10,3 +10,4 @@ pub mod jwt_signer;
pub mod id_token;
pub mod code_challenge;
pub mod open_id_user_info;
pub mod access_token;

View File

@ -32,4 +32,6 @@ pub struct OpenIDConfig {
/// RECOMMENDED. JSON array containing a list of the Claim Names of the Claims that the OpenID Provider MAY be able to supply values for. Note that for privacy or other reasons, this might not be an exhaustive list.
pub claims_supported: Vec<&'static str>,
pub code_challenge_methods_supported: Vec<&'static str>,
}