Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow response_mode to be null and if so do not add the query param #3700

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
16 changes: 10 additions & 6 deletions crates/cli/src/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,13 +235,17 @@ pub async fn config_sync(
}
};

let response_mode = match provider.response_mode {
Copy link
Member

Choose a reason for hiding this comment

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

Using provider.response_mode.map(…) would help simplify this

mas_config::UpstreamOAuth2ResponseMode::Query => {
mas_data_model::UpstreamOAuthProviderResponseMode::Query
}
mas_config::UpstreamOAuth2ResponseMode::FormPost => {
mas_data_model::UpstreamOAuthProviderResponseMode::FormPost
let response_mode = if let Some(response_mode) = provider.response_mode {
match response_mode {
mas_config::UpstreamOAuth2ResponseMode::Query => {
Some(mas_data_model::UpstreamOAuthProviderResponseMode::Query)
}
mas_config::UpstreamOAuth2ResponseMode::FormPost => {
Some(mas_data_model::UpstreamOAuthProviderResponseMode::FormPost)
}
}
} else {
None
};

if discovery_mode.is_disabled() {
Expand Down
14 changes: 3 additions & 11 deletions crates/config/src/sections/upstream_oauth2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,11 @@ impl ConfigurationSection for UpstreamOAuth2Config {
}

/// The response mode we ask the provider to use for the callback
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, JsonSchema)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, 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
Expand All @@ -121,13 +120,6 @@ pub enum ResponseMode {
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 Down Expand Up @@ -550,8 +542,8 @@ pub struct Provider {
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,
#[serde(skip_serializing_if = "Option::is_none")]
pub response_mode: Option<ResponseMode>,

/// How claims should be imported from the `id_token` provided by the
/// provider
Expand Down
2 changes: 1 addition & 1 deletion crates/data-model/src/upstream_oauth2/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ pub struct UpstreamOAuthProvider {
pub token_endpoint_signing_alg: Option<JsonWebSignatureAlg>,
pub token_endpoint_auth_method: TokenAuthMethod,
pub id_token_signed_response_alg: JsonWebSignatureAlg,
pub response_mode: ResponseMode,
pub response_mode: Option<ResponseMode>,
pub created_at: DateTime<Utc>,
pub disabled_at: Option<DateTime<Utc>>,
pub claims_imports: ClaimsImports,
Expand Down
23 changes: 15 additions & 8 deletions crates/handlers/src/oauth2/authorization/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,18 +106,25 @@ fn resolve_response_mode(
) -> Result<ResponseMode, RouteError> {
use ResponseMode as M;

// If the response type includes either "token" or "id_token", the default
// response mode is "fragment" and the response mode "query" must not be
// used
if response_type.has_token() || response_type.has_id_token() {
match suggested_response_mode {
None => Ok(M::Fragment),
Some(M::Query) => Err(RouteError::InvalidResponseMode),
Some(mode) => Ok(mode),
// If the response type includes either "token" or "id_token", the default
// response mode is "fragment" and the response mode "query" must not be
// used
if let Some(suggested_response_mode) = suggested_response_mode {
match suggested_response_mode {
M::Query => Err(RouteError::InvalidResponseMode),
mode => Ok(mode),
}
} else {
Ok(M::Fragment)
Copy link
Member

Choose a reason for hiding this comment

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

It's unclear to me why this has to change? This code is for the client -> MAS login part, and it is spec-compliant, nothing to do with the upstream provider stuff

}
} else {
// In other cases, all response modes are allowed, defaulting to "query"
Ok(suggested_response_mode.unwrap_or(M::Query))
if let Some(suggested_response_mode) = suggested_response_mode {
Ok(suggested_response_mode)
} else {
Ok(M::Query)
}
}
}

Expand Down
9 changes: 6 additions & 3 deletions crates/handlers/src/upstream_oauth2/authorize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,15 @@ pub(crate) async fn get(

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

let data = AuthorizationRequestData::new(
let mut data = AuthorizationRequestData::new(
provider.client_id.clone(),
provider.scope.clone(),
redirect_uri,
)
.with_response_mode(provider.response_mode.into());
);

if let Some(response_mode) = provider.response_mode {
data = data.with_response_mode(response_mode.into());
}

let data = if let Some(methods) = lazy_metadata.pkce_methods().await? {
data.with_code_challenge_methods_supported(methods)
Expand Down
2 changes: 1 addition & 1 deletion crates/handlers/src/upstream_oauth2/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -411,8 +411,8 @@ mod tests {
encrypted_client_secret: None,
token_endpoint_signing_alg: None,
token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::None,
response_mode: mas_data_model::UpstreamOAuthProviderResponseMode::Query,
id_token_signed_response_alg: JsonWebSignatureAlg::Rs256,
response_mode: None,
created_at: clock.now(),
disabled_at: None,
claims_imports: UpstreamOAuthProviderClaimsImports::default(),
Expand Down
62 changes: 43 additions & 19 deletions crates/handlers/src/upstream_oauth2/callback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,13 @@ pub(crate) enum RouteError {
MissingFormParams,

#[error("Invalid response mode, expected '{expected}'")]
InvalidParamsMode {
InvalidResponseMode {
expected: UpstreamOAuthProviderResponseMode,
},

#[error("Invalid request method '{method}'")]
InvalidReqMethod { method: Method },

#[error(transparent)]
Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
}
Expand Down Expand Up @@ -184,25 +187,46 @@ pub(crate) async fn handler(
// The `Form` extractor will use the body of the request for POST requests and
// the query parameters for GET requests. We need to then look at the method do
// make sure it matches the expected `response_mode`
match (provider.response_mode, method) {
(UpstreamOAuthProviderResponseMode::Query, Method::GET) => {}
(UpstreamOAuthProviderResponseMode::FormPost, Method::POST) => {
// We set the cookies with a `Same-Site` policy set to `Lax`, so because this is
// usually a cross-site form POST, we need to render a form with the
// same values, which posts back to the same URL. However, there are
// other valid reasons for the cookie to be missing, so to track whether we did
// this POST ourselves, we set a flag.
if sessions_cookie.is_empty() && !params.did_mas_repost_to_itself {
let params = Params {
did_mas_repost_to_itself: true,
..params
};
let context = FormPostContext::new_for_current_url(params).with_language(&locale);
let html = templates.render_form_post(&context)?;
return Ok(Html(html).into_response());
}
match method {
Copy link
Member

Choose a reason for hiding this comment

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

I'm finding the old single match on the tuple easier to read. Could you go back to using that?

Method::GET => {
match provider.response_mode {
Some(UpstreamOAuthProviderResponseMode::Query) | None => {}
Some(UpstreamOAuthProviderResponseMode::FormPost) => {
return Err(RouteError::InvalidResponseMode {
expected: UpstreamOAuthProviderResponseMode::Query,
})
}
};
}
Method::POST => {
match provider.response_mode {
Some(UpstreamOAuthProviderResponseMode::FormPost) => {
// We set the cookies with a `Same-Site` policy set to `Lax`, so because this is
// usually a cross-site form POST, we need to render a form with the
// same values, which posts back to the same URL. However, there are
// other valid reasons for the cookie to be missing, so to track whether we did
// this POST ourselves, we set a flag.
if sessions_cookie.is_empty() && !params.did_mas_repost_to_itself {
let params = Params {
did_mas_repost_to_itself: true,
..params
};
let context =
FormPostContext::new_for_current_url(params).with_language(&locale);
let html = templates.render_form_post(&context)?;
return Ok(Html(html).into_response());
}
}
Some(UpstreamOAuthProviderResponseMode::Query) | None => {
return Err(RouteError::InvalidResponseMode {
expected: UpstreamOAuthProviderResponseMode::FormPost,
})
}
};
}
method => {
return Err(RouteError::InvalidReqMethod { method });
Comment on lines +227 to +228
Copy link
Member

Choose a reason for hiding this comment

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

This should never happen, the route is installed as POST and GET handler anyway

}
(expected, _) => return Err(RouteError::InvalidParamsMode { expected }),
}

let (session_id, _post_auth_action) = sessions_cookie
Expand Down
2 changes: 1 addition & 1 deletion crates/handlers/src/upstream_oauth2/link.rs
Original file line number Diff line number Diff line change
Expand Up @@ -934,7 +934,7 @@ mod tests {
jwks_uri_override: None,
discovery_mode: mas_data_model::UpstreamOAuthProviderDiscoveryMode::Oidc,
pkce_mode: mas_data_model::UpstreamOAuthProviderPkceMode::Auto,
response_mode: mas_data_model::UpstreamOAuthProviderResponseMode::Query,
response_mode: None,
additional_authorization_parameters: Vec::new(),
},
)
Expand Down
4 changes: 2 additions & 2 deletions crates/handlers/src/views/login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@ mod test {
jwks_uri_override: None,
discovery_mode: mas_data_model::UpstreamOAuthProviderDiscoveryMode::Oidc,
pkce_mode: mas_data_model::UpstreamOAuthProviderPkceMode::Auto,
response_mode: mas_data_model::UpstreamOAuthProviderResponseMode::Query,
response_mode: None,
additional_authorization_parameters: Vec::new(),
},
)
Expand Down Expand Up @@ -456,7 +456,7 @@ mod test {
jwks_uri_override: None,
discovery_mode: mas_data_model::UpstreamOAuthProviderDiscoveryMode::Oidc,
pkce_mode: mas_data_model::UpstreamOAuthProviderPkceMode::Auto,
response_mode: mas_data_model::UpstreamOAuthProviderResponseMode::Query,
response_mode: None,
additional_authorization_parameters: Vec::new(),
},
)
Expand Down

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

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

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "upstream_oauth_providers" ALTER COLUMN "response_mode" DROP NOT NULL;
Copy link
Member

Choose a reason for hiding this comment

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

Can you add the copyright header + a short description of what this migration does?

4 changes: 2 additions & 2 deletions crates/storage-pg/src/upstream_oauth2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ mod tests {
jwks_uri_override: None,
discovery_mode: mas_data_model::UpstreamOAuthProviderDiscoveryMode::Oidc,
pkce_mode: mas_data_model::UpstreamOAuthProviderPkceMode::Auto,
response_mode: mas_data_model::UpstreamOAuthProviderResponseMode::Query,
response_mode: None,
additional_authorization_parameters: Vec::new(),
},
)
Expand Down Expand Up @@ -320,7 +320,7 @@ mod tests {
jwks_uri_override: None,
discovery_mode: mas_data_model::UpstreamOAuthProviderDiscoveryMode::Oidc,
pkce_mode: mas_data_model::UpstreamOAuthProviderPkceMode::Auto,
response_mode: mas_data_model::UpstreamOAuthProviderResponseMode::Query,
response_mode: None,
additional_authorization_parameters: Vec::new(),
},
)
Expand Down
22 changes: 13 additions & 9 deletions crates/storage-pg/src/upstream_oauth2/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ struct ProviderLookup {
userinfo_endpoint_override: Option<String>,
discovery_mode: String,
pkce_mode: String,
response_mode: String,
response_mode: Option<String>,
additional_parameters: Option<Json<Vec<(String, String)>>>,
}

Expand Down Expand Up @@ -177,12 +177,16 @@ impl TryFrom<ProviderLookup> for UpstreamOAuthProvider {
.source(e)
})?;

let response_mode = value.response_mode.parse().map_err(|e| {
DatabaseInconsistencyError::on("upstream_oauth_providers")
.column("response_mode")
.row(id)
.source(e)
})?;
let response_mode = value
.response_mode
.map(|x| x.parse())
.transpose()
.map_err(|e| {
DatabaseInconsistencyError::on("upstream_oauth_providers")
.column("response_mode")
.row(id)
.source(e)
})?;

let additional_authorization_parameters = value
.additional_parameters
Expand Down Expand Up @@ -370,7 +374,7 @@ impl UpstreamOAuthProviderRepository for PgUpstreamOAuthProviderRepository<'_> {
params.jwks_uri_override.as_ref().map(ToString::to_string),
params.discovery_mode.as_str(),
params.pkce_mode.as_str(),
params.response_mode.as_str(),
params.response_mode.as_ref().map(ToString::to_string),
created_at,
)
.traced()
Expand Down Expand Up @@ -576,7 +580,7 @@ impl UpstreamOAuthProviderRepository for PgUpstreamOAuthProviderRepository<'_> {
params.jwks_uri_override.as_ref().map(ToString::to_string),
params.discovery_mode.as_str(),
params.pkce_mode.as_str(),
params.response_mode.as_str(),
params.response_mode.as_ref().map(ToString::to_string),
Json(&params.additional_authorization_parameters) as _,
created_at,
)
Expand Down
2 changes: 1 addition & 1 deletion crates/storage/src/upstream_oauth2/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ pub struct UpstreamOAuthProviderParams {
pub pkce_mode: UpstreamOAuthProviderPkceMode,

/// What response mode it should ask
pub response_mode: UpstreamOAuthProviderResponseMode,
pub response_mode: Option<UpstreamOAuthProviderResponseMode>,

/// Additional parameters to include in the authorization request
pub additional_authorization_parameters: Vec<(String, String)>,
Expand Down
6 changes: 3 additions & 3 deletions crates/templates/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ use mas_data_model::{
AuthorizationGrant, BrowserSession, Client, CompatSsoLogin, CompatSsoLoginState,
DeviceCodeGrant, UpstreamOAuthLink, UpstreamOAuthProvider, UpstreamOAuthProviderClaimsImports,
UpstreamOAuthProviderDiscoveryMode, UpstreamOAuthProviderPkceMode,
UpstreamOAuthProviderResponseMode, UpstreamOAuthProviderTokenAuthMethod, User, UserAgent,
UserEmail, UserEmailVerification, UserRecoverySession,
UpstreamOAuthProviderTokenAuthMethod, User, UserAgent, UserEmail, UserEmailVerification,
UserRecoverySession,
};
use mas_i18n::DataLocale;
use mas_iana::jose::JsonWebSignatureAlg;
Expand Down Expand Up @@ -1408,7 +1408,7 @@ impl TemplateContext for UpstreamRegister {
userinfo_signed_response_alg: None,
discovery_mode: UpstreamOAuthProviderDiscoveryMode::Oidc,
pkce_mode: UpstreamOAuthProviderPkceMode::Auto,
response_mode: UpstreamOAuthProviderResponseMode::Query,
response_mode: None,
additional_authorization_parameters: Vec::new(),
created_at: now,
disabled_at: None,
Expand Down
Loading