Compare commits

...

2 Commits

Author SHA1 Message Date
0b64c88fc6 Normalize error responses 2022-04-14 17:13:07 +02:00
078a913f6a Can request refresh tokens 2022-04-14 17:02:47 +02:00
2 changed files with 161 additions and 62 deletions

View File

@ -45,10 +45,18 @@ pub struct PushNewSession(pub Session);
#[rtype(result = "Option<Session>")] #[rtype(result = "Option<Session>")]
pub struct FindSessionByAuthorizationCode(pub String); pub struct FindSessionByAuthorizationCode(pub String);
#[derive(Message)]
#[rtype(result = "Option<Session>")]
pub struct FindSessionByRefreshToken(pub String);
#[derive(Message)] #[derive(Message)]
#[rtype(result = "()")] #[rtype(result = "()")]
pub struct MarkAuthorizationCodeUsed(pub String); pub struct MarkAuthorizationCodeUsed(pub String);
#[derive(Message)]
#[rtype(result = "()")]
pub struct UpdateSession(pub Session);
#[derive(Default)] #[derive(Default)]
pub struct OpenIDSessionsActor { pub struct OpenIDSessionsActor {
session: Vec<Session>, 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 { impl Handler<MarkAuthorizationCodeUsed> for OpenIDSessionsActor {
type Result = (); type Result = ();
@ -101,4 +120,15 @@ impl Handler<MarkAuthorizationCodeUsed> for OpenIDSessionsActor {
r.authorization_code_used = true; r.authorization_code_used = true;
} }
} }
}
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;
}
}
} }

View File

