Login screen and api endpoint reorganization

This commit is contained in:
Maurus Decimus
2026-04-05 19:57:23 +02:00
parent eecec0aa31
commit fde37b670f
29 changed files with 1005 additions and 1606 deletions

View File

@@ -6,3 +6,4 @@
pub mod authenticate;
pub mod oauth;
pub mod permissions;

View File

@@ -5,7 +5,7 @@
*/
use super::{DeviceAuthResponse, FormData, MAX_POST_LEN, OAuthCode};
use crate::auth::oauth::OAuthStatus;
use crate::auth::oauth::{OAuthStatus, openid::OpenIdHandler};
use common::{
KV_OAUTH, Server,
auth::{
@@ -46,22 +46,29 @@ pub struct OAuthMetadata {
}
pub trait OAuthApiHandler: Sync + Send {
fn handle_discover_request(
&self,
req: &HttpRequest,
session: &HttpSessionData,
account_name: &str,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
fn handle_login_request(
&self,
session: HttpSessionData,
session: &HttpSessionData,
body: Vec<u8>,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
fn handle_device_auth(
&self,
req: &mut HttpRequest,
session: HttpSessionData,
session: &HttpSessionData,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
fn handle_oauth_metadata(
&self,
req: HttpRequest,
session: HttpSessionData,
session: &HttpSessionData,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
}
@@ -69,9 +76,6 @@ pub trait OAuthApiHandler: Sync + Send {
#[serde(tag = "type")]
#[serde(rename_all = "camelCase")]
pub enum LoginRequest {
Discovery {
account_name: String,
},
AuthCode {
account_name: String,
account_secret: String,
@@ -83,6 +87,14 @@ pub enum LoginRequest {
redirect_uri: Option<String>,
#[serde(default)]
nonce: Option<String>,
#[serde(default)]
scope: Option<String>,
#[serde(default)]
code_challenge: Option<String>,
#[serde(default)]
code_challenge_method: Option<String>,
#[serde(default)]
state: Option<String>,
},
AuthDevice {
account_name: String,
@@ -98,8 +110,6 @@ pub enum LoginRequest {
#[serde(tag = "type")]
#[serde(rename_all = "camelCase")]
pub enum LoginResponse {
Local,
External { endpoint: String },
Authenticated { client_code: String },
Verified,
MfaRequired,
@@ -107,9 +117,28 @@ pub enum LoginResponse {
}
impl OAuthApiHandler for Server {
async fn handle_discover_request(
&self,
req: &HttpRequest,
session: &HttpSessionData,
account_name: &str,
) -> trc::Result<HttpResponse> {
let account_name = account_name.trim().to_lowercase();
if let Some(domain_name) = account_name.try_domain_part()
&& let Some(endpoint) = self
.get_directory_for_domain(domain_name)
.await?
.and_then(|directory| directory.oidc_discovery_document())
{
Ok(JsonResponse::new(endpoint).no_cache().into_http_response())
} else {
self.handle_oidc_metadata(req, session).await
}
}
async fn handle_login_request(
&self,
session: HttpSessionData,
session: &HttpSessionData,
body: Vec<u8>,
) -> trc::Result<HttpResponse> {
let request = serde_json::from_slice::<LoginRequest>(&body).map_err(|err| {
@@ -117,22 +146,6 @@ impl OAuthApiHandler for Server {
})?;
let response = match request {
LoginRequest::Discovery { account_name } => {
let account_name = account_name.trim().to_lowercase();
if let Some(domain_name) = account_name.try_domain_part() {
if let Some(endpoint) = self
.get_directory_for_domain(domain_name)
.await?
.and_then(|directory| directory.oidc_authorization_endpoint())
{
LoginResponse::External { endpoint }
} else {
LoginResponse::Local
}
} else {
LoginResponse::Local
}
}
LoginRequest::AuthCode {
account_name,
account_secret,
@@ -140,6 +153,7 @@ impl OAuthApiHandler for Server {
client_id,
redirect_uri,
nonce,
..
} => {
// Validate clientId
if client_id.len() > CLIENT_ID_MAX_LEN {
@@ -315,7 +329,7 @@ impl OAuthApiHandler for Server {
async fn handle_device_auth(
&self,
req: &mut HttpRequest,
session: HttpSessionData,
session: &HttpSessionData,
) -> trc::Result<HttpResponse> {
// Parse form
let mut form_data = FormData::from_request(req, MAX_POST_LEN, session.session_id).await?;
@@ -379,10 +393,10 @@ impl OAuthApiHandler for Server {
.await?;
// Build response
let base_url = HttpContext::new(&session, req).resolve_response_url(self);
let base_url = HttpContext::new(session, req).resolve_response_url(self);
Ok(JsonResponse::new(DeviceAuthResponse {
verification_uri: format!("{base_url}/authorize"),
verification_uri_complete: format!("{base_url}/authorize/?code={user_code}"),
verification_uri: format!("{base_url}/device"),
verification_uri_complete: format!("{base_url}/device/?code={user_code}"),
device_code,
user_code,
expires_in: self.core.oauth.oauth_expiry_user_code,
@@ -395,12 +409,12 @@ impl OAuthApiHandler for Server {
async fn handle_oauth_metadata(
&self,
req: HttpRequest,
session: HttpSessionData,
session: &HttpSessionData,
) -> trc::Result<HttpResponse> {
let base_url = HttpContext::new(&session, &req).resolve_response_url(self);
let base_url = HttpContext::new(session, &req).resolve_response_url(self);
Ok(JsonResponse::new(OAuthMetadata {
authorization_endpoint: format!("{base_url}/authorize/code",),
authorization_endpoint: format!("{base_url}/login",),
token_endpoint: format!("{base_url}/auth/token"),
device_authorization_endpoint: format!("{base_url}/auth/device"),
introspection_endpoint: format!("{base_url}/auth/introspect"),

View File

@@ -34,8 +34,8 @@ pub trait OpenIdHandler: Sync + Send {
fn handle_oidc_metadata(
&self,
req: HttpRequest,
session: HttpSessionData,
req: &HttpRequest,
session: &HttpSessionData,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
}
@@ -57,10 +57,10 @@ impl OpenIdHandler for Server {
async fn handle_oidc_metadata(
&self,
req: HttpRequest,
session: HttpSessionData,
req: &HttpRequest,
session: &HttpSessionData,
) -> trc::Result<HttpResponse> {
let base_url = HttpContext::new(&session, &req).resolve_response_url(self);
let base_url = HttpContext::new(session, req).resolve_response_url(self);
Ok(JsonResponse::new(OpenIdMetadata {
authorization_endpoint: format!("{base_url}/authorize/code",),

View File

@@ -0,0 +1,47 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::{Server, auth::AccessToken};
use http_proto::{HttpResponse, JsonResponse, ToHttpResponse};
use registry::schema::enums::Permission;
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
pub struct Permissions {
pub permissions: Vec<Permission>,
#[serde(rename = "isEnterprise")]
pub is_enterprise: bool,
}
pub trait PermissionsApiHandler: Sync + Send {
fn handle_permissions_request(
&self,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
}
impl PermissionsApiHandler for Server {
async fn handle_permissions_request(
&self,
access_token: &AccessToken,
) -> trc::Result<HttpResponse> {
#[cfg(not(feature = "enterprise"))]
let is_enterprise = false;
// SPDX-SnippetBegin
// SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
// SPDX-License-Identifier: LicenseRef-SEL
#[cfg(feature = "enterprise")]
let is_enterprise = self.core.is_enterprise_edition();
// SPDX-SnippetEnd
Ok(JsonResponse::new(Permissions {
permissions: access_token.permissions(),
is_enterprise,
})
.into_http_response())
}
}