Support Sign in with Apple

This commit is contained in:
Quentin Gliech
2024-11-15 17:04:13 +01:00
parent 156483bb55
commit dddd9fe998
20 changed files with 374 additions and 74 deletions

6
Cargo.lock generated
View File

@@ -3346,6 +3346,7 @@ dependencies = [
"camino",
"chrono",
"cookie_store",
"elliptic-curve",
"futures-util",
"governor",
"headers",
@@ -3377,6 +3378,7 @@ dependencies = [
"opentelemetry",
"opentelemetry-semantic-conventions",
"pbkdf2",
"pkcs8",
"psl",
"rand",
"rand_chacha",
@@ -3612,6 +3614,7 @@ dependencies = [
"base64ct",
"bitflags 2.6.0",
"chrono",
"elliptic-curve",
"form_urlencoded",
"headers",
"http",
@@ -3623,6 +3626,9 @@ dependencies = [
"mas-keystore",
"mime",
"oauth2-types",
"p256",
"pem-rfc7468",
"pkcs8",
"rand",
"rand_chacha",
"reqwest",

View File

@@ -102,6 +102,11 @@ features = ["serde", "clock"]
version = "4.5.21"
features = ["derive"]
# Elliptic curve cryptography
[workspace.dependencies.elliptic-curve]
version = "0.13.8"
features = ["std", "pem", "sec1"]
# Configuration loading
[workspace.dependencies.figment]
version = "0.10.19"
@@ -188,6 +193,36 @@ features = ["pycompat"]
[workspace.dependencies.nonzero_ext]
version = "0.3.0"
# K256 elliptic curve
[workspace.dependencies.k256]
version = "0.13.4"
features = ["std"]
# P256 elliptic curve
[workspace.dependencies.p256]
version = "0.13.2"
features = ["std"]
# P384 elliptic curve
[workspace.dependencies.p384]
version = "0.13.0"
features = ["std"]
# PEM file decoding
[workspace.dependencies.pem-rfc7468]
version = "0.7.0"
features = ["std"]
# PKCS#1 encoding
[workspace.dependencies.pkcs1]
version = "0.7.5"
features = ["std"]
# PKCS#8 encoding
[workspace.dependencies.pkcs8]
version = "0.10.2"
features = ["std", "pkcs5", "encryption"]
# Random values
[workspace.dependencies.rand]
version = "0.8.5"

View File

@@ -187,11 +187,17 @@ pub async fn config_sync(
continue;
}
let encrypted_client_secret = provider
.client_secret
.as_deref()
.map(|client_secret| encrypter.encrypt_to_string(client_secret.as_bytes()))
.transpose()?;
let encrypted_client_secret =
if let Some(client_secret) = provider.client_secret.as_deref() {
Some(encrypter.encrypt_to_string(client_secret.as_bytes())?)
} else if let Some(siwa) = provider.sign_in_with_apple.as_ref() {
// For SIWA, we JSON-encode the config and encrypt it, reusing the client_secret
// field in the database
let encoded = serde_json::to_vec(siwa)?;
Some(encrypter.encrypt_to_string(&encoded)?)
} else {
None
};
let discovery_mode = match provider.discovery_mode {
mas_config::UpstreamOAuth2DiscoveryMode::Oidc => {
@@ -205,6 +211,27 @@ pub async fn config_sync(
}
};
let token_endpoint_auth_method = match provider.token_endpoint_auth_method {
mas_config::UpstreamOAuth2TokenAuthMethod::None => {
mas_data_model::UpstreamOAuthProviderTokenAuthMethod::None
}
mas_config::UpstreamOAuth2TokenAuthMethod::ClientSecretBasic => {
mas_data_model::UpstreamOAuthProviderTokenAuthMethod::ClientSecretBasic
}
mas_config::UpstreamOAuth2TokenAuthMethod::ClientSecretPost => {
mas_data_model::UpstreamOAuthProviderTokenAuthMethod::ClientSecretPost
}
mas_config::UpstreamOAuth2TokenAuthMethod::ClientSecretJwt => {
mas_data_model::UpstreamOAuthProviderTokenAuthMethod::ClientSecretJwt
}
mas_config::UpstreamOAuth2TokenAuthMethod::PrivateKeyJwt => {
mas_data_model::UpstreamOAuthProviderTokenAuthMethod::PrivateKeyJwt
}
mas_config::UpstreamOAuth2TokenAuthMethod::SignInWithApple => {
mas_data_model::UpstreamOAuthProviderTokenAuthMethod::SignInWithApple
}
};
if discovery_mode.is_disabled() {
if provider.authorization_endpoint.is_none() {
error!("Provider has discovery disabled but no authorization endpoint set");
@@ -240,7 +267,7 @@ pub async fn config_sync(
human_name: provider.human_name,
brand_name: provider.brand_name,
scope: provider.scope.parse()?,
token_endpoint_auth_method: provider.token_endpoint_auth_method.into(),
token_endpoint_auth_method,
token_endpoint_signing_alg: provider
.token_endpoint_auth_signing_alg
.clone(),

View File

@@ -51,7 +51,8 @@ pub use self::{
ClaimsImports as UpstreamOAuth2ClaimsImports, DiscoveryMode as UpstreamOAuth2DiscoveryMode,
EmailImportPreference as UpstreamOAuth2EmailImportPreference,
ImportAction as UpstreamOAuth2ImportAction, PkceMethod as UpstreamOAuth2PkceMethod,
SetEmailVerification as UpstreamOAuth2SetEmailVerification, UpstreamOAuth2Config,
SetEmailVerification as UpstreamOAuth2SetEmailVerification,
TokenAuthMethod as UpstreamOAuth2TokenAuthMethod, UpstreamOAuth2Config,
},
};
use crate::util::ConfigurationSection;

View File

@@ -6,7 +6,7 @@
use std::collections::BTreeMap;
use mas_iana::{jose::JsonWebSignatureAlg, oauth::OAuthClientAuthenticationMethod};
use mas_iana::jose::JsonWebSignatureAlg;
use schemars::JsonSchema;
use serde::{de::Error, Deserialize, Serialize};
use serde_with::skip_serializing_none;
@@ -48,7 +48,9 @@ impl ConfigurationSection for UpstreamOAuth2Config {
};
match provider.token_endpoint_auth_method {
TokenAuthMethod::None | TokenAuthMethod::PrivateKeyJwt => {
TokenAuthMethod::None
| TokenAuthMethod::PrivateKeyJwt
| TokenAuthMethod::SignInWithApple => {
if provider.client_secret.is_some() {
return annotate(figment::Error::custom("Unexpected field `client_secret` for the selected authentication method"));
}
@@ -65,7 +67,8 @@ impl ConfigurationSection for UpstreamOAuth2Config {
match provider.token_endpoint_auth_method {
TokenAuthMethod::None
| TokenAuthMethod::ClientSecretBasic
| TokenAuthMethod::ClientSecretPost => {
| TokenAuthMethod::ClientSecretPost
| TokenAuthMethod::SignInWithApple => {
if provider.token_endpoint_auth_signing_alg.is_some() {
return annotate(figment::Error::custom(
"Unexpected field `token_endpoint_auth_signing_alg` for the selected authentication method",
@@ -80,6 +83,22 @@ impl ConfigurationSection for UpstreamOAuth2Config {
}
}
}
match provider.token_endpoint_auth_method {
TokenAuthMethod::SignInWithApple => {
if provider.sign_in_with_apple.is_none() {
return annotate(figment::Error::missing_field("sign_in_with_apple"));
}
}
_ => {
if provider.sign_in_with_apple.is_some() {
return annotate(figment::Error::custom(
"Unexpected field `sign_in_with_apple` for the selected authentication method",
));
}
}
}
}
Ok(())
@@ -108,20 +127,9 @@ pub enum TokenAuthMethod {
/// `private_key_jwt`: a `client_assertion` sent in the request body and
/// signed by an asymmetric key
PrivateKeyJwt,
}
impl From<TokenAuthMethod> for OAuthClientAuthenticationMethod {
fn from(method: TokenAuthMethod) -> Self {
match method {
TokenAuthMethod::None => OAuthClientAuthenticationMethod::None,
TokenAuthMethod::ClientSecretBasic => {
OAuthClientAuthenticationMethod::ClientSecretBasic
}
TokenAuthMethod::ClientSecretPost => OAuthClientAuthenticationMethod::ClientSecretPost,
TokenAuthMethod::ClientSecretJwt => OAuthClientAuthenticationMethod::ClientSecretJwt,
TokenAuthMethod::PrivateKeyJwt => OAuthClientAuthenticationMethod::PrivateKeyJwt,
}
}
/// `sign_in_with_apple`: a special method for Signin with Apple
SignInWithApple,
}
/// How to handle a claim
@@ -343,6 +351,18 @@ fn is_default_true(value: &bool) -> bool {
*value
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SignInWithApple {
/// The private key used to sign the `id_token`
pub private_key: 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,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct Provider {
@@ -394,6 +414,10 @@ pub struct Provider {
/// The method to authenticate the client with the provider
pub token_endpoint_auth_method: TokenAuthMethod,
/// Additional parameters for the `sign_in_with_apple` method
#[serde(skip_serializing_if = "Option::is_none")]
pub sign_in_with_apple: Option<SignInWithApple>,
/// The JWS algorithm to use when authenticating the client with the
/// provider
///

View File

@@ -42,6 +42,7 @@ pub use self::{
UpstreamOAuthProviderClaimsImports, UpstreamOAuthProviderDiscoveryMode,
UpstreamOAuthProviderImportAction, UpstreamOAuthProviderImportPreference,
UpstreamOAuthProviderPkceMode, UpstreamOAuthProviderSubjectPreference,
UpstreamOAuthProviderTokenAuthMethod,
},
user_agent::{DeviceType, UserAgent},
users::{

View File

@@ -17,7 +17,8 @@ pub use self::{
ImportPreference as UpstreamOAuthProviderImportPreference,
PkceMode as UpstreamOAuthProviderPkceMode,
SetEmailVerification as UpsreamOAuthProviderSetEmailVerification,
SubjectPreference as UpstreamOAuthProviderSubjectPreference, UpstreamOAuthProvider,
SubjectPreference as UpstreamOAuthProviderSubjectPreference,
TokenAuthMethod as UpstreamOAuthProviderTokenAuthMethod, UpstreamOAuthProvider,
},
session::{UpstreamOAuthAuthorizationSession, UpstreamOAuthAuthorizationSessionState},
};

View File

@@ -5,7 +5,7 @@
// Please see LICENSE in the repository root for full details.
use chrono::{DateTime, Utc};
use mas_iana::{jose::JsonWebSignatureAlg, oauth::OAuthClientAuthenticationMethod};
use mas_iana::jose::JsonWebSignatureAlg;
use oauth2_types::scope::Scope;
use serde::{Deserialize, Serialize};
use thiserror::Error;
@@ -116,6 +116,57 @@ impl std::fmt::Display for PkceMode {
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum TokenAuthMethod {
None,
ClientSecretBasic,
ClientSecretPost,
ClientSecretJwt,
PrivateKeyJwt,
SignInWithApple,
}
impl TokenAuthMethod {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::None => "none",
Self::ClientSecretBasic => "client_secret_basic",
Self::ClientSecretPost => "client_secret_post",
Self::ClientSecretJwt => "client_secret_jwt",
Self::PrivateKeyJwt => "private_key_jwt",
Self::SignInWithApple => "sign_in_with_apple",
}
}
}
impl std::fmt::Display for TokenAuthMethod {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl std::str::FromStr for TokenAuthMethod {
type Err = InvalidUpstreamOAuth2TokenAuthMethod;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"none" => Ok(Self::None),
"client_secret_post" => Ok(Self::ClientSecretPost),
"client_secret_basic" => Ok(Self::ClientSecretBasic),
"client_secret_jwt" => Ok(Self::ClientSecretJwt),
"private_key_jwt" => Ok(Self::PrivateKeyJwt),
"sign_in_with_apple" => Ok(Self::SignInWithApple),
s => Err(InvalidUpstreamOAuth2TokenAuthMethod(s.to_owned())),
}
}
}
#[derive(Debug, Clone, Error)]
#[error("Invalid upstream OAuth 2.0 token auth method: {0}")]
pub struct InvalidUpstreamOAuth2TokenAuthMethod(String);
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct UpstreamOAuthProvider {
pub id: Ulid,
@@ -126,12 +177,12 @@ pub struct UpstreamOAuthProvider {
pub pkce_mode: PkceMode,
pub jwks_uri_override: Option<Url>,
pub authorization_endpoint_override: Option<Url>,
pub token_endpoint_override: Option<Url>,
pub scope: Scope,
pub token_endpoint_override: Option<Url>,
pub client_id: String,
pub encrypted_client_secret: Option<String>,
pub token_endpoint_signing_alg: Option<JsonWebSignatureAlg>,
pub token_endpoint_auth_method: OAuthClientAuthenticationMethod,
pub token_endpoint_auth_method: TokenAuthMethod,
pub created_at: DateTime<Utc>,
pub disabled_at: Option<DateTime<Utc>>,
pub claims_imports: ClaimsImports,

View File

@@ -71,8 +71,10 @@ zeroize = "1.8.1"
base64ct = "1.6.0"
camino.workspace = true
chrono.workspace = true
elliptic-curve.workspace = true
governor.workspace = true
indexmap = "2.6.0"
pkcs8.workspace = true
psl = "2.1.56"
time = "0.3.36"
url.workspace = true

View File

@@ -274,8 +274,9 @@ mod tests {
// XXX: sadly, we can't test HTTPS requests with wiremock, so we can only test
// 'insecure' discovery
use mas_data_model::UpstreamOAuthProviderClaimsImports;
use mas_iana::oauth::OAuthClientAuthenticationMethod;
use mas_data_model::{
UpstreamOAuthProviderClaimsImports, UpstreamOAuthProviderTokenAuthMethod,
};
use mas_storage::{clock::MockClock, Clock};
use oauth2_types::scope::{Scope, OPENID};
use ulid::Ulid;
@@ -393,7 +394,7 @@ mod tests {
client_id: "client_id".to_owned(),
encrypted_client_secret: None,
token_endpoint_signing_alg: None,
token_endpoint_auth_method: OAuthClientAuthenticationMethod::None,
token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::None,
created_at: clock.now(),
disabled_at: None,
claims_imports: UpstreamOAuthProviderClaimsImports::default(),

View File

@@ -843,8 +843,9 @@ mod tests {
use hyper::{header::CONTENT_TYPE, Request, StatusCode};
use mas_data_model::{
UpstreamOAuthProviderClaimsImports, UpstreamOAuthProviderImportPreference,
UpstreamOAuthProviderTokenAuthMethod,
};
use mas_iana::{jose::JsonWebSignatureAlg, oauth::OAuthClientAuthenticationMethod};
use mas_iana::jose::JsonWebSignatureAlg;
use mas_jose::jwt::{JsonWebSignatureHeader, Jwt};
use mas_router::Route;
use mas_storage::upstream_oauth2::UpstreamOAuthProviderParams;
@@ -906,7 +907,7 @@ mod tests {
human_name: Some("Example Ltd.".to_owned()),
brand_name: None,
scope: Scope::from_iter([OPENID]),
token_endpoint_auth_method: OAuthClientAuthenticationMethod::None,
token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::None,
token_endpoint_signing_alg: None,
client_id: "client".to_owned(),
encrypted_client_secret: None,

View File

@@ -6,10 +6,12 @@
use std::string::FromUtf8Error;
use mas_data_model::UpstreamOAuthProvider;
use mas_iana::{jose::JsonWebSignatureAlg, oauth::OAuthClientAuthenticationMethod};
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 serde::Deserialize;
use thiserror::Error;
use url::Url;
@@ -39,6 +41,25 @@ enum ProviderCredentialsError {
#[from]
inner: FromUtf8Error,
},
#[error("Invalid JSON in client secret")]
InvalidClientSecretJson {
#[from]
inner: serde_json::Error,
},
#[error("Could not parse PEM encoded private key")]
InvalidPrivateKey {
#[from]
inner: pkcs8::Error,
},
}
#[derive(Debug, Deserialize)]
pub struct SignInWithApple {
pub private_key: String,
pub team_id: String,
pub key_id: String,
}
fn client_credentials_for_provider(
@@ -61,28 +82,38 @@ fn client_credentials_for_provider(
.transpose()?;
let client_credentials = match provider.token_endpoint_auth_method {
OAuthClientAuthenticationMethod::None => ClientCredentials::None { client_id },
OAuthClientAuthenticationMethod::ClientSecretPost => ClientCredentials::ClientSecretPost {
client_id,
client_secret: client_secret.ok_or(ProviderCredentialsError::MissingClientSecret)?,
},
OAuthClientAuthenticationMethod::ClientSecretBasic => {
UpstreamOAuthProviderTokenAuthMethod::None => ClientCredentials::None { client_id },
UpstreamOAuthProviderTokenAuthMethod::ClientSecretPost => {
ClientCredentials::ClientSecretPost {
client_id,
client_secret: client_secret
.ok_or(ProviderCredentialsError::MissingClientSecret)?,
}
}
UpstreamOAuthProviderTokenAuthMethod::ClientSecretBasic => {
ClientCredentials::ClientSecretBasic {
client_id,
client_secret: client_secret
.ok_or(ProviderCredentialsError::MissingClientSecret)?,
}
}
OAuthClientAuthenticationMethod::ClientSecretJwt => ClientCredentials::ClientSecretJwt {
client_id,
client_secret: client_secret.ok_or(ProviderCredentialsError::MissingClientSecret)?,
signing_algorithm: provider
.token_endpoint_signing_alg
.clone()
.unwrap_or(JsonWebSignatureAlg::Rs256),
token_endpoint: token_endpoint.clone(),
},
OAuthClientAuthenticationMethod::PrivateKeyJwt => ClientCredentials::PrivateKeyJwt {
UpstreamOAuthProviderTokenAuthMethod::ClientSecretJwt => {
ClientCredentials::ClientSecretJwt {
client_id,
client_secret: client_secret
.ok_or(ProviderCredentialsError::MissingClientSecret)?,
signing_algorithm: provider
.token_endpoint_signing_alg
.clone()
.unwrap_or(JsonWebSignatureAlg::Rs256),
token_endpoint: token_endpoint.clone(),
}
}
UpstreamOAuthProviderTokenAuthMethod::PrivateKeyJwt => ClientCredentials::PrivateKeyJwt {
client_id,
keystore: keystore.clone(),
signing_algorithm: provider
@@ -91,8 +122,21 @@ fn client_credentials_for_provider(
.unwrap_or(JsonWebSignatureAlg::Rs256),
token_endpoint: token_endpoint.clone(),
},
// XXX: The database should never have an unsupported method in it
_ => unreachable!(),
UpstreamOAuthProviderTokenAuthMethod::SignInWithApple => {
let params = client_secret.ok_or(ProviderCredentialsError::MissingClientSecret)?;
let params: SignInWithApple = serde_json::from_str(&params)?;
let key = elliptic_curve::SecretKey::from_pkcs8_pem(&params.private_key)?;
ClientCredentials::SignInWithApple {
client_id,
audience: provider.issuer.clone(),
key,
key_id: params.key_id,
team_id: params.team_id,
}
}
};
Ok(client_credentials)

View File

@@ -343,8 +343,9 @@ mod test {
header::{CONTENT_TYPE, LOCATION},
Request, StatusCode,
};
use mas_data_model::UpstreamOAuthProviderClaimsImports;
use mas_iana::oauth::OAuthClientAuthenticationMethod;
use mas_data_model::{
UpstreamOAuthProviderClaimsImports, UpstreamOAuthProviderTokenAuthMethod,
};
use mas_router::Route;
use mas_storage::{
upstream_oauth2::{UpstreamOAuthProviderParams, UpstreamOAuthProviderRepository},
@@ -400,7 +401,7 @@ mod test {
human_name: Some("First Ltd.".to_owned()),
brand_name: None,
scope: [OPENID].into_iter().collect(),
token_endpoint_auth_method: OAuthClientAuthenticationMethod::None,
token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::None,
token_endpoint_signing_alg: None,
client_id: "client".to_owned(),
encrypted_client_secret: None,
@@ -435,7 +436,7 @@ mod test {
human_name: None,
brand_name: None,
scope: [OPENID].into_iter().collect(),
token_endpoint_auth_method: OAuthClientAuthenticationMethod::None,
token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::None,
token_endpoint_signing_alg: None,
client_id: "client".to_owned(),
encrypted_client_secret: None,

View File

@@ -16,7 +16,7 @@ base64ct = { version = "1.6.0", features = ["std"] }
chrono.workspace = true
digest = "0.10.7"
ecdsa = { version = "0.16.9", features = ["signing", "verifying"] }
elliptic-curve = "0.13.8"
elliptic-curve.workspace = true
generic-array = "0.14.7"
hmac = "0.12.1"
k256 = { version = "0.13.4", features = ["ecdsa"] }

View File

@@ -16,13 +16,13 @@ aead = { version = "0.5.2", features = ["std"] }
const-oid = { version = "0.9.6", features = ["std"] }
der = { version = "0.7.9", features = ["std"] }
ecdsa = { version = "0.16.9", features = ["std"] }
elliptic-curve = { version = "0.13.8", features = ["std", "pem", "sec1"] }
k256 = { version = "0.13.4", features = ["std"] }
p256 = { version = "0.13.2", features = ["std"] }
p384 = { version = "0.13.0", features = ["std"] }
pem-rfc7468 = { version = "0.7.0", features = ["std"] }
pkcs1 = { version = "0.7.5", features = ["std"] }
pkcs8 = { version = "0.10.2", features = ["std", "pkcs5", "encryption"] }
elliptic-curve.workspace = true
k256.workspace = true
p256.workspace = true
p384.workspace = true
pem-rfc7468.workspace = true
pkcs1.workspace = true
pkcs8.workspace = true
rand.workspace = true
rsa = { version = "0.9.6", features = ["std", "pem"] }
sec1 = { version = "0.7.3", features = ["std"] }

View File

@@ -15,11 +15,15 @@ workspace = true
async-trait.workspace = true
base64ct = { version = "1.6.0", features = ["std"] }
chrono.workspace = true
elliptic-curve.workspace = true
form_urlencoded = "1.2.1"
headers.workspace = true
http.workspace = true
language-tags = "0.3.2"
mime = "0.3.17"
pem-rfc7468.workspace = true
pkcs8.workspace = true
p256.workspace = true
rand.workspace = true
reqwest.workspace = true
serde.workspace = true

View File

@@ -14,7 +14,7 @@ use mas_iana::{jose::JsonWebSignatureAlg, oauth::OAuthClientAuthenticationMethod
use mas_jose::{
claims::{self, ClaimError},
constraints::Constrainable,
jwa::SymmetricKey,
jwa::{AsymmetricSigningKey, SymmetricKey},
jwt::{JsonWebSignatureHeader, Jwt},
};
use mas_keystore::Keystore;
@@ -97,6 +97,24 @@ pub enum ClientCredentials {
/// The URL of the issuer's Token endpoint.
token_endpoint: Url,
},
/// The client authenticates like Sign in with Apple wants
SignInWithApple {
/// The unique ID for the client.
client_id: String,
/// The audience to use. Usually `https://appleid.apple.com`
audience: String,
/// The ECDSA key used to sign
key: elliptic_curve::SecretKey<p256::NistP256>,
/// The key ID
key_id: String,
/// The Apple Team ID
team_id: String,
},
}
impl ClientCredentials {
@@ -108,12 +126,14 @@ impl ClientCredentials {
| ClientCredentials::ClientSecretBasic { client_id, .. }
| ClientCredentials::ClientSecretPost { client_id, .. }
| ClientCredentials::ClientSecretJwt { client_id, .. }
| ClientCredentials::PrivateKeyJwt { client_id, .. } => client_id,
| ClientCredentials::PrivateKeyJwt { client_id, .. }
| ClientCredentials::SignInWithApple { client_id, .. } => client_id,
}
}
/// Apply these [`ClientCredentials`] to the given request with the given
/// form.
#[allow(clippy::too_many_lines)]
pub(crate) fn authenticated_form<T: Serialize>(
&self,
request: reqwest::RequestBuilder,
@@ -217,6 +237,39 @@ impl ClientCredentials {
client_assertion_type: Some(JwtBearerClientAssertionType),
})
}
ClientCredentials::SignInWithApple {
client_id,
audience,
key,
key_id,
team_id,
} => {
// SIWA expects a signed JWT as client secret
// https://developer.apple.com/documentation/accountorganizationaldatasharing/creating-a-client-secret
let signer = AsymmetricSigningKey::es256(key.clone());
let mut claims = HashMap::new();
claims::ISS.insert(&mut claims, team_id)?;
claims::SUB.insert(&mut claims, client_id)?;
claims::AUD.insert(&mut claims, audience.clone())?;
claims::IAT.insert(&mut claims, now)?;
claims::EXP.insert(&mut claims, now + Duration::microseconds(60 * 1000 * 1000))?;
let header =
JsonWebSignatureHeader::new(JsonWebSignatureAlg::Es256).with_kid(key_id);
let client_secret = Jwt::sign(header, claims, &signer)?;
request.form(&RequestWithClientCredentials {
body: form,
client_id,
client_secret: Some(client_secret.as_str()),
client_assertion: None,
client_assertion_type: None,
})
}
};
Ok(request)
@@ -260,6 +313,17 @@ impl fmt::Debug for ClientCredentials {
.field("signing_algorithm", signing_algorithm)
.field("token_endpoint", token_endpoint)
.finish_non_exhaustive(),
Self::SignInWithApple {
client_id,
key_id,
team_id,
..
} => f
.debug_struct("SignInWithApple")
.field("client_id", client_id)
.field("key_id", key_id)
.field("team_id", team_id)
.finish_non_exhaustive(),
}
}
}

View File

@@ -19,7 +19,9 @@ pub use self::{
#[cfg(test)]
mod tests {
use chrono::Duration;
use mas_data_model::UpstreamOAuthProviderClaimsImports;
use mas_data_model::{
UpstreamOAuthProviderClaimsImports, UpstreamOAuthProviderTokenAuthMethod,
};
use mas_storage::{
clock::MockClock,
upstream_oauth2::{
@@ -57,8 +59,7 @@ mod tests {
human_name: None,
brand_name: None,
scope: Scope::from_iter([OPENID]),
token_endpoint_auth_method:
mas_iana::oauth::OAuthClientAuthenticationMethod::None,
token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::None,
token_endpoint_signing_alg: None,
client_id: "client-id".to_owned(),
encrypted_client_secret: None,
@@ -299,8 +300,7 @@ mod tests {
human_name: None,
brand_name: None,
scope: scope.clone(),
token_endpoint_auth_method:
mas_iana::oauth::OAuthClientAuthenticationMethod::None,
token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::None,
token_endpoint_signing_alg: None,
client_id,
encrypted_client_secret: None,

View File

@@ -9,9 +9,9 @@ use std::marker::PhantomData;
use async_trait::async_trait;
use mas_data_model::{
UpstreamOAuthProvider, UpstreamOAuthProviderClaimsImports, UpstreamOAuthProviderDiscoveryMode,
UpstreamOAuthProviderPkceMode,
UpstreamOAuthProviderPkceMode, UpstreamOAuthProviderTokenAuthMethod,
};
use mas_iana::{jose::JsonWebSignatureAlg, oauth::OAuthClientAuthenticationMethod};
use mas_iana::jose::JsonWebSignatureAlg;
use oauth2_types::scope::Scope;
use rand_core::RngCore;
use ulid::Ulid;
@@ -35,7 +35,7 @@ pub struct UpstreamOAuthProviderParams {
pub scope: Scope,
/// The token endpoint authentication method
pub token_endpoint_auth_method: OAuthClientAuthenticationMethod,
pub token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod,
/// The JWT signing algorithm to use when then `client_secret_jwt` or
/// `private_key_jwt` authentication methods are used

View File

@@ -1859,6 +1859,14 @@
}
]
},
"sign_in_with_apple": {
"description": "Additional parameters for the `sign_in_with_apple` method",
"allOf": [
{
"$ref": "#/definitions/SignInWithApple"
}
]
},
"token_endpoint_auth_signing_alg": {
"description": "The JWS algorithm to use when authenticating the client with the provider\n\nUsed by the `client_secret_jwt` and `private_key_jwt` methods",
"allOf": [
@@ -1956,9 +1964,38 @@
"enum": [
"private_key_jwt"
]
},
{
"description": "`sign_in_with_apple`: a special method for Signin with Apple",
"type": "string",
"enum": [
"sign_in_with_apple"
]
}
]
},
"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"
},
"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",
"oneOf": [