Add /openid/token route

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

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,
}))
}