Browse Source

Merge 8778a6f934 into 0cefa4cca7

pull/7461/merge
Raphaël Roumezin 5 days ago
committed by GitHub
parent
commit
a0990409a3
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 3
      .env.template
  2. 17
      src/api/core/organizations.rs
  3. 19
      src/api/identity.rs
  4. 2
      src/config.rs
  5. 4
      src/sso.rs
  6. 7
      src/sso_client.rs

3
.env.template

@ -535,6 +535,9 @@
## Additional authorization url parameters (ex: to obtain a `refresh_token` with Google Auth).
# SSO_AUTHORIZE_EXTRA_PARAMS="access_type=offline&prompt=consent"
## Use the email provided during SSO logon to indicate the account to use to the OIDC provider through the `login_hint` OIDC field.
# SSO_LOGIN_HINT=true
## Activate PKCE for the Auth Code flow.
# SSO_PKCE=true

17
src/api/core/organizations.rs

@ -917,16 +917,21 @@ async fn get_org_details_impl(
Ok(json!(ciphers_json))
}
#[derive(Deserialize)]
struct DomainSSoVerifiedData {
email: String,
}
// Returning a Domain/Organization here allow to prefill it and prevent prompting the user
// So we return a dummy value, since we only support a single SSO integration, and do not use the response anywhere
// In use since `v2025.6.0`, appears to use only the first `organizationIdentifier`
#[post("/organizations/domain/sso/verified")]
fn get_org_domain_sso_verified() -> JsonResult {
// Always return a dummy value, no matter if SSO is enabled or not
#[post("/organizations/domain/sso/verified", data = "<data>")]
fn get_org_domain_sso_verified(data: Json<DomainSSoVerifiedData>) -> JsonResult {
let data = data.into_inner();
Ok(Json(json!({
"object": "list",
"data": [{
"organizationIdentifier": FAKE_SSO_IDENTIFIER,
// The given email is passed to the identity authorize endpoint through the org identifier
"organizationIdentifier": format!("{FAKE_SSO_IDENTIFIER}+{}", data.email),
// These appear to be unused
"organizationName": FAKE_SSO_IDENTIFIER,
"domainName": CONFIG.domain()

19
src/api/identity.rs

@ -24,8 +24,9 @@ use crate::{
master_password_policy,
push::register_push_device,
},
auth,
auth::{AuthMethod, ClientHeaders, ClientIp, ClientVersion, Secure, generate_organization_api_key_login_claims},
auth::{
self, AuthMethod, ClientHeaders, ClientIp, ClientVersion, Secure, generate_organization_api_key_login_claims,
},
crypto,
db::{
DbConn,
@ -36,8 +37,8 @@ use crate::{
},
},
error::MapResult,
mail, sso,
sso::{OIDCCode, OIDCCodeChallenge, OIDCCodeVerifier, OIDCState},
mail,
sso::{self, FAKE_SSO_IDENTIFIER, OIDCCode, OIDCCodeChallenge, OIDCCodeVerifier, OIDCState},
util,
};
@ -1281,7 +1282,6 @@ struct AuthorizeData {
code_challenge_method: String,
#[allow(unused)]
response_mode: Option<String>,
#[allow(unused)]
domain_hint: Option<String>,
#[allow(unused)]
#[field(name = uncased("ssoToken"))]
@ -1297,6 +1297,7 @@ async fn authorize(data: AuthorizeData, cookies: &CookieJar<'_>, secure: Secure,
state,
code_challenge,
code_challenge_method,
domain_hint,
..
} = data;
@ -1309,8 +1310,14 @@ async fn authorize(data: AuthorizeData, cookies: &CookieJar<'_>, secure: Secure,
let binding_token = data_encoding::BASE64URL_NOPAD.encode(&crypto::get_random_bytes::<32>());
let binding_hash = crypto::sha256_hex(binding_token.as_bytes());
let login_hint = domain_hint.and_then(|hint| {
let (sso_org_id, email) = hint.split_once('+')?;
(sso_org_id == FAKE_SSO_IDENTIFIER && !email.is_empty()).then(|| email.to_owned())
});
let auth_url =
sso::authorize_url(state, code_challenge, &client_id, &redirect_uri, Some(binding_hash), conn).await?;
sso::authorize_url(state, code_challenge, &client_id, &redirect_uri, Some(binding_hash), login_hint, conn)
.await?;
cookies.add(
Cookie::build((SSO_BINDING_COOKIE, binding_token))

2
src/config.rs

@ -831,6 +831,8 @@ make_config! {
sso_scopes: String, true, def, "email profile".to_owned();
/// Authorization request extra parameters
sso_authorize_extra_params: String, true, def, String::new();
/// Use login hint in authorization request |> Use the email provided during SSO logon to indicate the account to use to the OIDC provider.
sso_login_hint: bool, true, def, true;
/// Use PKCE during Authorization flow
sso_pkce: bool, true, def, true;
/// Regex for additional trusted Id token audience |> By default only the client_id is trusted.

4
src/sso.rs

@ -191,6 +191,7 @@ pub async fn authorize_url(
client_id: &str,
raw_redirect_uri: &str,
binding_hash: Option<String>,
login_hint: Option<String>,
conn: DbConn,
) -> ApiResult<Url> {
let redirect_uri = match client_id {
@ -209,7 +210,8 @@ pub async fn authorize_url(
_ => err!(format!("Unsupported client {client_id}")),
};
let (auth_url, sso_auth) = Client::authorize_url(state, client_challenge, redirect_uri, binding_hash).await?;
let (auth_url, sso_auth) =
Client::authorize_url(state, client_challenge, redirect_uri, binding_hash, login_hint).await?;
sso_auth.save(&conn).await?;
Ok(auth_url)
}

7
src/sso_client.rs

@ -187,6 +187,7 @@ impl Client {
client_challenge: OIDCCodeChallenge,
redirect_uri: String,
binding_hash: Option<String>,
login_hint: Option<String>,
) -> ApiResult<(Url, SsoAuth)> {
let scopes = CONFIG.sso_scopes_vec().into_iter().map(Scope::new);
let base64_state = data_encoding::BASE64.encode(state.to_string().as_bytes());
@ -208,6 +209,12 @@ impl Client {
.add_extra_param("code_challenge_method", "S256");
}
if CONFIG.sso_login_hint()
&& let Some(value) = login_hint
{
auth_req = auth_req.add_extra_param("login_hint", value);
}
let (auth_url, _, nonce) = auth_req.url();
Ok((auth_url, SsoAuth::new(state, client_challenge, nonce.secret().clone(), redirect_uri, binding_hash)))
}

Loading…
Cancel
Save