ezidam: oauth: added token endpoint, handling "authorization_code" and "refresh_token" grants

This commit is contained in:
Philippe Loctaux 2023-04-04 00:03:41 +02:00
parent dd69fc99ea
commit fe24825c3b
5 changed files with 402 additions and 1 deletions

View file

@ -34,4 +34,10 @@ impl App {
pub fn is_archived(&self) -> bool {
self.is_archived
}
pub fn is_confidential(&self) -> bool {
self.is_confidential
}
pub fn secret_hashed(&self) -> &str {
&self.secret
}
}

View file

@ -16,4 +16,30 @@ pub struct AuthenticationRequest<'r> {
pub client_id: &'r str,
pub redirect_uri: &'r str,
pub state: &'r str,
pub nonce: Option<&'r str>,
}
#[derive(Debug, FromForm, UriDisplayQuery)]
pub struct RedirectRequest<'r> {
pub code: &'r str,
pub state: &'r str,
}
#[derive(Debug, FromFormField, UriDisplayQuery)]
pub enum GrantType {
#[field(value = "authorization_code")]
AuthorizationCode,
#[field(value = "refresh_token")]
RefreshToken,
}
#[derive(Debug, FromForm, UriDisplayQuery)]
pub struct TokenRequest<'r> {
pub grant_type: GrantType,
pub code: Option<&'r str>,
pub redirect_uri: Option<&'r str>,
pub client_id: &'r str,
pub client_secret: Option<&'r str>,
pub scope: Option<&'r str>,
pub refresh_token: Option<&'r str>,
}

View file

@ -1,16 +1,19 @@
use authorize::*;
use redirect::*;
use rocket::{routes, Route};
use token::*;
pub mod authorize;
pub mod redirect;
pub mod token;
pub fn routes() -> Vec<Route> {
routes![
authorize_page,
authorize_form,
authorize_ezidam,
redirect_page
redirect_page,
request_token,
]
}

View file

@ -59,6 +59,7 @@ pub async fn authorize_ezidam(mut db: Connection<Database>) -> Result<Redirect>
client_id: app.id().as_ref(),
redirect_uri: app.redirect_uri(),
state: "TODO",
nonce: None,
};
Ok(Redirect::to(uri!(authorize_page(auth_request = request))))
}

View file

