ezidam: oauth: added token endpoint, handling "authorization_code" and "refresh_token" grants
This commit is contained in:
parent
dd69fc99ea
commit
fe24825c3b
5 changed files with 402 additions and 1 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))))
|
||||
}
|
||||
|
|
|
|||
365
crates/ezidam/src/routes/oauth/token.rs
Normal file
365
crates/ezidam/src/routes/oauth/token.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue