From 05ab1ec3a9ebc514cc9a57c300d42dc219121bf9 Mon Sep 17 00:00:00 2001 From: Adis Veletanlic Date: Mon, 14 Apr 2025 12:21:00 +0200 Subject: [PATCH] Add private_key_file option for apple sso and edit docs --- crates/config/src/sections/upstream_oauth2.rs | 13 ++- .../handlers/src/upstream_oauth2/callback.rs | 2 +- crates/handlers/src/upstream_oauth2/mod.rs | 92 ++++++++++++++++--- docs/config.schema.json | 61 ++++++++---- docs/setup/sso.md | 19 ++-- 5 files changed, 143 insertions(+), 44 deletions(-) diff --git a/crates/config/src/sections/upstream_oauth2.rs b/crates/config/src/sections/upstream_oauth2.rs index 98b5f3c3c..ee184f726 100644 --- a/crates/config/src/sections/upstream_oauth2.rs +++ b/crates/config/src/sections/upstream_oauth2.rs @@ -10,6 +10,7 @@ use mas_iana::jose::JsonWebSignatureAlg; use schemars::JsonSchema; use serde::{Deserialize, Serialize, de::Error}; use serde_with::skip_serializing_none; +use camino::Utf8PathBuf; use ulid::Ulid; use url::Url; @@ -383,15 +384,21 @@ fn signed_response_alg_default() -> JsonWebSignatureAlg { #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct SignInWithApple { + /// The private key file used to sign the `id_token` + #[serde(skip_serializing_if = "Option::is_none")] + #[schemars(with = "Option")] + pub private_key_file: Option, + /// The private key used to sign the `id_token` - pub private_key: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub private_key: Option, /// The Team ID of the Apple Developer Portal pub team_id: String, /// The key ID of the Apple Developer Portal pub key_id: String, -} + } /// Configuration for one upstream OAuth 2 provider. #[skip_serializing_none] @@ -558,4 +565,4 @@ pub struct Provider { /// Specify `oidc` here. #[serde(skip_serializing_if = "Option::is_none")] pub synapse_idp_id: Option, -} +} \ No newline at end of file diff --git a/crates/handlers/src/upstream_oauth2/callback.rs b/crates/handlers/src/upstream_oauth2/callback.rs index be4b5a2d1..845ae1b3c 100644 --- a/crates/handlers/src/upstream_oauth2/callback.rs +++ b/crates/handlers/src/upstream_oauth2/callback.rs @@ -294,7 +294,7 @@ pub(crate) async fn handler( lazy_metadata.token_endpoint().await?, &keystore, &encrypter, - )?; + ).await?; let redirect_uri = url_builder.upstream_oauth_callback(provider.id); diff --git a/crates/handlers/src/upstream_oauth2/mod.rs b/crates/handlers/src/upstream_oauth2/mod.rs index c387aca1b..60360562f 100644 --- a/crates/handlers/src/upstream_oauth2/mod.rs +++ b/crates/handlers/src/upstream_oauth2/mod.rs @@ -11,6 +11,8 @@ use mas_iana::jose::JsonWebSignatureAlg; use mas_keystore::{DecryptError, Encrypter, Keystore}; use mas_oidc_client::types::client_credentials::ClientCredentials; use pkcs8::DecodePrivateKey; +use schemars::JsonSchema; +use camino::Utf8PathBuf; use serde::Deserialize; use thiserror::Error; use url::Url; @@ -30,6 +32,9 @@ enum ProviderCredentialsError { #[error("Provider doesn't have a client secret")] MissingClientSecret, + #[error("Missing private key for signing the id_token")] + MissingPrivateKey, + #[error("Could not decrypt client secret")] DecryptClientSecret { #[from] @@ -55,14 +60,37 @@ enum ProviderCredentialsError { }, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Error)] +enum AppleCredentialsError { + #[error("Missing private key for signing the id_token")] + MissingPrivateKey, + + #[error("Duplicate private key for signing the id_token")] + DuplicatePrivateKey, + + #[error(transparent)] + InvalidPrivateKey(#[from] pkcs8::Error), +} + +#[derive(Debug, Deserialize, JsonSchema)] pub struct SignInWithApple { - pub private_key: String, + /// The private key file used to sign the `id_token` + #[serde(skip_serializing_if = "Option::is_none")] + #[schemars(with = "Option")] + pub private_key_file: Option, + + /// The private key used to sign the `id_token` + #[serde(skip_serializing_if = "Option::is_none")] + pub private_key: Option, + + /// The Team ID of the Apple Developer Portal pub team_id: String, + + /// The key ID of the Apple Developer Portal pub key_id: String, } -fn client_credentials_for_provider( +async fn client_credentials_for_provider( provider: &UpstreamOAuthProvider, token_endpoint: &Url, keystore: &Keystore, @@ -70,7 +98,6 @@ fn client_credentials_for_provider( ) -> Result { let client_id = provider.client_id.clone(); - // Decrypt the client secret let client_secret = provider .encrypted_client_secret .as_deref() @@ -124,19 +151,54 @@ fn client_credentials_for_provider( }, UpstreamOAuthProviderTokenAuthMethod::SignInWithApple => { - let params = client_secret.ok_or(ProviderCredentialsError::MissingClientSecret)?; - let params: SignInWithApple = serde_json::from_str(¶ms)?; - - let key = elliptic_curve::SecretKey::from_pkcs8_pem(¶ms.private_key)?; - - ClientCredentials::SignInWithApple { - client_id, - key, - key_id: params.key_id, - team_id: params.team_id, - } + let client_secret = client_secret.ok_or(ProviderCredentialsError::MissingClientSecret)?; + + resolve_apple_credentials(client_id, client_secret) + .await + .map_err(|err| { + match err { + AppleCredentialsError::MissingPrivateKey => ProviderCredentialsError::MissingPrivateKey, + AppleCredentialsError::DuplicatePrivateKey => ProviderCredentialsError::MissingPrivateKey, // maybe define a better one later + AppleCredentialsError::InvalidPrivateKey(inner) => ProviderCredentialsError::InvalidPrivateKey { inner }, + } + })? } }; Ok(client_credentials) } + +async fn resolve_apple_credentials( + client_id: String, + client_secret: String, +) -> Result { + let params: SignInWithApple = serde_json::from_str(&client_secret) + .map_err(|_| AppleCredentialsError::MissingPrivateKey)?; + + if params.private_key.is_none() && params.private_key_file.is_none() { + return Err(AppleCredentialsError::MissingPrivateKey); + } + + if params.private_key.is_some() && params.private_key_file.is_some() { + return Err(AppleCredentialsError::DuplicatePrivateKey); + } + + let private_key_pem = if let Some(private_key) = params.private_key { + private_key + } else if let Some(private_key_file) = params.private_key_file { + tokio::fs::read_to_string(private_key_file) + .await + .map_err(|_| AppleCredentialsError::MissingPrivateKey)? + } else { + unreachable!("already validated") + }; + + let key = elliptic_curve::SecretKey::from_pkcs8_pem(&private_key_pem)?; + + Ok(ClientCredentials::SignInWithApple { + client_id, + key, + key_id: params.key_id, + team_id: params.team_id, + }) +} \ No newline at end of file diff --git a/docs/config.schema.json b/docs/config.schema.json index e49a75754..bd69b42bd 100644 --- a/docs/config.schema.json +++ b/docs/config.schema.json @@ -2156,25 +2156,50 @@ }, "SignInWithApple": { "type": "object", - "required": [ - "key_id", - "private_key", - "team_id" - ], - "properties": { - "private_key": { - "description": "The private key used to sign the `id_token`", - "type": "string" + "oneOf": [ + { + "required": [ + "key_id", + "private_key_file", + "team_id" + ], + "properties": { + "private_key_file": { + "description": "The private key file used to sign the `id_token`", + "type": "string" + }, + "team_id": { + "description": "The Team ID of the Apple Developer Portal", + "type": "string" + }, + "key_id": { + "description": "The key ID of the Apple Developer Portal", + "type": "string" + } + } }, - "team_id": { - "description": "The Team ID of the Apple Developer Portal", - "type": "string" - }, - "key_id": { - "description": "The key ID of the Apple Developer Portal", - "type": "string" + { + "required": [ + "key_id", + "private_key", + "team_id" + ], + "properties": { + "private_key": { + "description": "The private key used to sign the `id_token`", + "type": "string" + }, + "team_id": { + "description": "The Team ID of the Apple Developer Portal", + "type": "string" + }, + "key_id": { + "description": "The key ID of the Apple Developer Portal", + "type": "string" + } + } } - } + ] }, "DiscoveryMode": { "description": "How to discover the provider's configuration", @@ -2571,4 +2596,4 @@ } } } -} \ No newline at end of file +} diff --git a/docs/setup/sso.md b/docs/setup/sso.md index 0dafd9045..8260ff44e 100644 --- a/docs/setup/sso.md +++ b/docs/setup/sso.md @@ -84,18 +84,23 @@ Sign-in with Apple uses special non-standard for authenticating clients, which r ```yaml upstream_oauth2: providers: - - client_id: 01JAYS74TCG3BTWKADN5Q4518C - client_name: "" # TO BE FILLED + - id: 01JAYS74TCG3BTWKADN5Q4518C + issuer: "https://appleid.apple.com" + human_name: "Apple" + brand_name: "apple" + client_id: "" # TO BE FILLED scope: "openid name email" response_mode: "form_post" - token_endpoint_auth_method: "sign_in_with_apple" sign_in_with_apple: - private_key: | - # Content of the PEM-encoded private key file, TO BE FILLED + + # Only one of the below should be filled for the private key + private_key_file: "" # TO BE FILLED + private_key: | # TO BE FILLED + # + team_id: "" # TO BE FILLED key_id: "" # TO BE FILLED - claims_imports: localpart: action: ignore @@ -548,4 +553,4 @@ To use a Rauthy-supported [Ephemeral Client](https://sebadob.github.io/rauthy/wo "access_token_signed_response_alg": "RS256", "id_token_signed_response_alg": "RS256" } -``` +``` \ No newline at end of file