2024-11-09 19:57:15 +01:00
|
|
|
use axum::{extract::{Query, State}, http::{HeaderMap, HeaderValue, StatusCode}, response::{Html, IntoResponse}, Extension, Form};
|
|
|
|
|
use chrono::{SecondsFormat, Utc};
|
2024-11-08 23:38:54 +01:00
|
|
|
use fully_pub::fully_pub;
|
2024-11-09 19:57:15 +01:00
|
|
|
use log::{debug, error, info};
|
2024-10-21 00:05:20 +02:00
|
|
|
use minijinja::context;
|
2024-11-09 19:57:15 +01:00
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
use url::Url;
|
|
|
|
|
use uuid::Uuid;
|
2024-10-21 00:05:20 +02:00
|
|
|
|
2024-12-12 01:12:40 +01:00
|
|
|
use kernel::models::{authorization::Authorization, config::AppAuthorizeFlow};
|
2024-11-28 12:47:00 +01:00
|
|
|
use utils::get_random_alphanumerical;
|
2024-11-11 14:49:17 +01:00
|
|
|
use crate::{
|
2024-11-28 12:47:00 +01:00
|
|
|
renderer::TemplateRenderer, services::oauth2::{parse_scope, verify_redirect_uri}, token_claims::UserTokenClaims, AppState
|
2024-11-11 14:49:17 +01:00
|
|
|
};
|
2024-10-21 00:05:20 +02:00
|
|
|
|
2024-11-28 12:47:00 +01:00
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
2024-11-08 23:38:54 +01:00
|
|
|
#[fully_pub]
|
2024-11-11 14:49:17 +01:00
|
|
|
/// query params described in [RFC6749 section 4.1.1](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1)
|
2024-11-09 19:57:15 +01:00
|
|
|
struct AuthorizationParams {
|
2024-11-08 23:38:54 +01:00
|
|
|
response_type: String,
|
|
|
|
|
client_id: String,
|
|
|
|
|
scope: String,
|
|
|
|
|
redirect_uri: String,
|
|
|
|
|
/// An opaque value used by the client to maintain state between the request and callback
|
|
|
|
|
state: String,
|
2024-12-12 01:12:40 +01:00
|
|
|
nonce: Option<String>
|
2024-11-08 23:38:54 +01:00
|
|
|
}
|
2024-10-21 00:05:20 +02:00
|
|
|
|
2024-11-09 19:57:15 +01:00
|
|
|
fn redirect_to_client(
|
|
|
|
|
authorization_code: &str,
|
|
|
|
|
authorization_params: &AuthorizationParams
|
|
|
|
|
) -> impl IntoResponse {
|
|
|
|
|
let target_url = format!("{}?code={}&state={}",
|
|
|
|
|
authorization_params.redirect_uri,
|
|
|
|
|
authorization_code,
|
2024-12-12 01:12:40 +01:00
|
|
|
authorization_params.state
|
2024-11-09 19:57:15 +01:00
|
|
|
);
|
|
|
|
|
debug!("Redirecting to {}", target_url);
|
|
|
|
|
|
|
|
|
|
let mut headers = HeaderMap::new();
|
|
|
|
|
headers.insert("Location", HeaderValue::from_str(&target_url).unwrap());
|
|
|
|
|
(
|
|
|
|
|
StatusCode::FOUND,
|
|
|
|
|
headers,
|
|
|
|
|
Html("Redirecting to client…")
|
|
|
|
|
).into_response()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The authorization endpoint is used to interact with the resource owner and obtain an authorization grant.
|
|
|
|
|
/// https://datatracker.ietf.org/doc/html/rfc6749#section-3.1
|
2024-10-21 00:05:20 +02:00
|
|
|
pub async fn authorize_form(
|
2024-11-02 17:37:57 +01:00
|
|
|
State(app_state): State<AppState>,
|
2024-11-11 14:49:17 +01:00
|
|
|
Extension(token_claims): Extension<UserTokenClaims>,
|
2024-11-08 23:38:54 +01:00
|
|
|
Extension(renderer): Extension<TemplateRenderer>,
|
2024-11-09 19:57:15 +01:00
|
|
|
query_params: Query<AuthorizationParams>
|
2024-10-21 00:05:20 +02:00
|
|
|
) -> impl IntoResponse {
|
2024-11-09 19:57:15 +01:00
|
|
|
let Query(authorization_params) = query_params;
|
2024-12-09 09:38:39 +01:00
|
|
|
dbg!(&authorization_params);
|
2024-11-09 19:57:15 +01:00
|
|
|
|
2024-11-08 23:38:54 +01:00
|
|
|
// 1. Verify the app details
|
2024-11-09 19:57:15 +01:00
|
|
|
let app = match app_state.config.applications
|
2024-11-08 23:38:54 +01:00
|
|
|
.iter()
|
2024-11-09 19:57:15 +01:00
|
|
|
.find(|a| a.client_id == authorization_params.client_id) {
|
|
|
|
|
Some(app) => app,
|
|
|
|
|
None => {
|
|
|
|
|
return (
|
|
|
|
|
StatusCode::BAD_REQUEST,
|
|
|
|
|
Html("Invalid client_id query params, app not found.")
|
|
|
|
|
).into_response();
|
|
|
|
|
}
|
|
|
|
|
};
|
2024-11-08 23:38:54 +01:00
|
|
|
|
2024-11-09 19:57:15 +01:00
|
|
|
// 1.1. Verify that the app redirect_uri is authorized
|
|
|
|
|
if !verify_redirect_uri(app, &authorization_params.redirect_uri) {
|
2024-11-08 23:38:54 +01:00
|
|
|
return (
|
|
|
|
|
StatusCode::BAD_REQUEST,
|
2024-11-09 19:57:15 +01:00
|
|
|
Html("Unauthorized redirect_uri.")
|
2024-11-08 23:38:54 +01:00
|
|
|
).into_response();
|
|
|
|
|
}
|
2024-11-09 19:57:15 +01:00
|
|
|
// 1.3. Parse and validate redirect_uri.
|
|
|
|
|
// Note: for now, we only support HTTP(s) redirect URLs.
|
|
|
|
|
let parsed_redirect_uri: Url = match Url::parse(&authorization_params.redirect_uri) {
|
|
|
|
|
Ok(url) => url,
|
|
|
|
|
Err(_) => {
|
|
|
|
|
return (
|
|
|
|
|
StatusCode::BAD_REQUEST,
|
|
|
|
|
Html("Invalid redirect URL. Could not parse as HTTP(S) URL.")
|
|
|
|
|
).into_response();
|
|
|
|
|
}
|
|
|
|
|
};
|
2024-11-11 23:16:50 +01:00
|
|
|
// 1.4. Parse and validate scopes
|
|
|
|
|
let scopes = match parse_scope(&authorization_params.scope) {
|
|
|
|
|
Ok(v) => v,
|
|
|
|
|
Err(_err) => {
|
|
|
|
|
return (
|
|
|
|
|
StatusCode::BAD_REQUEST,
|
|
|
|
|
Html("Invalid scopes. Scopes must be space-delimited and snake_case.")
|
|
|
|
|
).into_response();
|
|
|
|
|
}
|
|
|
|
|
};
|
2024-11-14 13:35:46 +01:00
|
|
|
|
2024-11-08 23:38:54 +01:00
|
|
|
// 2. Check if the app is already authorized
|
2024-11-09 19:57:15 +01:00
|
|
|
let authorizations_res = sqlx::query_as::<_, Authorization>(
|
2024-11-11 23:16:50 +01:00
|
|
|
"SELECT * FROM authorizations WHERE user_id = $1 AND client_id = $2 AND scopes = $3"
|
2024-11-09 19:57:15 +01:00
|
|
|
)
|
2024-11-08 23:38:54 +01:00
|
|
|
.bind(&token_claims.sub)
|
2024-11-09 19:57:15 +01:00
|
|
|
.bind(&authorization_params.client_id)
|
2024-11-11 23:16:50 +01:00
|
|
|
.bind(sqlx::types::Json(&scopes))
|
2024-11-28 12:47:00 +01:00
|
|
|
.fetch_one(&app_state.db.0)
|
2024-11-09 19:57:15 +01:00
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
match authorizations_res {
|
|
|
|
|
Ok(existing_authorization) => {
|
2024-11-11 14:49:17 +01:00
|
|
|
info!("Reusing existing authorization: {}", &existing_authorization.id);
|
2024-11-09 19:57:15 +01:00
|
|
|
|
2024-11-11 14:49:17 +01:00
|
|
|
// Create new auth code
|
|
|
|
|
let authorization_code = get_random_alphanumerical(32);
|
2024-11-09 19:57:15 +01:00
|
|
|
// Update last used timestamp for this authorization
|
2024-12-12 01:12:40 +01:00
|
|
|
let _result = sqlx::query("UPDATE authorizations SET code = $2, nonce = $3, last_used_at = $4 WHERE id = $1")
|
2024-11-09 19:57:15 +01:00
|
|
|
.bind(existing_authorization.id)
|
2024-11-11 14:49:17 +01:00
|
|
|
.bind(authorization_code.clone())
|
2024-12-12 01:12:40 +01:00
|
|
|
.bind(authorization_params.nonce.clone())
|
2024-11-09 19:57:15 +01:00
|
|
|
.bind(Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true))
|
2024-11-28 12:47:00 +01:00
|
|
|
.execute(&app_state.db.0)
|
2024-11-09 19:57:15 +01:00
|
|
|
.await.unwrap();
|
|
|
|
|
|
|
|
|
|
// Authorization already given, just redirect to the app
|
|
|
|
|
return redirect_to_client(
|
2024-11-11 14:49:17 +01:00
|
|
|
&authorization_code,
|
2024-11-09 19:57:15 +01:00
|
|
|
&authorization_params
|
|
|
|
|
).into_response()
|
|
|
|
|
},
|
|
|
|
|
Err(sqlx::Error::RowNotFound) => {
|
|
|
|
|
debug!("Authorization not found.");
|
|
|
|
|
},
|
|
|
|
|
Err(err) => {
|
|
|
|
|
error!("Cannot get existing authorization. {}", err);
|
|
|
|
|
return (
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
Html("Internal server error: Failed to verify conditions.")
|
|
|
|
|
).into_response();
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-11-08 23:38:54 +01:00
|
|
|
|
2024-11-14 13:35:46 +01:00
|
|
|
// 3. Check for implicit/explicit flow
|
|
|
|
|
if app.authorize_flow == AppAuthorizeFlow::Implicit {
|
|
|
|
|
debug!("Performing Implicit authorization flow.");
|
|
|
|
|
// Authorization already given, just redirect to the app
|
|
|
|
|
return perform_authorize(
|
|
|
|
|
State(app_state),
|
|
|
|
|
Extension(token_claims),
|
|
|
|
|
Form(authorization_params)
|
|
|
|
|
).await.into_response()
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-08 23:38:54 +01:00
|
|
|
// 4. Show form that POST to authorize
|
2024-11-14 13:35:46 +01:00
|
|
|
debug!("Performing explicit authorization flow.");
|
2024-11-08 23:38:54 +01:00
|
|
|
renderer
|
|
|
|
|
.render(
|
|
|
|
|
"pages/authorize",
|
2024-11-09 19:57:15 +01:00
|
|
|
context!(
|
|
|
|
|
app => app,
|
|
|
|
|
authorization_params => authorization_params,
|
|
|
|
|
redirect_uri_host => parsed_redirect_uri.host_str()
|
|
|
|
|
)
|
2024-11-08 23:38:54 +01:00
|
|
|
)
|
|
|
|
|
.into_response()
|
|
|
|
|
}
|
2024-11-02 17:37:57 +01:00
|
|
|
|
2024-10-21 00:05:20 +02:00
|
|
|
|
2024-11-08 23:38:54 +01:00
|
|
|
pub async fn perform_authorize(
|
|
|
|
|
State(app_state): State<AppState>,
|
2024-11-11 14:49:17 +01:00
|
|
|
Extension(token_claims): Extension<UserTokenClaims>,
|
2024-11-09 19:57:15 +01:00
|
|
|
Form(authorize_form): Form<AuthorizationParams>
|
2024-11-08 23:38:54 +01:00
|
|
|
) -> impl IntoResponse {
|
2024-12-12 01:12:40 +01:00
|
|
|
dbg!(&authorize_form);
|
2024-11-09 19:57:15 +01:00
|
|
|
// 1. Get the app details
|
|
|
|
|
let app = match app_state.config.applications
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|a| a.client_id == authorize_form.client_id) {
|
|
|
|
|
Some(app) => app,
|
|
|
|
|
None => {
|
|
|
|
|
return (
|
|
|
|
|
StatusCode::BAD_REQUEST,
|
|
|
|
|
Html("Invalid client_id, app not found.")
|
|
|
|
|
).into_response();
|
|
|
|
|
}
|
|
|
|
|
};
|
2024-11-11 23:16:50 +01:00
|
|
|
// 1.2. Parse and validate scope to use in DB
|
2024-11-11 14:49:17 +01:00
|
|
|
let scopes = match parse_scope(&authorize_form.scope) {
|
|
|
|
|
Ok(v) => v,
|
2024-11-11 23:16:50 +01:00
|
|
|
Err(_err) => {
|
2024-11-11 14:49:17 +01:00
|
|
|
return (
|
|
|
|
|
StatusCode::BAD_REQUEST,
|
2024-11-11 23:16:50 +01:00
|
|
|
Html("Invalid scopes.")
|
2024-11-11 14:49:17 +01:00
|
|
|
).into_response();
|
|
|
|
|
}
|
|
|
|
|
};
|
2024-11-11 23:16:50 +01:00
|
|
|
// 2. Create an authorization code
|
2024-11-09 19:57:15 +01:00
|
|
|
let authorization_code = get_random_alphanumerical(32);
|
|
|
|
|
|
|
|
|
|
let authorization = Authorization {
|
|
|
|
|
id: Uuid::new_v4().to_string(),
|
|
|
|
|
user_id: token_claims.sub,
|
|
|
|
|
client_id: app.client_id.clone(),
|
2024-11-11 14:49:17 +01:00
|
|
|
scopes: sqlx::types::Json(scopes),
|
2024-11-09 19:57:15 +01:00
|
|
|
code: authorization_code.clone(),
|
2024-12-12 01:12:40 +01:00
|
|
|
nonce: authorize_form.nonce.clone(),
|
2024-11-09 19:57:15 +01:00
|
|
|
last_used_at: Some(Utc::now()),
|
|
|
|
|
created_at: Utc::now(),
|
|
|
|
|
};
|
2024-11-11 14:49:17 +01:00
|
|
|
|
2024-11-09 19:57:15 +01:00
|
|
|
// 3. Save authorization in DB with state
|
|
|
|
|
let res = sqlx::query("
|
|
|
|
|
INSERT INTO authorizations
|
2024-12-12 01:12:40 +01:00
|
|
|
(id, user_id, client_id, scopes, code, nonce, last_used_at, created_at)
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
2024-11-09 19:57:15 +01:00
|
|
|
")
|
|
|
|
|
.bind(authorization.id.clone())
|
|
|
|
|
.bind(authorization.user_id)
|
|
|
|
|
.bind(authorization.client_id)
|
|
|
|
|
.bind(authorization.scopes)
|
|
|
|
|
.bind(authorization.code)
|
2024-12-12 01:12:40 +01:00
|
|
|
.bind(authorization.nonce)
|
2024-11-09 19:57:15 +01:00
|
|
|
.bind(authorization.last_used_at.map(|x| x.to_rfc3339_opts(SecondsFormat::Millis, true)))
|
|
|
|
|
.bind(authorization.created_at.to_rfc3339_opts(SecondsFormat::Millis, true))
|
2024-11-28 12:47:00 +01:00
|
|
|
.execute(&app_state.db.0)
|
2024-11-09 19:57:15 +01:00
|
|
|
.await;
|
2024-11-12 14:29:27 +01:00
|
|
|
if let Err(err) = res {
|
|
|
|
|
error!("Failed to save authorization in DB. {}", err);
|
|
|
|
|
return (StatusCode::INTERNAL_SERVER_ERROR, Html("Internal server error: Failed to process authorization form.")).into_response();
|
2024-11-09 19:57:15 +01:00
|
|
|
}
|
|
|
|
|
info!("Created authorization {}", &authorization.id);
|
|
|
|
|
|
|
|
|
|
// 4. Redirect to the app with the authorization code and state
|
|
|
|
|
redirect_to_client(&authorization_code, &authorize_form).into_response()
|
2024-11-08 23:38:54 +01:00
|
|
|
}
|