summaryrefslogtreecommitdiff
path: root/crates/jmap/src/auth/oauth
diff options
context:
space:
mode:
authormdecimus <mauro@stalw.art>2024-09-30 16:57:34 +0200
committermdecimus <mauro@stalw.art>2024-09-30 16:57:34 +0200
commit6a5f963b43851e1720f17df24d58b413ea6d2926 (patch)
treee9f8bb5887b119722ee93c744fc3c169c0764375 /crates/jmap/src/auth/oauth
parent1fed40a926b7070ad59fc69cb57ee1b67d53502f (diff)
OpenID Connect implementation (closes #298)
Diffstat (limited to 'crates/jmap/src/auth/oauth')
-rw-r--r--crates/jmap/src/auth/oauth/auth.rs81
-rw-r--r--crates/jmap/src/auth/oauth/mod.rs35
-rw-r--r--crates/jmap/src/auth/oauth/openid.rs116
-rw-r--r--crates/jmap/src/auth/oauth/token.rs39
4 files changed, 214 insertions, 57 deletions
diff --git a/crates/jmap/src/auth/oauth/auth.rs b/crates/jmap/src/auth/oauth/auth.rs
index 9cb93a3f..44179cff 100644
--- a/crates/jmap/src/auth/oauth/auth.rs
+++ b/crates/jmap/src/auth/oauth/auth.rs
@@ -14,6 +14,7 @@ use common::{
Server,
};
use rand::distributions::Standard;
+use serde::Deserialize;
use serde_json::json;
use std::future::Future;
use store::{
@@ -23,12 +24,27 @@ use store::{
};
use crate::{
- api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
+ api::{
+ http::{HttpContext, HttpSessionData, ToHttpResponse},
+ HttpRequest, HttpResponse, JsonResponse,
+ },
auth::oauth::OAuthStatus,
};
use super::{DeviceAuthResponse, FormData, OAuthCode, OAuthCodeRequest, MAX_POST_LEN};
+#[derive(Debug, serde::Serialize, Deserialize)]
+pub struct OAuthMetadata {
+ pub issuer: String,
+ pub token_endpoint: String,
+ pub authorization_endpoint: String,
+ pub device_authorization_endpoint: String,
+ pub introspection_endpoint: String,
+ pub grant_types_supported: Vec<String>,
+ pub response_types_supported: Vec<String>,
+ pub scopes_supported: Vec<String>,
+}
+
pub trait OAuthApiHandler: Sync + Send {
fn handle_oauth_api_request(
&self,
@@ -39,8 +55,13 @@ pub trait OAuthApiHandler: Sync + Send {
fn handle_device_auth(
&self,
req: &mut HttpRequest,
- base_url: impl AsRef<str> + Send,
- session_id: u64,
+ session: HttpSessionData,
+ ) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
+
+ fn handle_oauth_metadata(
+ &self,
+ req: HttpRequest,
+ session: HttpSessionData,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
}
@@ -98,7 +119,7 @@ impl OAuthApiHandler for Server {
.key_set(
format!("oauth:{client_code}").into_bytes(),
value,
- self.core.jmap.oauth_expiry_auth_code.into(),
+ self.core.oauth.oauth_expiry_auth_code.into(),
)
.await?;
@@ -146,7 +167,7 @@ impl OAuthApiHandler for Server {
.key_set(
format!("oauth:{device_code}").into_bytes(),
auth_code.serialize(),
- self.core.jmap.oauth_expiry_auth_code.into(),
+ self.core.oauth.oauth_expiry_auth_code.into(),
)
.await?;
}
@@ -164,11 +185,10 @@ impl OAuthApiHandler for Server {
async fn handle_device_auth(
&self,
req: &mut HttpRequest,
- base_url: impl AsRef<str>,
- session_id: u64,
+ session: HttpSessionData,
) -> trc::Result<HttpResponse> {
// Parse form
- let client_id = FormData::from_request(req, MAX_POST_LEN, session_id)
+ 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)
@@ -215,7 +235,7 @@ impl OAuthApiHandler for Server {
.key_set(
format!("oauth:{device_code}").into_bytes(),
oauth_code.clone(),
- self.core.jmap.oauth_expiry_user_code.into(),
+ self.core.oauth.oauth_expiry_user_code.into(),
)
.await?;
@@ -226,20 +246,53 @@ impl OAuthApiHandler for Server {
.key_set(
format!("oauth:{user_code}").into_bytes(),
oauth_code,
- self.core.jmap.oauth_expiry_user_code.into(),
+ self.core.oauth.oauth_expiry_user_code.into(),
)
.await?;
// Build response
- let base_url = base_url.as_ref();
+ let base_url = HttpContext::new(&session, req)
+ .resolve_response_url(self)
+ .await;
Ok(JsonResponse::new(DeviceAuthResponse {
- verification_uri: format!("{}/authorize", base_url),
- verification_uri_complete: format!("{}/authorize/?code={}", base_url, user_code),
+ verification_uri: format!("{base_url}/authorize"),
+ verification_uri_complete: format!("{base_url}/authorize/?code={user_code}"),
device_code,
user_code,
- expires_in: self.core.jmap.oauth_expiry_user_code,
+ expires_in: self.core.oauth.oauth_expiry_user_code,
interval: 5,
})
.into_http_response())
}
+
+ async fn handle_oauth_metadata(
+ &self,
+ req: HttpRequest,
+ session: HttpSessionData,
+ ) -> trc::Result<HttpResponse> {
+ let base_url = HttpContext::new(&session, &req)
+ .resolve_response_url(self)
+ .await;
+
+ Ok(JsonResponse::new(OAuthMetadata {
+ authorization_endpoint: format!("{base_url}/authorize/code",),
+ token_endpoint: format!("{base_url}/auth/token"),
+ 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(),
+ "code token".to_string(),
+ "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 eaa8c0c1..14d1caa9 100644
--- a/crates/jmap/src/auth/oauth/mod.rs
+++ b/crates/jmap/src/auth/oauth/mod.rs
@@ -11,6 +11,7 @@ use utils::map::vec_map::VecMap;
use crate::api::{http::fetch_body, HttpRequest};
pub mod auth;
+pub mod openid;
pub mod token;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
@@ -110,6 +111,8 @@ pub struct OAuthResponse {
pub refresh_token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scope: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub id_token: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
@@ -137,38 +140,6 @@ pub enum ErrorType {
}
#[derive(Debug, Serialize, Deserialize)]
-pub struct OAuthMetadata {
- pub issuer: String,
- pub token_endpoint: String,
- pub authorization_endpoint: String,
- pub device_authorization_endpoint: String,
- pub introspection_endpoint: String,
- pub grant_types_supported: Vec<String>,
- pub response_types_supported: Vec<String>,
- pub scopes_supported: Vec<String>,
-}
-
-impl OAuthMetadata {
- pub fn new(base_url: impl AsRef<str>) -> Self {
- let base_url = base_url.as_ref();
- OAuthMetadata {
- issuer: base_url.into(),
- authorization_endpoint: format!("{base_url}/authorize/code",),
- token_endpoint: format!("{base_url}/auth/token"),
- 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(), "code token".to_string()],
- scopes_supported: vec!["offline_access".to_string()],
- introspection_endpoint: format!("{base_url}/auth/introspect"),
- }
- }
-}
-
-#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum OAuthCodeRequest {
Code {
diff --git a/crates/jmap/src/auth/oauth/openid.rs b/crates/jmap/src/auth/oauth/openid.rs
new file mode 100644
index 00000000..47825c6c
--- /dev/null
+++ b/crates/jmap/src/auth/oauth/openid.rs
@@ -0,0 +1,116 @@
+/*
+ * 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::oidc::Userinfo, AccessToken},
+ Server,
+};
+use serde::{Deserialize, Serialize};
+
+use crate::api::{
+ http::{HttpContext, HttpSessionData, ToHttpResponse},
+ HttpRequest, HttpResponse, JsonResponse,
+};
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct OpenIdMetadata {
+ pub issuer: String,
+ pub authorization_endpoint: String,
+ pub token_endpoint: String,
+ pub userinfo_endpoint: String,
+ pub jwks_uri: String,
+ pub registration_endpoint: String,
+ pub scopes_supported: Vec<String>,
+ pub response_types_supported: Vec<String>,
+ pub subject_types_supported: Vec<String>,
+ pub grant_types_supported: Vec<String>,
+ pub id_token_signing_alg_values_supported: Vec<String>,
+ pub claims_supported: Vec<String>,
+}
+
+pub trait OpenIdHandler: Sync + Send {
+ fn handle_userinfo_request(
+ &self,
+ access_token: &AccessToken,
+ ) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
+
+ fn handle_oidc_metadata(
+ &self,
+ req: HttpRequest,
+ session: HttpSessionData,
+ ) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
+}
+
+impl OpenIdHandler for Server {
+ async fn handle_userinfo_request(
+ &self,
+ access_token: &AccessToken,
+ ) -> trc::Result<HttpResponse> {
+ Ok(JsonResponse::new(Userinfo {
+ sub: Some(access_token.primary_id.to_string()),
+ name: access_token.description.clone(),
+ preferred_username: Some(access_token.name.clone()),
+ email: access_token.emails.first().cloned(),
+ email_verified: !access_token.emails.is_empty(),
+ ..Default::default()
+ })
+ .into_http_response())
+ }
+
+ async fn handle_oidc_metadata(
+ &self,
+ req: HttpRequest,
+ session: HttpSessionData,
+ ) -> trc::Result<HttpResponse> {
+ let base_url = HttpContext::new(&session, &req)
+ .resolve_response_url(self)
+ .await;
+
+ Ok(JsonResponse::new(OpenIdMetadata {
+ authorization_endpoint: format!("{base_url}/authorize/code",),
+ token_endpoint: format!("{base_url}/auth/token"),
+ userinfo_endpoint: format!("{base_url}/auth/userinfo"),
+ jwks_uri: format!("{base_url}/auth/jwks.json"),
+ registration_endpoint: format!("{base_url}/auth/register"),
+ response_types_supported: vec![
+ "code".to_string(),
+ "id_token".to_string(),
+ "id_token token".to_string(),
+ ],
+ grant_types_supported: vec![
+ "authorization_code".to_string(),
+ "implicit".to_string(),
+ "urn:ietf:params:oauth:grant-type:device_code".to_string(),
+ ],
+ scopes_supported: vec!["openid".to_string(), "offline_access".to_string()],
+ subject_types_supported: vec!["public".to_string()],
+ id_token_signing_alg_values_supported: vec![
+ "RS256".to_string(),
+ "RS384".to_string(),
+ "RS512".to_string(),
+ "ES256".to_string(),
+ "ES384".to_string(),
+ "PS256".to_string(),
+ "PS384".to_string(),
+ "PS512".to_string(),
+ "HS256".to_string(),
+ "HS384".to_string(),
+ "HS512".to_string(),
+ ],
+ claims_supported: vec![
+ "sub".to_string(),
+ "name".to_string(),
+ "preferred_username".to_string(),
+ "email".to_string(),
+ "email_verified".to_string(),
+ ],
+ issuer: base_url,
+ })
+ .into_http_response())
+ }
+}
diff --git a/crates/jmap/src/auth/oauth/token.rs b/crates/jmap/src/auth/oauth/token.rs
index 49046746..677028b9 100644
--- a/crates/jmap/src/auth/oauth/token.rs
+++ b/crates/jmap/src/auth/oauth/token.rs
@@ -12,7 +12,10 @@ use hyper::StatusCode;
use std::future::Future;
use store::write::Bincode;
-use crate::api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse};
+use crate::api::{
+ http::{HttpContext, HttpSessionData, ToHttpResponse},
+ HttpRequest, HttpResponse, JsonResponse,
+};
use super::{
ErrorType, FormData, OAuthCode, OAuthResponse, OAuthStatus, TokenResponse, MAX_POST_LEN,
@@ -22,7 +25,7 @@ pub trait TokenHandler: Sync + Send {
fn handle_token_request(
&self,
req: &mut HttpRequest,
- session_id: u64,
+ session: HttpSessionData,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
fn handle_token_introspect(
@@ -36,6 +39,7 @@ pub trait TokenHandler: Sync + Send {
&self,
account_id: u32,
client_id: &str,
+ issuer: String,
with_refresh_token: bool,
) -> impl Future<Output = trc::Result<OAuthResponse>> + Send;
}
@@ -45,14 +49,18 @@ impl TokenHandler for Server {
async fn handle_token_request(
&self,
req: &mut HttpRequest,
- session_id: u64,
+ session: HttpSessionData,
) -> trc::Result<HttpResponse> {
// Parse form
- let params = FormData::from_request(req, MAX_POST_LEN, session_id).await?;
+ let params = FormData::from_request(req, MAX_POST_LEN, session.session_id).await?;
let grant_type = params.get("grant_type").unwrap_or_default();
let mut response = TokenResponse::error(ErrorType::InvalidGrant);
+ let issuer = HttpContext::new(&session, req)
+ .resolve_response_url(self)
+ .await;
+
if grant_type.eq_ignore_ascii_case("authorization_code") {
response = if let (Some(code), Some(client_id), Some(redirect_uri)) = (
params.get("code"),
@@ -80,7 +88,7 @@ impl TokenHandler for Server {
.await?;
// Issue token
- self.issue_token(oauth.account_id, &oauth.client_id, true)
+ self.issue_token(oauth.account_id, &oauth.client_id, issuer, true)
.await
.map(TokenResponse::Granted)
.map_err(|err| {
@@ -126,7 +134,7 @@ impl TokenHandler for Server {
.await?;
// Issue token
- self.issue_token(oauth.account_id, &oauth.client_id, true)
+ self.issue_token(oauth.account_id, &oauth.client_id, issuer, true)
.await
.map(TokenResponse::Granted)
.map_err(|err| {
@@ -156,8 +164,9 @@ impl TokenHandler for Server {
.issue_token(
token_info.account_id,
&token_info.client_id,
+ issuer,
token_info.expires_in
- <= self.core.jmap.oauth_expiry_refresh_token_renew,
+ <= self.core.oauth.oauth_expiry_refresh_token_renew,
)
.await
.map(TokenResponse::Granted)
@@ -171,7 +180,7 @@ impl TokenHandler for Server {
trc::error!(err
.caused_by(trc::location!())
.details("Failed to validate refresh token")
- .span_id(session_id));
+ .span_id(session.session_id));
TokenResponse::error(ErrorType::InvalidGrant)
}
};
@@ -216,6 +225,7 @@ impl TokenHandler for Server {
&self,
account_id: u32,
client_id: &str,
+ issuer: String,
with_refresh_token: bool,
) -> trc::Result<OAuthResponse> {
Ok(OAuthResponse {
@@ -224,23 +234,30 @@ impl TokenHandler for Server {
GrantType::AccessToken,
account_id,
client_id,
- self.core.jmap.oauth_expiry_token,
+ self.core.oauth.oauth_expiry_token,
)
.await?,
token_type: "bearer".to_string(),
- expires_in: self.core.jmap.oauth_expiry_token,
+ expires_in: self.core.oauth.oauth_expiry_token,
refresh_token: if with_refresh_token {
self.encode_access_token(
GrantType::RefreshToken,
account_id,
client_id,
- self.core.jmap.oauth_expiry_refresh_token,
+ self.core.oauth.oauth_expiry_refresh_token,
)
.await?
.into()
} else {
None
},
+ id_token: match self.issue_id_token(account_id.to_string(), issuer, client_id) {
+ Ok(id_token) => Some(id_token),
+ Err(err) => {
+ trc::error!(err);
+ None
+ }
+ },
scope: None,
})
}