Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion crates/config/src/sections/upstream_oauth2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use std::collections::BTreeMap;

use camino::Utf8PathBuf;
use mas_iana::jose::JsonWebSignatureAlg;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize, de::Error};
Expand Down Expand Up @@ -383,8 +384,14 @@ 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<String>")]
pub private_key_file: Option<Utf8PathBuf>,

/// The private key used to sign the `id_token`
pub private_key: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub private_key: Option<String>,

/// The Team ID of the Apple Developer Portal
pub team_id: String,
Expand Down
3 changes: 2 additions & 1 deletion crates/handlers/src/upstream_oauth2/callback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,8 @@ pub(crate) async fn handler(
lazy_metadata.token_endpoint().await?,
&keystore,
&encrypter,
)?;
)
.await?;

let redirect_uri = url_builder.upstream_oauth_callback(provider.id);

Expand Down
54 changes: 47 additions & 7 deletions crates/handlers/src/upstream_oauth2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@

use std::string::FromUtf8Error;

use camino::Utf8PathBuf;
use mas_data_model::{UpstreamOAuthProvider, UpstreamOAuthProviderTokenAuthMethod};
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 serde::Deserialize;
use thiserror::Error;
use url::Url;
Expand All @@ -30,6 +32,12 @@ enum ProviderCredentialsError {
#[error("Provider doesn't have a client secret")]
MissingClientSecret,

#[error("Duplicate private key and private key file for Sign in with Apple")]
DuplicatePrivateKey,

#[error("Missing private key for signing the id_token")]
MissingPrivateKey,

#[error("Could not decrypt client secret")]
DecryptClientSecret {
#[from]
Expand All @@ -55,22 +63,32 @@ enum ProviderCredentialsError {
},
}

#[derive(Debug, Deserialize)]
#[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<String>")]
pub private_key_file: Option<Utf8PathBuf>,

/// The private key used to sign the `id_token`
#[serde(skip_serializing_if = "Option::is_none")]
pub private_key: Option<String>,

/// 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,
encrypter: &Encrypter,
) -> Result<ClientCredentials, ProviderCredentialsError> {
let client_id = provider.client_id.clone();

// Decrypt the client secret
let client_secret = provider
.encrypted_client_secret
.as_deref()
Expand Down Expand Up @@ -124,10 +142,32 @@ fn client_credentials_for_provider(
},

UpstreamOAuthProviderTokenAuthMethod::SignInWithApple => {
let params = client_secret.ok_or(ProviderCredentialsError::MissingClientSecret)?;
let params: SignInWithApple = serde_json::from_str(&params)?;
let client_secret =
client_secret.ok_or(ProviderCredentialsError::MissingClientSecret)?;

let params: SignInWithApple = serde_json::from_str(&client_secret)
.map_err(|inner| ProviderCredentialsError::InvalidClientSecretJson { inner })?;

if params.private_key.is_none() && params.private_key_file.is_none() {
return Err(ProviderCredentialsError::MissingPrivateKey);
}

if params.private_key.is_some() && params.private_key_file.is_some() {
return Err(ProviderCredentialsError::DuplicatePrivateKey);
}

let key = elliptic_curve::SecretKey::from_pkcs8_pem(&params.private_key)?;
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(|_| ProviderCredentialsError::MissingPrivateKey)?
} else {
unreachable!("already validated above")
};

let key = elliptic_curve::SecretKey::from_pkcs8_pem(&private_key_pem)
.map_err(|inner| ProviderCredentialsError::InvalidPrivateKey { inner })?;

ClientCredentials::SignInWithApple {
client_id,
Expand Down
5 changes: 4 additions & 1 deletion docs/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2158,10 +2158,13 @@
"type": "object",
"required": [
"key_id",
"private_key",
"team_id"
],
"properties": {
"private_key_file": {
"description": "The private key file used to sign the `id_token`",
"type": "string"
},
"private_key": {
"description": "The private key used to sign the `id_token`",
"type": "string"
Expand Down
19 changes: 12 additions & 7 deletions docs/setup/sso.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: "<Service ID>" # TO BE FILLED
- id: 01JAYS74TCG3BTWKADN5Q4518C
issuer: "https://appleid.apple.com"
human_name: "Apple"
brand_name: "apple"
client_id: "<Service 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: "<Location of the PEM-encoded private key file>" # TO BE FILLED
private_key: | # TO BE FILLED
# <Contents of the private key>

team_id: "<Team ID>" # TO BE FILLED
key_id: "<Key ID>" # TO BE FILLED

claims_imports:
localpart:
action: ignore
Expand Down Expand Up @@ -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"
}
```
```
Loading