@ -1,3 +1,5 @@
use std::fmt::Debug;
use actix::Addr; use actix::Addr;
use actix_identity::Identity; use actix_identity::Identity;
use actix_web::{HttpRequest, HttpResponse, Responder, web}; use actix_web::{HttpRequest, HttpResponse, Responder, web};
@ -152,13 +154,44 @@ pub async fn authorize(user: CurrentUser, id: Identity, query: web::Query<Author
))).finish() ))).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)] #[derive(Debug, serde::Deserialize)]
pub struct TokenQuery { pub struct TokenQuery {
grant_type: String, grant_type: String,
client_id: Option<ClientID>, client_id: Option<ClientID>,
client_secret: Option<String>, 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)] #[derive(Debug, serde::Serialize)]
@ -167,7 +200,8 @@ pub struct TokenResponse {
token_type: &'static str, token_type: &'static str,
refresh_token: String, refresh_token: String,
expires_in: u64, expires_in: u64,
id_token: String, #[serde(skip_serializing_if = "Option::is_none")]
id_token: Option<String>,
} }
pub async fn token(req: HttpRequest, pub async fn token(req: HttpRequest,
@ -176,8 +210,7 @@ pub async fn token(req: HttpRequest,
app_config: web::Data<AppConfig>, app_config: web::Data<AppConfig>,
sessions: web::Data<Addr<OpenIDSessionsActor>>, sessions: web::Data<Addr<OpenIDSessionsActor>>,
jwt_signer: web::Data<JWTSigner>) -> actix_web::Result<HttpResponse> { 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 : https://oa.dnc.global/-fr-.html?page=unarticle&id_article=148&lang=fr
// TODO : check auth challenge
// Extraction authentication information // Extraction authentication information
let authorization_header = req.headers().get("authorization"); let authorization_header = req.headers().get("authorization");
@ -191,8 +224,11 @@ pub async fn token(req: HttpRequest,
(None, None, Some(v)) => { (None, None, Some(v)) => {
let token = match v.to_str().unwrap_or_default().strip_prefix("Basic ") { let token = match v.to_str().unwrap_or_default().strip_prefix("Basic ") {
None => { None => {
log::warn!("Token request failed: Authorization header does not start with 'Basic '! => got '{:#?}'", v); return Ok(error_response(
return Ok(HttpResponse::Unauthorized().body("Authorization header does not start with 'Basic '")); &query,
"invalid_request",
&format!("Authorization header does not start with 'Basic ', got '{:#?}'", v),
));
} }
Some(v) => v Some(v) => v
}; };
@ -200,8 +236,8 @@ pub async fn token(req: HttpRequest,
let decode = String::from_utf8_lossy(&match base64::decode(token) { let decode = String::from_utf8_lossy(&match base64::decode(token) {
Ok(d) => d, Ok(d) => d,
Err(e) => { Err(e) => {
log::warn!("Failed to decode authorization header! {:?}", e); log::error!("Failed to decode authorization header: {:?}", e);
return Ok(HttpResponse::InternalServerError().body("Failed to decode authorization header!")); return Ok(error_response(&query, "invalid_request", "Failed to decode authorization header!"));
} }
}).to_string(); }).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(error_response(&query, "invalid_request", "Authentication method unknown!"));
return Ok(HttpResponse::BadRequest().body("Authentication method unknown!"));
} }
}; };
@ -222,72 +257,106 @@ pub async fn token(req: HttpRequest,
.ok_or_else(|| ErrorUnauthorized("Client not found"))?; .ok_or_else(|| ErrorUnauthorized("Client not found"))?;
if !client.secret.eq(&client_secret) { if !client.secret.eq(&client_secret) {
log::warn!("Token request failed: client secret is invalid! {:#?}", query.0); return Ok(error_response(&query, "invalid_request", "Client secret is invalid!"));
return Ok(HttpResponse::Unauthorized().body("Client secret is invalid!"));
} }
if query.grant_type != "authorization_code" { let token_response = match (query.grant_type.as_str(),
log::warn!("Token request failed: Grant type unsupported! {:#?}", query.0); &query.authorization_code_query,
return Ok(HttpResponse::BadRequest().body("Grant type unsupported!")); &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 if session.client != client.id {
.send(openid_sessions_actor::FindSessionByAuthorizationCode(query.code.clone())) return Ok(error_response(&query, "invalid_request", "Client mismatch!"));
.await.unwrap() }
{
None => { if session.redirect_uri != q.redirect_uri {
log::warn!("Token request failed: Session not found! {:#?}", query.0); return Ok(error_response(&query, "invalid_request", "Invalid redirect URI!"));
return Ok(HttpResponse::NotFound().body("Session not found!")); }
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 { ("refresh_token", _, Some(q)) => {
log::warn!("Token request failed: Client mismatch! {:#?}", query.0); let mut session: Session = match sessions
return Ok(HttpResponse::Unauthorized().body("Client mismatch!")); .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 { if session.client != client.id {
log::warn!("Token request failed: Invalid redirect URI! {:#?}", query.0); return Ok(error_response(&query, "invalid_request", "Client mismatch!"));
return Ok(HttpResponse::Unauthorized().body("Invalid redirect URI!")); }
}
if session.authorization_code_expire_at < time() { session.refresh_token = rand_str(OPEN_ID_REFRESH_TOKEN_LEN);
log::warn!("Token request failed: Authorization code expired! {:#?}", query.0); session.refresh_token_expire_at = OPEN_ID_REFRESH_TOKEN_TIMEOUT + time();
return Ok(HttpResponse::Unauthorized().body("Authorization code expired!")); 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 { sessions
log::warn!("Token request failed: Authorization already used! {:#?}", query.0); .send(openid_sessions_actor::UpdateSession(session.clone()))
return Ok(HttpResponse::Unauthorized().body("Authorization already used!")); .await.unwrap();
}
// Mark session as used TokenResponse {
sessions.send(openid_sessions_actor::MarkAuthorizationCodeUsed(session.authorization_code)) access_token: session.access_token,
.await.unwrap(); token_type: "Bearer",
refresh_token: session.refresh_token,
expires_in: session.access_token_expire_at - time(),
id_token: None,
}
}
_ => {
// Generate id token return Ok(error_response(&query, "invalid_request", "Grant type unsupported!"));
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,
}; };
Ok(HttpResponse::Ok() Ok(HttpResponse::Ok()
.append_header(("Cache-Control", "no-store")) .append_header(("Cache-Control", "no-store"))
.append_header(("Pragam", "no-cache")) .append_header(("Pragam", "no-cache"))
.json(TokenResponse { .json(token_response))
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())?
}))
} }
#[derive(serde::Serialize)] #[derive(serde::Serialize)]