Compare commits
5 Commits
1d21b30b68
...
03a4bbb580
Author | SHA1 | Date | |
---|---|---|---|
03a4bbb580 | |||
acabf438ed | |||
94c601119a | |||
69bb2816b9 | |||
cac461e03d |
@ -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 = ();
|
||||
|
||||
|
@ -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;
|
@ -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
34
src/data/access_token.rs
Normal 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
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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;
|
@ -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>,
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user