Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 35 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
10 changes: 9 additions & 1 deletion crates/axum-utils/src/cookies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,15 @@ impl CookieOption {
cookie.set_http_only(true);
cookie.set_secure(self.secure());
cookie.set_path(self.path().to_owned());
cookie.set_same_site(SameSite::Lax);

// The `form_post` callback requires that, as it means a 3rd party origin will
// POST to MAS. This is presumably fine, as our forms are protected with a CSRF
// token
cookie.set_same_site(if self.secure() {
SameSite::None
} else {
SameSite::Lax
});
cookie
}
}
Expand Down
49 changes: 43 additions & 6 deletions crates/cli/src/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand All @@ -205,6 +211,36 @@ pub async fn config_sync(
}
};

let token_endpoint_auth_method = match provider.token_endpoint_auth_method {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe this would be better suited as an .into() implementation if that's not impossible due to the crates being different

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The crates where both structures are defined don't depend on each other, which is why we can't implement it

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
}
};

let response_mode = match provider.response_mode {
mas_config::UpstreamOAuth2ResponseMode::Query => {
mas_data_model::UpstreamOAuthProviderResponseMode::Query
}
mas_config::UpstreamOAuth2ResponseMode::FormPost => {
mas_data_model::UpstreamOAuthProviderResponseMode::FormPost
}
};

if discovery_mode.is_disabled() {
if provider.authorization_endpoint.is_none() {
error!("Provider has discovery disabled but no authorization endpoint set");
Expand Down Expand Up @@ -240,7 +276,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(),
Expand All @@ -252,6 +288,7 @@ pub async fn config_sync(
jwks_uri_override: provider.jwks_uri,
discovery_mode,
pkce_mode,
response_mode,
additional_authorization_parameters: provider
.additional_authorization_parameters
.into_iter()
Expand Down
4 changes: 3 additions & 1 deletion crates/config/src/sections/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ pub use self::{
ClaimsImports as UpstreamOAuth2ClaimsImports, DiscoveryMode as UpstreamOAuth2DiscoveryMode,
EmailImportPreference as UpstreamOAuth2EmailImportPreference,
ImportAction as UpstreamOAuth2ImportAction, PkceMethod as UpstreamOAuth2PkceMethod,
SetEmailVerification as UpstreamOAuth2SetEmailVerification, UpstreamOAuth2Config,
ResponseMode as UpstreamOAuth2ResponseMode,
SetEmailVerification as UpstreamOAuth2SetEmailVerification,
TokenAuthMethod as UpstreamOAuth2TokenAuthMethod, UpstreamOAuth2Config,
},
};
use crate::util::ConfigurationSection;
Expand Down
83 changes: 67 additions & 16 deletions crates/config/src/sections/upstream_oauth2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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"));
}
Expand All @@ -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",
Expand All @@ -80,12 +83,51 @@ 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(())
}
}

/// The response mode we ask the provider to use for the callback
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ResponseMode {
/// `query`: The provider will send the response as a query string in the
/// URL search parameters
#[default]
Query,

/// `form_post`: The provider will send the response as a POST request with
/// the response parameters in the request body
///
/// <https://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html>
FormPost,
}

impl ResponseMode {
#[allow(clippy::trivially_copy_pass_by_ref)]
const fn is_default(&self) -> bool {
matches!(self, ResponseMode::Query)
}
}

/// Authentication methods used against the OAuth 2.0 provider
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
Expand All @@ -108,20 +150,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
Expand Down Expand Up @@ -343,6 +374,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 {
Expand Down Expand Up @@ -394,6 +437,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
///
Expand Down Expand Up @@ -436,6 +483,10 @@ pub struct Provider {
#[serde(skip_serializing_if = "Option::is_none")]
pub jwks_uri: Option<Url>,

/// The response mode we ask the provider to use for the callback
#[serde(default, skip_serializing_if = "ResponseMode::is_default")]
pub response_mode: ResponseMode,

/// How claims should be imported from the `id_token` provided by the
/// provider
#[serde(default, skip_serializing_if = "ClaimsImports::is_default")]
Expand Down
1 change: 1 addition & 0 deletions crates/data-model/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ workspace = true
chrono.workspace = true
thiserror.workspace = true
serde.workspace = true
serde_json.workspace = true
url.workspace = true
crc = "3.2.1"
ulid.workspace = true
Expand Down
3 changes: 2 additions & 1 deletion crates/data-model/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ pub use self::{
UpstreamOAuthAuthorizationSessionState, UpstreamOAuthLink, UpstreamOAuthProvider,
UpstreamOAuthProviderClaimsImports, UpstreamOAuthProviderDiscoveryMode,
UpstreamOAuthProviderImportAction, UpstreamOAuthProviderImportPreference,
UpstreamOAuthProviderPkceMode, UpstreamOAuthProviderSubjectPreference,
UpstreamOAuthProviderPkceMode, UpstreamOAuthProviderResponseMode,
UpstreamOAuthProviderSubjectPreference, UpstreamOAuthProviderTokenAuthMethod,
},
user_agent::{DeviceType, UserAgent},
users::{
Expand Down
4 changes: 3 additions & 1 deletion crates/data-model/src/upstream_oauth2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ pub use self::{
ImportAction as UpstreamOAuthProviderImportAction,
ImportPreference as UpstreamOAuthProviderImportPreference,
PkceMode as UpstreamOAuthProviderPkceMode,
ResponseMode as UpstreamOAuthProviderResponseMode,
SetEmailVerification as UpsreamOAuthProviderSetEmailVerification,
SubjectPreference as UpstreamOAuthProviderSubjectPreference, UpstreamOAuthProvider,
SubjectPreference as UpstreamOAuthProviderSubjectPreference,
TokenAuthMethod as UpstreamOAuthProviderTokenAuthMethod, UpstreamOAuthProvider,
},
session::{UpstreamOAuthAuthorizationSession, UpstreamOAuthAuthorizationSessionState},
};
Loading
Loading