@ -0,0 +1,365 @@
use crate::routes::prelude::*;
use crate::tokens::{generate_jwt, generate_refresh_token, JWT_DURATION_MINUTES};
use apps::App;
use authorization_codes::AuthorizationCode;
use hash::Secret;
use jwt::database::Key;
use jwt::PrivateKey;
use refresh_tokens::RefreshToken;
use rocket::http::{Header, Status};
use rocket::response::Responder;
use rocket::serde::json::serde_json::json;
use rocket::serde::json::Json;
use rocket::serde::Serialize;
use rocket::time::Duration;
use rocket::{post, response, Request, Response};
use settings::Settings;
use std::net::IpAddr;
use users::User;
#[derive(Serialize)]
#[serde(crate = "rocket::serde")]
pub struct TokenResponse {
pub access_token: String,
pub token_type: &'static str,
pub refresh_token: String,
pub expires_in: i64,
pub id_token: String,
}
impl<'r> Responder<'r, 'static> for TokenResponse {
fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> {
Response::build_from(Json(&self).respond_to(req)?)
.header(Header::new("Cache-Control", "no-store"))
.header(Header::new("Pragma", "no-cache"))
.ok()
}
}
pub enum TokenError {
TransactionStart(rocket_db_pools::sqlx::Error),
TransactionCommit(rocket_db_pools::sqlx::Error),
AuthorizationCodeNotProvided,
RefreshTokenNotProvided,
AuthorizationError(authorization_codes::Error),
AuthorizationCodeNotFound(String),
RefreshTokenError(refresh_tokens::Error),
RefreshTokenNotFound(String),
RefreshTokenUsed,
RefreshTokenRevoked,
RefreshTokenExpired,
AuthorizationCodeUsed,
AuthorizationCodeExpired,
HttpAuthDifferentClientId,
AppError(apps::Error),
AppNotFound(String),
AppSecretNotProvided,
Blocking(task::JoinError),
SecretCompare(hash::Error),
AppSecretWrong,
UserError(users::Error),
UserNotFound,
UserArchived(String),
SettingsError(settings::Error),
ServerUrlNotSet,
RefreshTokenGenerate(String),
JwtError(jwt::Error),
JwkNotFound,
JwkRevoked(String),
JwkImport(jwt::Error),
JwtGenerate(String),
}
impl<'r> Responder<'r, 'static> for TokenError {
fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> {
let (status, data) = match self {
TokenError::TransactionStart(e) => (Status::InternalServerError, e.to_string()),
TokenError::TransactionCommit(e) => (Status::InternalServerError, e.to_string()),
TokenError::AuthorizationCodeNotProvided => (
Status::BadRequest,
"Authorization code is not provided".to_string(),
),
TokenError::RefreshTokenNotProvided => (
Status::BadRequest,
"Refresh token is not provided".to_string(),
),
TokenError::AuthorizationError(e) => (Status::InternalServerError, e.to_string()),
TokenError::AuthorizationCodeNotFound(e) => (
Status::NotFound,
format!("Could not find authorization code {e}"),
),
TokenError::RefreshTokenError(e) => (Status::InternalServerError, e.to_string()),
TokenError::RefreshTokenNotFound(e) => {
(Status::NotFound, format!("Refresh token {e} was not found"))
}
TokenError::RefreshTokenUsed => {
(Status::BadRequest, "Refresh token has expired".to_string())
}
TokenError::RefreshTokenRevoked => (
Status::BadRequest,
"Refresh token has been revoked".to_string(),
),
TokenError::RefreshTokenExpired => {
(Status::BadRequest, "Refresh token has expired".to_string())
}
TokenError::AuthorizationCodeUsed => (
Status::BadRequest,
"Authorization code has already been used".to_string(),
),
TokenError::AuthorizationCodeExpired => (
Status::BadRequest,
"Authorization code has expired".to_string(),
),
TokenError::HttpAuthDifferentClientId => (
Status::BadRequest,
"HTTP Auth differs from provided client_id".to_string(),
),
TokenError::AppError(e) => (Status::InternalServerError, e.to_string()),
TokenError::AppNotFound(e) => {
(Status::NotFound, format!("Could not find application {e}"))
}
TokenError::AppSecretNotProvided => {
(Status::BadRequest, "Secret was not provided".to_string())
}
TokenError::Blocking(e) => (Status::InternalServerError, e.to_string()),
TokenError::SecretCompare(e) => (
Status::InternalServerError,
format!("Failed to check app secret: {e}"),
),
TokenError::AppSecretWrong => {
(Status::Forbidden, "Invalid secret provided".to_string())
}
TokenError::UserError(e) => (Status::InternalServerError, e.to_string()),
TokenError::UserNotFound => (
Status::NotFound,
"Could not find user from authorization code".to_string(),
),
TokenError::UserArchived(e) => (Status::Forbidden, format!("User {e} is archived")),
TokenError::SettingsError(e) => (Status::InternalServerError, e.to_string()),
TokenError::ServerUrlNotSet => {
(Status::BadRequest, "Server url is not set".to_string())
}
TokenError::RefreshTokenGenerate(e) => (Status::InternalServerError, e),
TokenError::JwtError(e) => (Status::InternalServerError, e.to_string()),
TokenError::JwkNotFound => (
Status::InternalServerError,
"Failed to get key to sign JWT".to_string(),
),
TokenError::JwkRevoked(e) => (
Status::InternalServerError,
format!("Signing key {e} has been revoked"),
),
TokenError::JwkImport(e) => (Status::InternalServerError, e.to_string()),
TokenError::JwtGenerate(e) => (Status::InternalServerError, e),
};
Response::build_from(json!({ "error": data }).respond_to(req)?)
.status(status)
.header(Header::new("Cache-Control", "no-store"))
.header(Header::new("Pragma", "no-cache"))
.ok()
}
}
#[post("/oauth/token", data = "<token_request>")]
pub async fn request_token(
mut db: Connection<Database>,
token_request: Form<TokenRequest<'_>>,
app_auth: Option<BasicAuth>,
ip_address: IpAddr,
) -> std::result::Result<TokenResponse, TokenError> {
let mut transaction = db.begin().await.map_err(TokenError::TransactionStart)?;
// Get user depending on grant type
let user = match token_request.grant_type {
GrantType::AuthorizationCode => {
let authorization_code = token_request
.code
.ok_or(TokenError::AuthorizationCodeNotProvided)?;
// Get authorization code
let code = AuthorizationCode::get_one(&mut transaction, authorization_code)
.await
.map_err(TokenError::AuthorizationError)?
.ok_or(TokenError::AuthorizationCodeNotFound(
authorization_code.into(),
))?;
// Make sure code has not been used
if code.has_been_used() {
// Revoke all codes and refresh tokens for user
code.use_all_for_user(&mut transaction)
.await
.map_err(TokenError::AuthorizationError)?;
RefreshToken::revoke_all_for_user(&mut transaction, code.user())
.await
.map_err(TokenError::RefreshTokenError)?;
transaction
.commit()
.await
.map_err(TokenError::TransactionCommit)?;
return Err(TokenError::AuthorizationCodeUsed);
}
// Make sure it has not expired
if code.has_expired() {
return Err(TokenError::AuthorizationCodeExpired);
}
// Get user info
let user = User::get_one_from_authorization_code(&mut transaction, authorization_code)
.await
.map_err(TokenError::UserError)?
.ok_or(TokenError::UserNotFound)?;
// Check if user is archived
if user.is_archived() {
return Err(TokenError::UserArchived(user.id().to_string()));
}
// Mark code as used
code.use_code(&mut transaction)
.await
.map_err(TokenError::AuthorizationError)?;
user
}
GrantType::RefreshToken => {
let refresh_token = token_request
.refresh_token
.ok_or(TokenError::RefreshTokenNotProvided)?;
let user = User::get_one_from_refresh_token(&mut transaction, refresh_token)
.await
.map_err(TokenError::UserError)?
.ok_or(TokenError::UserNotFound)?;
let refresh_token = RefreshToken::get_one(&mut transaction, refresh_token)
.await
.map_err(TokenError::RefreshTokenError)?
.ok_or(TokenError::RefreshTokenNotFound(refresh_token.into()))?;
if refresh_token.has_been_used() {
// Revoke all tokens for user
RefreshToken::revoke_all_for_user(&mut transaction, refresh_token.user())
.await
.map_err(TokenError::RefreshTokenError)?;
transaction
.commit()
.await
.map_err(TokenError::TransactionCommit)?;
return Err(TokenError::RefreshTokenUsed);
}
if refresh_token.is_revoked() {
return Err(TokenError::RefreshTokenRevoked);
}
if refresh_token.has_expired() {
return Err(TokenError::RefreshTokenExpired);
}
refresh_token
.use_token(&mut transaction)
.await
.map_err(TokenError::RefreshTokenError)?;
user
}
};
// If HTTP Basic Auth is provided, verify provided client id in form
if let Some(app_auth) = &app_auth {
if app_auth.id != token_request.client_id {
return Err(TokenError::HttpAuthDifferentClientId);
}
}
// Get app
let app = App::get_one_by_id(&mut transaction, token_request.client_id)
.await
.map_err(TokenError::AppError)?
.ok_or(TokenError::AppNotFound(token_request.client_id.into()))?;
if app.is_confidential() {
let provided_secret = match (app_auth, token_request.client_secret) {
(Some(http_auth), _) => http_auth.password,
(None, Some(form)) => form.into(),
(None, None) => {
return Err(TokenError::AppSecretNotProvided);
}
};
// Get secret (can't use Secret struct directly because of non-async `compare`)
let secret = Secret::from_hash(app.secret_hashed());
// Verify secret
if !task::spawn_blocking(move || secret.compare(&provided_secret))
.await
.map_err(TokenError::Blocking)?
.map_err(TokenError::SecretCompare)?
{
return Err(TokenError::AppSecretWrong);
}
}
// Get base url
let settings = Settings::get(&mut transaction)
.await
.map_err(TokenError::SettingsError)?;
let home_page = settings
.url()
.map(String::from)
.ok_or(TokenError::ServerUrlNotSet)?;
// Generate refresh token
let refresh_token = generate_refresh_token(&mut transaction, ip_address, user.id(), app.id())
.await
.map_err(TokenError::RefreshTokenGenerate)?;
// Get latest key from database
let key = Key::get_most_recent(&mut transaction)
.await
.map_err(TokenError::JwtError)?
.ok_or(TokenError::JwkNotFound)?;
// Make sure key has not been revoked
if key.is_revoked() {
return Err(TokenError::JwkRevoked(key.key_id().to_string()));
}
// Import private key
let private_key =
task::spawn_blocking(move || PrivateKey::from_der(key.private_der(), key.key_id()))
.await
.map_err(TokenError::Blocking)?
.map_err(TokenError::JwkImport)?;
// Generate jwt
let jwt = generate_jwt(
&mut transaction,
&private_key,
&home_page,
&app.id().0,
&user,
)
.await
.map_err(TokenError::JwtGenerate)?;
transaction
.commit()
.await
.map_err(TokenError::TransactionCommit)?;
Ok(TokenResponse {
access_token: jwt.clone(),
token_type: "Bearer",
refresh_token,
expires_in: Duration::minutes(JWT_DURATION_MINUTES).whole_minutes(),
id_token: jwt,
})
}