Compare commits
2 Commits
8a005c4185
...
0b64c88fc6
Author | SHA1 | Date | |
---|---|---|---|
0b64c88fc6 | |||
078a913f6a |
@ -45,10 +45,18 @@ pub struct PushNewSession(pub Session);
|
||||
#[rtype(result = "Option<Session>")]
|
||||
pub struct FindSessionByAuthorizationCode(pub String);
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "Option<Session>")]
|
||||
pub struct FindSessionByRefreshToken(pub String);
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "()")]
|
||||
pub struct MarkAuthorizationCodeUsed(pub String);
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "()")]
|
||||
pub struct UpdateSession(pub Session);
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct OpenIDSessionsActor {
|
||||
session: Vec<Session>,
|
||||
@ -91,6 +99,17 @@ impl Handler<FindSessionByAuthorizationCode> for OpenIDSessionsActor {
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<FindSessionByRefreshToken> for OpenIDSessionsActor {
|
||||
type Result = Option<Session>;
|
||||
|
||||
fn handle(&mut self, msg: FindSessionByRefreshToken, _ctx: &mut Self::Context) -> Self::Result {
|
||||
self.session
|
||||
.iter()
|
||||
.find(|f| f.refresh_token.eq(&msg.0))
|
||||
.cloned()
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<MarkAuthorizationCodeUsed> for OpenIDSessionsActor {
|
||||
type Result = ();
|
||||
|
||||
@ -102,3 +121,14 @@ impl Handler<MarkAuthorizationCodeUsed> for OpenIDSessionsActor {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<UpdateSession> for OpenIDSessionsActor {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: UpdateSession, _ctx: &mut Self::Context) -> Self::Result {
|
||||
if let Some(r) = self.session.iter().enumerate()
|
||||
.find(|f| f.1.session_id.eq(&msg.0.session_id)).map(|f| f.0) {
|
||||
self.session[r] = msg.0;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use actix::Addr;
|
||||
use actix_identity::Identity;
|
||||
use actix_web::{HttpRequest, HttpResponse, Responder, web};
|
||||
@ -152,13 +154,44 @@ pub async fn authorize(user: CurrentUser, id: Identity, query: web::Query<Author
|
||||
))).finish()
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct ErrorResponse {
|
||||
error: String,
|
||||
error_description: String,
|
||||
}
|
||||
|
||||
pub fn error_response<D: Debug>(query: &D, error: &str, description: &str) -> HttpResponse {
|
||||
log::warn!("request failed: {} - {} => '{:#?}'", error, description, query);
|
||||
HttpResponse::BadRequest()
|
||||
.json(ErrorResponse {
|
||||
error: error.to_string(),
|
||||
error_description: description.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct TokenAuthorizationCodeQuery {
|
||||
redirect_uri: String,
|
||||
code: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct TokenRefreshTokenQuery {
|
||||
refresh_token: String,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct TokenQuery {
|
||||
grant_type: String,
|
||||
client_id: Option<ClientID>,
|
||||
client_secret: Option<String>,
|
||||
redirect_uri: String,
|
||||
code: String,
|
||||
|
||||
#[serde(flatten)]
|
||||
authorization_code_query: Option<TokenAuthorizationCodeQuery>,
|
||||
|
||||
#[serde(flatten)]
|
||||
refresh_token_query: Option<TokenRefreshTokenQuery>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
@ -167,7 +200,8 @@ pub struct TokenResponse {
|
||||
token_type: &'static str,
|
||||
refresh_token: String,
|
||||
expires_in: u64,
|
||||
id_token: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
id_token: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn token(req: HttpRequest,
|
||||
@ -176,8 +210,7 @@ pub async fn token(req: HttpRequest,
|
||||
app_config: web::Data<AppConfig>,
|
||||
sessions: web::Data<Addr<OpenIDSessionsActor>>,
|
||||
jwt_signer: web::Data<JWTSigner>) -> actix_web::Result<HttpResponse> {
|
||||
// TODO : add refresh tokens : https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens
|
||||
// TODO : check auth challenge
|
||||
// TODO : check auth challenge : https://oa.dnc.global/-fr-.html?page=unarticle&id_article=148&lang=fr
|
||||
|
||||
// Extraction authentication information
|
||||
let authorization_header = req.headers().get("authorization");
|
||||
@ -191,8 +224,11 @@ pub async fn token(req: HttpRequest,
|
||||
(None, None, Some(v)) => {
|
||||
let token = match v.to_str().unwrap_or_default().strip_prefix("Basic ") {
|
||||
None => {
|
||||
log::warn!("Token request failed: Authorization header does not start with 'Basic '! => got '{:#?}'", v);
|
||||
return Ok(HttpResponse::Unauthorized().body("Authorization header does not start with 'Basic '"));
|
||||
return Ok(error_response(
|
||||
&query,
|
||||
"invalid_request",
|
||||
&format!("Authorization header does not start with 'Basic ', got '{:#?}'", v),
|
||||
));
|
||||
}
|
||||
Some(v) => v
|
||||
};
|
||||
@ -200,8 +236,8 @@ pub async fn token(req: HttpRequest,
|
||||
let decode = String::from_utf8_lossy(&match base64::decode(token) {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
log::warn!("Failed to decode authorization header! {:?}", e);
|
||||
return Ok(HttpResponse::InternalServerError().body("Failed to decode authorization header!"));
|
||||
log::error!("Failed to decode authorization header: {:?}", e);
|
||||
return Ok(error_response(&query, "invalid_request", "Failed to decode authorization header!"));
|
||||
}
|
||||
}).to_string();
|
||||
|
||||
@ -212,8 +248,7 @@ pub async fn token(req: HttpRequest,
|
||||
}
|
||||
|
||||
_ => {
|
||||
log::warn!("Token request failed: Unknown client authentication method! {:#?}", query.0);
|
||||
return Ok(HttpResponse::BadRequest().body("Authentication method unknown!"));
|
||||
return Ok(error_response(&query, "invalid_request", "Authentication method unknown!"));
|
||||
}
|
||||
};
|
||||
|
||||
@ -222,72 +257,106 @@ pub async fn token(req: HttpRequest,
|
||||
.ok_or_else(|| ErrorUnauthorized("Client not found"))?;
|
||||
|
||||
if !client.secret.eq(&client_secret) {
|
||||
log::warn!("Token request failed: client secret is invalid! {:#?}", query.0);
|
||||
return Ok(HttpResponse::Unauthorized().body("Client secret is invalid!"));
|
||||
return Ok(error_response(&query, "invalid_request", "Client secret is invalid!"));
|
||||
}
|
||||
|
||||
if query.grant_type != "authorization_code" {
|
||||
log::warn!("Token request failed: Grant type unsupported! {:#?}", query.0);
|
||||
return Ok(HttpResponse::BadRequest().body("Grant type unsupported!"));
|
||||
}
|
||||
let token_response = match (query.grant_type.as_str(),
|
||||
&query.authorization_code_query,
|
||||
&query.refresh_token_query) {
|
||||
("authorization_code", Some(q), _) => {
|
||||
let session: Session = match sessions
|
||||
.send(openid_sessions_actor::FindSessionByAuthorizationCode(q.code.clone()))
|
||||
.await.unwrap()
|
||||
{
|
||||
None => {
|
||||
return Ok(error_response(&query, "invalid_request", "Session not found!"));
|
||||
}
|
||||
Some(s) => s,
|
||||
};
|
||||
|
||||
let session: Session = match sessions
|
||||
.send(openid_sessions_actor::FindSessionByAuthorizationCode(query.code.clone()))
|
||||
.await.unwrap()
|
||||
{
|
||||
None => {
|
||||
log::warn!("Token request failed: Session not found! {:#?}", query.0);
|
||||
return Ok(HttpResponse::NotFound().body("Session not found!"));
|
||||
if session.client != client.id {
|
||||
return Ok(error_response(&query, "invalid_request", "Client mismatch!"));
|
||||
}
|
||||
|
||||
if session.redirect_uri != q.redirect_uri {
|
||||
return Ok(error_response(&query, "invalid_request", "Invalid redirect URI!"));
|
||||
}
|
||||
|
||||
if session.authorization_code_expire_at < time() {
|
||||
return Ok(error_response(&query, "invalid_request", "Authorization code expired!"));
|
||||
}
|
||||
|
||||
if session.authorization_code_used {
|
||||
return Ok(error_response(&query, "invalid_request", "Authorization already used!"));
|
||||
}
|
||||
|
||||
// Mark session as used
|
||||
sessions.send(openid_sessions_actor::MarkAuthorizationCodeUsed(session.authorization_code))
|
||||
.await.unwrap();
|
||||
|
||||
|
||||
// Generate id token
|
||||
let token = IdToken {
|
||||
issuer: app_config.website_origin.to_string(),
|
||||
subject_identifier: session.user,
|
||||
audience: session.client.0.to_string(),
|
||||
expiration_time: session.access_token_expire_at,
|
||||
issued_at: time(),
|
||||
auth_time: session.auth_time,
|
||||
nonce: session.nonce,
|
||||
};
|
||||
|
||||
TokenResponse {
|
||||
access_token: session.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())?),
|
||||
}
|
||||
}
|
||||
Some(s) => s,
|
||||
};
|
||||
|
||||
if session.client != client.id {
|
||||
log::warn!("Token request failed: Client mismatch! {:#?}", query.0);
|
||||
return Ok(HttpResponse::Unauthorized().body("Client mismatch!"));
|
||||
}
|
||||
("refresh_token", _, Some(q)) => {
|
||||
let mut session: Session = match sessions
|
||||
.send(openid_sessions_actor::FindSessionByRefreshToken(q.refresh_token.clone()))
|
||||
.await.unwrap()
|
||||
{
|
||||
None => {
|
||||
return Ok(error_response(&query, "invalid_request", "Session not found!"));
|
||||
}
|
||||
Some(s) => s,
|
||||
};
|
||||
|
||||
if session.redirect_uri != query.redirect_uri {
|
||||
log::warn!("Token request failed: Invalid redirect URI! {:#?}", query.0);
|
||||
return Ok(HttpResponse::Unauthorized().body("Invalid redirect URI!"));
|
||||
}
|
||||
if session.client != client.id {
|
||||
return Ok(error_response(&query, "invalid_request", "Client mismatch!"));
|
||||
}
|
||||
|
||||
if session.authorization_code_expire_at < time() {
|
||||
log::warn!("Token request failed: Authorization code expired! {:#?}", query.0);
|
||||
return Ok(HttpResponse::Unauthorized().body("Authorization code 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();
|
||||
|
||||
if session.authorization_code_used {
|
||||
log::warn!("Token request failed: Authorization already used! {:#?}", query.0);
|
||||
return Ok(HttpResponse::Unauthorized().body("Authorization already used!"));
|
||||
}
|
||||
sessions
|
||||
.send(openid_sessions_actor::UpdateSession(session.clone()))
|
||||
.await.unwrap();
|
||||
|
||||
// Mark session as used
|
||||
sessions.send(openid_sessions_actor::MarkAuthorizationCodeUsed(session.authorization_code))
|
||||
.await.unwrap();
|
||||
TokenResponse {
|
||||
access_token: session.access_token,
|
||||
token_type: "Bearer",
|
||||
refresh_token: session.refresh_token,
|
||||
expires_in: session.access_token_expire_at - time(),
|
||||
id_token: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Generate id token
|
||||
let token = IdToken {
|
||||
issuer: app_config.website_origin.to_string(),
|
||||
subject_identifier: session.user,
|
||||
audience: session.client.0.to_string(),
|
||||
expiration_time: session.access_token_expire_at,
|
||||
issued_at: time(),
|
||||
auth_time: session.auth_time,
|
||||
nonce: session.nonce,
|
||||
_ => {
|
||||
return Ok(error_response(&query, "invalid_request", "Grant type unsupported!"));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.append_header(("Cache-Control", "no-store"))
|
||||
.append_header(("Pragam", "no-cache"))
|
||||
.json(TokenResponse {
|
||||
access_token: session.access_token,
|
||||
token_type: "Bearer",
|
||||
refresh_token: session.refresh_token,
|
||||
expires_in: session.access_token_expire_at - time(),
|
||||
id_token: jwt_signer.sign_token(token.to_jwt_claims())?
|
||||
}))
|
||||
.json(token_response))
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
|
Loading…
x
Reference in New Issue
Block a user