Add /openid/token route

This commit is contained in:
Pierre HUBERT 2022-04-12 20:40:44 +02:00
parent 97203a955d
commit d69b44528e
8 changed files with 188 additions and 16 deletions

1
Cargo.lock generated
View File

@ -386,6 +386,7 @@ dependencies = [
"actix-identity",
"actix-web",
"askama",
"base64",
"bcrypt",
"clap",
"env_logger",

View File

@ -22,4 +22,5 @@ mime_guess = "2.0.4"
askama = "0.11.1"
futures-util = "0.3.21"
urlencoding = "2.1.0"
rand = "0.8.5"
rand = "0.8.5"
base64 = "0.13.0"

View File

@ -17,10 +17,13 @@ pub struct Session {
pub redirect_uri: String,
pub authorization_code: String,
pub code_expire_on: u64,
pub authorization_code_expire_at: u64,
pub authorization_code_used: bool,
pub token: String,
pub token_expire_at: u64,
pub access_token: String,
pub access_token_expire_at: u64,
pub refresh_token: String,
pub refresh_token_expire_at: u64,
pub nonce: Option<String>,
pub code_challenge: Option<(String, String)>,
@ -28,7 +31,8 @@ pub struct Session {
impl Session {
pub fn is_expired(&self) -> bool {
self.code_expire_on < time() && self.token_expire_at < time()
self.authorization_code_expire_at < time() && self.access_token_expire_at < time()
&& self.refresh_token_expire_at < time()
}
}
@ -36,6 +40,14 @@ impl Session {
#[rtype(result = "()")]
pub struct PushNewSession(pub Session);
#[derive(Message)]
#[rtype(result = "Option<Session>")]
pub struct FindSessionByAuthorizationCode(pub String);
#[derive(Message)]
#[rtype(result = "()")]
pub struct MarkAuthorizationCodeUsed(pub String);
#[derive(Default)]
pub struct OpenIDSessionsActor {
session: Vec<Session>,
@ -65,4 +77,27 @@ impl Handler<PushNewSession> for OpenIDSessionsActor {
fn handle(&mut self, msg: PushNewSession, _ctx: &mut Self::Context) -> Self::Result {
self.session.push(msg.0)
}
}
impl Handler<FindSessionByAuthorizationCode> for OpenIDSessionsActor {
type Result = Option<Session>;
fn handle(&mut self, msg: FindSessionByAuthorizationCode, _ctx: &mut Self::Context) -> Self::Result {
self.session
.iter()
.find(|f| f.authorization_code.eq(&msg.0))
.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;
}
}
}

View File

@ -44,11 +44,14 @@ pub const TEMPORARY_PASSWORDS_LEN: usize = 20;
/// Open ID routes
pub const AUTHORIZE_URI: &str = "/openid/authorize";
pub const TOKEN_URI: &str = "/openid/token";
/// Open ID constants
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_TOKEN_LEN: usize = 120;
pub const OPEN_ID_TOKEN_TIMEOUT: u64 = 3600;
pub const OPEN_ID_ACCESS_TOKEN_LEN: usize = 120;
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

@ -1,10 +1,11 @@
use actix::Addr;
use actix_web::{HttpResponse, Responder, web};
use actix_web::{HttpRequest, HttpResponse, Responder, web};
use actix_web::error::ErrorUnauthorized;
use askama::Template;
use crate::actors::openid_sessions_actor;
use crate::actors::openid_sessions_actor::{OpenIDSessionsActor, Session, SessionID};
use crate::constants::{AUTHORIZE_URI, OPEN_ID_AUTHORIZATION_CODE_LEN, OPEN_ID_AUTHORIZATION_CODE_TIMEOUT, OPEN_ID_SESSION_LEN, OPEN_ID_TOKEN_LEN, OPEN_ID_TOKEN_TIMEOUT};
use crate::constants::{AUTHORIZE_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};
use crate::controllers::base_controller::FatalErrorPage;
use crate::data::app_config::AppConfig;
use crate::data::client::{ClientID, ClientManager};
@ -17,13 +18,14 @@ pub async fn get_configuration(app_conf: web::Data<AppConfig>) -> impl Responder
HttpResponse::Ok().json(OpenIDConfig {
issuer: app_conf.full_url("/"),
authorization_endpoint: app_conf.full_url(AUTHORIZE_URI),
token_endpoint: app_conf.full_url("openid/token"),
token_endpoint: app_conf.full_url(TOKEN_URI),
userinfo_endpoint: app_conf.full_url("openid/userinfo"),
jwks_uri: app_conf.full_url("openid/jwks_uri"),
scopes_supported: vec!["openid", "profile", "email"],
response_types_supported: vec!["code", "id_token", "token id_token"],
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"],
})
}
@ -123,9 +125,12 @@ pub async fn authorize(user: CurrentUser, query: web::Query<AuthorizeQuery>,
user: user.uid.clone(),
redirect_uri,
authorization_code: rand_str(OPEN_ID_AUTHORIZATION_CODE_LEN),
code_expire_on: time() + OPEN_ID_AUTHORIZATION_CODE_TIMEOUT,
token: rand_str(OPEN_ID_TOKEN_LEN),
token_expire_at: time() + OPEN_ID_TOKEN_TIMEOUT,
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_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,
nonce: query.0.nonce,
code_challenge,
};
@ -140,4 +145,127 @@ pub async fn authorize(user: CurrentUser, query: web::Query<AuthorizeQuery>,
urlencoding::encode(&query.0.state),
urlencoding::encode(&session.authorization_code)
))).finish()
}
#[derive(Debug, serde::Deserialize)]
pub struct TokenQuery {
grant_type: String,
client_id: Option<ClientID>,
client_secret: Option<String>,
redirect_uri: String,
code: String,
}
#[derive(Debug, serde::Serialize)]
pub struct TokenResponse {
access_token: String,
token_type: &'static str,
refresh_token: String,
expires_in: u64,
id_token: String,
}
pub async fn token(req: HttpRequest,
query: web::Form<TokenQuery>,
clients: web::Data<ClientManager>,
sessions: web::Data<Addr<OpenIDSessionsActor>>) -> actix_web::Result<HttpResponse> {
// Extraction authentication information
let authorization_header = req.headers().get("authorization");
let (client_id, client_secret) = match (&query.client_id, &query.client_secret, authorization_header) {
// post authentication
(Some(client_id), Some(client_secret), None) => {
(client_id.clone(), client_secret.to_string())
}
// Basic authentication
(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 '"));
}
Some(v) => v
};
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!"));
}
}).to_string();
match decode.split_once(':') {
None => (ClientID(decode), "".to_string()),
Some((id, secret)) => (ClientID(id.to_string()), secret.to_string())
}
}
_ => {
log::warn!("Token request failed: Unknown client authentication method! {:#?}", query.0);
return Ok(HttpResponse::BadRequest().body("Authentication method unknown!"));
}
};
let client = clients
.find_by_id(&client_id)
.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!"));
}
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 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!"));
}
Some(s) => s,
};
if session.client != client.id {
log::warn!("Token request failed: Client mismatch! {:#?}", query.0);
return Ok(HttpResponse::Unauthorized().body("Client mismatch!"));
}
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.authorization_code_expire_at < time() {
log::warn!("Token request failed: Authorization code expired! {:#?}", query.0);
return Ok(HttpResponse::Unauthorized().body("Authorization code expired!"));
}
if session.authorization_code_used {
log::warn!("Token request failed: Authorization already used! {:#?}", query.0);
return Ok(HttpResponse::Unauthorized().body("Authorization already used!"));
}
// Mark session as used
sessions.send(openid_sessions_actor::MarkAuthorizationCodeUsed(session.authorization_code))
.await.unwrap();
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: session.session_id.0,
}))
}

