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 = ();
@ -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;
}
}
}

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,44 +257,37 @@ 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" {
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 let session: Session = match sessions
.send(openid_sessions_actor::FindSessionByAuthorizationCode(query.code.clone())) .send(openid_sessions_actor::FindSessionByAuthorizationCode(q.code.clone()))
.await.unwrap() .await.unwrap()
{ {
None => { None => {
log::warn!("Token request failed: Session not found! {:#?}", query.0); return Ok(error_response(&query, "invalid_request", "Session not found!"));
return Ok(HttpResponse::NotFound().body("Session not found!"));
} }
Some(s) => s, Some(s) => s,
}; };
if session.client != client.id { if session.client != client.id {
log::warn!("Token request failed: Client mismatch! {:#?}", query.0); return Ok(error_response(&query, "invalid_request", "Client mismatch!"));
return Ok(HttpResponse::Unauthorized().body("Client mismatch!"));
} }
if session.redirect_uri != query.redirect_uri { if session.redirect_uri != q.redirect_uri {
log::warn!("Token request failed: Invalid redirect URI! {:#?}", query.0); return Ok(error_response(&query, "invalid_request", "Invalid redirect URI!"));
return Ok(HttpResponse::Unauthorized().body("Invalid redirect URI!"));
} }
if session.authorization_code_expire_at < time() { if session.authorization_code_expire_at < time() {
log::warn!("Token request failed: Authorization code expired! {:#?}", query.0); return Ok(error_response(&query, "invalid_request", "Authorization code expired!"));
return Ok(HttpResponse::Unauthorized().body("Authorization code expired!"));
} }
if session.authorization_code_used { if session.authorization_code_used {
log::warn!("Token request failed: Authorization already used! {:#?}", query.0); return Ok(error_response(&query, "invalid_request", "Authorization already used!"));
return Ok(HttpResponse::Unauthorized().body("Authorization already used!"));
} }
// Mark session as used // Mark session as used
@ -278,16 +306,57 @@ pub async fn token(req: HttpRequest,
nonce: session.nonce, nonce: session.nonce,
}; };
Ok(HttpResponse::Ok() TokenResponse {
.append_header(("Cache-Control", "no-store"))
.append_header(("Pragam", "no-cache"))
.json(TokenResponse {
access_token: session.access_token, access_token: session.access_token,
token_type: "Bearer", token_type: "Bearer",
refresh_token: session.refresh_token, refresh_token: session.refresh_token,
expires_in: session.access_token_expire_at - time(), expires_in: session.access_token_expire_at - time(),
id_token: jwt_signer.sign_token(token.to_jwt_claims())? id_token: Some(jwt_signer.sign_token(token.to_jwt_claims())?),
})) }
}
("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.client != client.id {
return Ok(error_response(&query, "invalid_request", "Client mismatch!"));
}
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();
sessions
.send(openid_sessions_actor::UpdateSession(session.clone()))
.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,
}
}
_ => {
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(token_response))
} }
#[derive(serde::Serialize)] #[derive(serde::Serialize)]