summaryrefslogtreecommitdiff
path: root/crates/jmap/src/auth
diff options
context:
space:
mode:
Diffstat (limited to 'crates/jmap/src/auth')
-rw-r--r--crates/jmap/src/auth/authenticate.rs54
-rw-r--r--crates/jmap/src/auth/oauth/auth.rs8
-rw-r--r--crates/jmap/src/auth/oauth/mod.rs1
-rw-r--r--crates/jmap/src/auth/oauth/registration.rs156
-rw-r--r--crates/jmap/src/auth/oauth/token.rs75
5 files changed, 250 insertions, 44 deletions
diff --git a/crates/jmap/src/auth/authenticate.rs b/crates/jmap/src/auth/authenticate.rs
index d2f46b09..4e04d9ab 100644
--- a/crates/jmap/src/auth/authenticate.rs
+++ b/crates/jmap/src/auth/authenticate.rs
@@ -24,6 +24,7 @@ pub trait Authenticator: Sync + Send {
&self,
req: &HttpRequest,
session: &HttpSessionData,
+ allow_api_access: bool,
) -> impl Future<Output = trc::Result<(InFlight, Arc<AccessToken>)>> + Send;
}
@@ -32,6 +33,7 @@ impl Authenticator for Server {
&self,
req: &HttpRequest,
session: &HttpSessionData,
+ allow_api_access: bool,
) -> trc::Result<(InFlight, Arc<AccessToken>)> {
if let Some((mechanism, token)) = req.authorization() {
let access_token =
@@ -43,29 +45,24 @@ impl Authenticator for Server {
self.is_auth_allowed_soft(&session.remote_ip).await?;
// Decode the base64 encoded credentials
- if let Some((username, secret)) = base64_decode(token.as_bytes())
- .and_then(|token| String::from_utf8(token).ok())
- .and_then(|token| {
- token.split_once(':').map(|(login, secret)| {
- (login.trim().to_lowercase(), secret.to_string())
- })
- })
- {
- Credentials::Plain { username, secret }
- } else {
- return Err(trc::AuthEvent::Error
+ decode_plain_auth(token).ok_or_else(|| {
+ trc::AuthEvent::Error
.into_err()
.details("Failed to decode Basic auth request.")
.id(token.to_string())
- .caused_by(trc::location!()));
- }
+ .caused_by(trc::location!())
+ })?
} else if mechanism.eq_ignore_ascii_case("bearer") {
// Enforce anonymous rate limit
self.is_anonymous_allowed(&session.remote_ip).await?;
- Credentials::OAuthBearer {
- token: token.to_string(),
- }
+ decode_bearer_token(token, allow_api_access).ok_or_else(|| {
+ trc::AuthEvent::Error
+ .into_err()
+ .details("Failed to decode Bearer token.")
+ .id(token.to_string())
+ .caused_by(trc::location!())
+ })?
} else {
// Enforce anonymous rate limit
self.is_anonymous_allowed(&session.remote_ip).await?;
@@ -139,3 +136,28 @@ impl HttpHeaders for HttpRequest {
})
}
}
+
+fn decode_plain_auth(token: &str) -> Option<Credentials<String>> {
+ base64_decode(token.as_bytes())
+ .and_then(|token| String::from_utf8(token).ok())
+ .and_then(|token| {
+ token
+ .split_once(':')
+ .map(|(login, secret)| Credentials::Plain {
+ username: login.trim().to_lowercase(),
+ secret: secret.to_string(),
+ })
+ })
+}
+
+fn decode_bearer_token(token: &str, allow_api_access: bool) -> Option<Credentials<String>> {
+ if allow_api_access {
+ if let Some(token) = token.strip_prefix("api_") {
+ return decode_plain_auth(token);
+ }
+ }
+
+ Some(Credentials::OAuthBearer {
+ token: token.to_string(),
+ })
+}
diff --git a/crates/jmap/src/auth/oauth/auth.rs b/crates/jmap/src/auth/oauth/auth.rs
index 44179cff..a669b60b 100644
--- a/crates/jmap/src/auth/oauth/auth.rs
+++ b/crates/jmap/src/auth/oauth/auth.rs
@@ -39,6 +39,7 @@ pub struct OAuthMetadata {
pub token_endpoint: String,
pub authorization_endpoint: String,
pub device_authorization_endpoint: String,
+ pub registration_endpoint: String,
pub introspection_endpoint: String,
pub grant_types_supported: Vec<String>,
pub response_types_supported: Vec<String>,
@@ -191,7 +192,7 @@ impl OAuthApiHandler for Server {
let client_id = FormData::from_request(req, MAX_POST_LEN, session.session_id)
.await?
.remove("client_id")
- .filter(|client_id| client_id.len() < CLIENT_ID_MAX_LEN)
+ .filter(|client_id| client_id.len() <= CLIENT_ID_MAX_LEN)
.ok_or_else(|| {
trc::ResourceEvent::BadParameters
.into_err()
@@ -277,12 +278,14 @@ impl OAuthApiHandler for Server {
Ok(JsonResponse::new(OAuthMetadata {
authorization_endpoint: format!("{base_url}/authorize/code",),
token_endpoint: format!("{base_url}/auth/token"),
+ device_authorization_endpoint: format!("{base_url}/auth/device"),
+ introspection_endpoint: format!("{base_url}/auth/introspect"),
+ registration_endpoint: format!("{base_url}/auth/register"),
grant_types_supported: vec![
"authorization_code".to_string(),
"implicit".to_string(),
"urn:ietf:params:oauth:grant-type:device_code".to_string(),
],
- device_authorization_endpoint: format!("{base_url}/auth/device"),
response_types_supported: vec![
"code".to_string(),
"id_token".to_string(),
@@ -290,7 +293,6 @@ impl OAuthApiHandler for Server {
"id_token token".to_string(),
],
scopes_supported: vec!["openid".to_string(), "offline_access".to_string()],
- introspection_endpoint: format!("{base_url}/auth/introspect"),
issuer: base_url,
})
.into_http_response())
diff --git a/crates/jmap/src/auth/oauth/mod.rs b/crates/jmap/src/auth/oauth/mod.rs
index 14d1caa9..738ac989 100644
--- a/crates/jmap/src/auth/oauth/mod.rs
+++ b/crates/jmap/src/auth/oauth/mod.rs
@@ -12,6 +12,7 @@ use crate::api::{http::fetch_body, HttpRequest};
pub mod auth;
pub mod openid;
+pub mod registration;
pub mod token;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
diff --git a/crates/jmap/src/auth/oauth/registration.rs b/crates/jmap/src/auth/oauth/registration.rs
new file mode 100644
index 00000000..7c151fcf
--- /dev/null
+++ b/crates/jmap/src/auth/oauth/registration.rs
@@ -0,0 +1,156 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
+ */
+
+use std::future::Future;
+
+use common::{
+ auth::oauth::registration::{ClientRegistrationRequest, ClientRegistrationResponse},
+ Server,
+};
+use directory::{
+ backend::internal::{lookup::DirectoryStore, manage::ManageDirectory, PrincipalField},
+ Permission, Principal, QueryBy, Type,
+};
+use rand::{distributions::Alphanumeric, thread_rng, Rng};
+use trc::{AddContext, AuthEvent};
+
+use crate::{
+ api::{
+ http::{fetch_body, HttpSessionData, ToHttpResponse},
+ HttpRequest, HttpResponse, JsonResponse,
+ },
+ auth::{authenticate::Authenticator, rate_limit::RateLimiter},
+};
+
+use super::ErrorType;
+
+pub trait ClientRegistrationHandler: Sync + Send {
+ fn handle_oauth_registration_request(
+ &self,
+ req: &mut HttpRequest,
+ session: HttpSessionData,
+ ) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
+
+ fn validate_client_registration(
+ &self,
+ client_id: &str,
+ redirect_uri: Option<&str>,
+ account_id: u32,
+ ) -> impl Future<Output = trc::Result<Option<ErrorType>>> + Send;
+}
+impl ClientRegistrationHandler for Server {
+ async fn handle_oauth_registration_request(
+ &self,
+ req: &mut HttpRequest,
+ session: HttpSessionData,
+ ) -> trc::Result<HttpResponse> {
+ if !self.core.oauth.allow_anonymous_client_registration {
+ // Authenticate request
+ let (_, access_token) = self.authenticate_headers(req, &session, true).await?;
+
+ // Validate permissions
+ access_token.assert_has_permission(Permission::OauthClientRegistration)?;
+ } else {
+ self.is_anonymous_allowed(&session.remote_ip).await?;
+ }
+
+ // Parse request
+ let body = fetch_body(req, 20 * 1024, session.session_id).await;
+ let request = serde_json::from_slice::<ClientRegistrationRequest>(
+ body.as_deref().unwrap_or_default(),
+ )
+ .map_err(|err| {
+ trc::EventType::Resource(trc::ResourceEvent::BadParameters).from_json_error(err)
+ })?;
+
+ // Generate client ID
+ let client_id = thread_rng()
+ .sample_iter(Alphanumeric)
+ .take(20)
+ .map(|ch| char::from(ch.to_ascii_lowercase()))
+ .collect::<String>();
+ self.store()
+ .create_principal(
+ Principal::new(u32::MAX, Type::OauthClient)
+ .with_field(PrincipalField::Name, client_id.clone())
+ .with_field(PrincipalField::Urls, request.redirect_uris.clone())
+ .with_opt_field(PrincipalField::Description, request.client_name.clone())
+ .with_field(PrincipalField::Emails, request.contacts.clone())
+ .with_opt_field(PrincipalField::Picture, request.logo_uri.clone()),
+ None,
+ )
+ .await
+ .caused_by(trc::location!())?;
+
+ trc::event!(
+ Auth(AuthEvent::ClientRegistration),
+ Id = client_id.to_string(),
+ RemoteIp = session.remote_ip
+ );
+
+ Ok(JsonResponse::new(ClientRegistrationResponse {
+ client_id,
+ request,
+ ..Default::default()
+ })
+ .into_http_response())
+ }
+
+ async fn validate_client_registration(
+ &self,
+ client_id: &str,
+ redirect_uri: Option<&str>,
+ account_id: u32,
+ ) -> trc::Result<Option<ErrorType>> {
+ if !self.core.oauth.require_client_authentication {
+ return Ok(None);
+ }
+
+ // Fetch client registration
+ let found_registration = if let Some(client) = self
+ .store()
+ .query(QueryBy::Name(client_id), false)
+ .await
+ .caused_by(trc::location!())?
+ .filter(|p| p.typ() == Type::OauthClient)
+ {
+ if let Some(redirect_uri) = redirect_uri {
+ if client
+ .get_str_array(PrincipalField::Urls)
+ .unwrap_or_default()
+ .iter()
+ .any(|uri| uri == redirect_uri)
+ {
+ return Ok(None);
+ }
+ } else {
+ // Device flow does not require a redirect URI
+
+ return Ok(None);
+ }
+
+ true
+ } else {
+ false
+ };
+
+ // Check if the account is allowed to override client registration
+ if self
+ .get_cached_access_token(account_id)
+ .await
+ .caused_by(trc::location!())?
+ .has_permission(Permission::OauthClientOverride)
+ {
+ return Ok(None);
+ }
+
+ Ok(Some(if found_registration {
+ ErrorType::InvalidClient
+ } else {
+ ErrorType::InvalidRequest
+ }))
+ }
+}
diff --git a/crates/jmap/src/auth/oauth/token.rs b/crates/jmap/src/auth/oauth/token.rs
index 677028b9..73e15829 100644
--- a/crates/jmap/src/auth/oauth/token.rs
+++ b/crates/jmap/src/auth/oauth/token.rs
@@ -18,7 +18,8 @@ use crate::api::{
};
use super::{
- ErrorType, FormData, OAuthCode, OAuthResponse, OAuthStatus, TokenResponse, MAX_POST_LEN,
+ registration::ClientRegistrationHandler, ErrorType, FormData, OAuthCode, OAuthResponse,
+ OAuthStatus, TokenResponse, MAX_POST_LEN,
};
pub trait TokenHandler: Sync + Send {
@@ -80,23 +81,35 @@ impl TokenHandler for Server {
if client_id != oauth.client_id || redirect_uri != oauth.params {
TokenResponse::error(ErrorType::InvalidClient)
} else if oauth.status == OAuthStatus::Authorized {
- // Mark this token as issued
- self.core
- .storage
- .lookup
- .key_delete(format!("oauth:{code}").into_bytes())
- .await?;
+ // Validate client id
+ if let Some(error) = self
+ .validate_client_registration(
+ client_id,
+ redirect_uri.into(),
+ oauth.account_id,
+ )
+ .await?
+ {
+ TokenResponse::error(error)
+ } else {
+ // Mark this token as issued
+ self.core
+ .storage
+ .lookup
+ .key_delete(format!("oauth:{code}").into_bytes())
+ .await?;
- // Issue token
- self.issue_token(oauth.account_id, &oauth.client_id, issuer, true)
- .await
- .map(TokenResponse::Granted)
- .map_err(|err| {
- trc::AuthEvent::Error
- .into_err()
- .details(err)
- .caused_by(trc::location!())
- })?
+ // Issue token
+ self.issue_token(oauth.account_id, &oauth.client_id, issuer, true)
+ .await
+ .map(TokenResponse::Granted)
+ .map_err(|err| {
+ trc::AuthEvent::Error
+ .into_err()
+ .details(err)
+ .caused_by(trc::location!())
+ })?
+ }
} else {
TokenResponse::error(ErrorType::InvalidGrant)
}
@@ -126,15 +139,26 @@ impl TokenHandler for Server {
} else {
match oauth.status {
OAuthStatus::Authorized => {
- // Mark this token as issued
- self.core
- .storage
- .lookup
- .key_delete(format!("oauth:{device_code}").into_bytes())
- .await?;
+ if let Some(error) = self
+ .validate_client_registration(client_id, None, oauth.account_id)
+ .await?
+ {
+ TokenResponse::error(error)
+ } else {
+ // Mark this token as issued
+ self.core
+ .storage
+ .lookup
+ .key_delete(format!("oauth:{device_code}").into_bytes())
+ .await?;
- // Issue token
- self.issue_token(oauth.account_id, &oauth.client_id, issuer, true)
+ // Issue token
+ self.issue_token(
+ oauth.account_id,
+ &oauth.client_id,
+ issuer,
+ true,
+ )
.await
.map(TokenResponse::Granted)
.map_err(|err| {
@@ -143,6 +167,7 @@ impl TokenHandler for Server {
.details(err)
.caused_by(trc::location!())
})?
+ }
}
OAuthStatus::Pending => {
TokenResponse::error(ErrorType::AuthorizationPending)