View File

@ -27,6 +27,9 @@ pub struct OpenIDConfig {
/// REQUIRED. JSON array containing a list of the JWS signing algorithms (alg values) supported by the OP for the ID Token to encode the Claims in a JWT `[`JWT`. The algorithm RS256 MUST be included. The value none MAY be supported, but MUST NOT be used unless the Response Type used returns no ID Token from the Authorization Endpoint (such as when using the Authorization Code Flow).
pub id_token_signing_alg_values_supported: Vec<&'static str>,
/// OPTIONAL. JSON array containing a list of Client Authentication methods supported by this Token Endpoint. The options are client_secret_post, client_secret_basic, client_secret_jwt, and private_key_jwt
pub token_endpoint_auth_methods_supported: Vec<&'static str>,
/// 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>,
}

View File

@ -129,7 +129,8 @@ async fn main() -> std::io::Result<()> {
// OpenID routes
.route("/.well-known/openid-configuration", web::get().to(openid_controller::get_configuration))
.route("/openid/authorize", web::get().to(openid_controller::authorize))
.route(AUTHORIZE_URI, web::get().to(openid_controller::authorize))
.route(TOKEN_URI, web::post().to(openid_controller::token))
})
.bind(listen_address)?
.run()

View File

@ -13,7 +13,7 @@ use actix_web::body::EitherBody;
use actix_web::http::{header, Method};
use askama::Template;
use crate::constants::{ADMIN_ROUTES, AUTHENTICATED_ROUTES, AUTHORIZE_URI};
use crate::constants::{ADMIN_ROUTES, AUTHENTICATED_ROUTES, AUTHORIZE_URI, TOKEN_URI};
use crate::controllers::base_controller::{FatalErrorPage, redirect_user_for_login};
use crate::data::app_config::AppConfig;
use crate::data::session_identity::{SessionIdentity, SessionIdentityData, SessionStatus};
@ -91,7 +91,7 @@ impl<S, B> Service<ServiceRequest> for AuthInnerMiddleware<S>
// Check if POST request comes from another website (block invalid origins)
let origin = req.headers().get(header::ORIGIN);
if req.method() == Method::POST {
if req.method() == Method::POST && req.path() != TOKEN_URI {
if let Some(o) = origin {
if !o.to_str().unwrap_or("bad").eq(&config.website_origin) {
log::warn!(