1
0
Fork 0
mirror of https://gitlab.com/famedly/conduit.git synced 2025-06-27 16:35:59 +00:00
conduit/src/api/client_server/sso.rs

400 lines
12 KiB
Rust
Raw Normal View History

2024-07-11 21:55:52 +02:00
use std::{borrow::Borrow, collections::HashMap, iter::Iterator, time::SystemTime};
use crate::{
2024-07-11 22:24:22 +02:00
config::IdpConfig,
2024-07-11 21:55:52 +02:00
service::sso::{
2024-07-11 22:24:22 +02:00
LoginToken, ValidationData, SSO_AUTH_EXPIRATION_SECS, SSO_SESSION_COOKIE, SUBJECT_CLAIM_KEY,
2024-07-11 21:55:52 +02:00
},
services, utils, Error, Result, Ruma,
};
use axum::{
response::{AppendHeaders, IntoResponse, Redirect},
RequestExt,
};
use axum_extra::{
2024-07-11 22:24:22 +02:00
headers::{self},
2024-07-11 21:55:52 +02:00
TypedHeader,
};
use http::header;
use mas_oidc_client::{
requests::{
2024-07-11 22:24:22 +02:00
authorization_code::{self, AuthorizationRequestData},
2024-07-11 21:55:52 +02:00
jose::{self, JwtVerificationData},
userinfo,
},
types::{
client_credentials::ClientCredentials,
errors::ClientError,
iana::jose::JsonWebSignatureAlg,
requests::{AccessTokenResponse, AuthorizationResponse},
},
};
2024-07-11 22:24:22 +02:00
use rand::{rngs::StdRng, Rng, SeedableRng};
2024-07-11 21:55:52 +02:00
use ruma::{
api::client::{
error::ErrorKind,
2024-07-11 22:24:22 +02:00
session::{sso_login, sso_login_with_provider},
2024-07-11 21:55:52 +02:00
},
2024-07-11 22:24:22 +02:00
events::{room::message::RoomMessageEventContent, GlobalAccountDataEventType},
push, UserId,
2024-07-11 21:55:52 +02:00
};
2024-07-11 22:24:22 +02:00
use serde_json::Value;
use tracing::{error, info, warn};
2024-07-11 21:55:52 +02:00
use url::Url;
pub const CALLBACK_PATH: &str = "_matrix/client/unstable/sso/callback";
/// # `GET /_matrix/client/v3/login/sso/redirect`
///
/// Redirect the user to the SSO interface.
/// TODO: this should be removed once Ruma supports trailing slashes.
2024-07-11 22:24:22 +02:00
pub async fn get_sso_redirect_route(
Ruma {
body,
sender_user,
sender_device,
sender_servername,
json_body,
..
}: Ruma<sso_login::v3::Request>,
2024-07-11 21:55:52 +02:00
) -> Result<sso_login::v3::Response> {
let sso_login_with_provider::v3::Response { location, cookie } =
2024-07-11 22:24:22 +02:00
get_sso_redirect_with_provider_route(
2024-07-11 21:55:52 +02:00
Ruma {
body: sso_login_with_provider::v3::Request::new(
Default::default(),
2024-07-11 22:24:22 +02:00
body.redirect_url,
2024-07-11 21:55:52 +02:00
),
2024-07-11 22:24:22 +02:00
sender_user,
sender_device,
sender_servername,
json_body,
appservice_info: None,
2024-07-11 21:55:52 +02:00
}
.into(),
)
.await?;
Ok(sso_login::v3::Response { location, cookie })
}
/// # `GET /_matrix/client/v3/login/sso/redirect/{idpId}`
///
/// Redirects the user to the SSO interface.
2024-07-11 22:24:22 +02:00
pub async fn get_sso_redirect_with_provider_route(
2024-07-11 21:55:52 +02:00
body: Ruma<sso_login_with_provider::v3::Request>,
) -> Result<sso_login_with_provider::v3::Response> {
let idp_ids: Vec<&str> = services()
.globals
.config
.idps
.iter()
.map(Borrow::borrow)
.collect();
let provider = match &*idp_ids {
[] => {
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"Single Sign-On is disabled.",
));
}
[idp_id] => services().sso.get(idp_id).expect("we know it exists"),
[_, ..] => services().sso.get(&body.idp_id).ok_or_else(|| {
Error::BadRequest(ErrorKind::InvalidParam, "Unknown identity provider.")
})?,
};
let redirect_url = body
.redirect_url
.parse::<Url>()
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid redirect_url."))?;
let mut callback = services()
.globals
.well_known_client()
.parse::<Url>()
.map_err(|_| Error::bad_config("Invalid well_known_client url."))?;
callback.set_path(CALLBACK_PATH);
let (auth_url, validation_data) = authorization_code::build_authorization_url(
provider.metadata.authorization_endpoint().clone(),
AuthorizationRequestData::new(
provider.config.client_id.clone(),
provider.config.scopes.clone(),
2024-07-11 22:44:47 +02:00
callback,
2024-07-11 21:55:52 +02:00
),
&mut StdRng::from_entropy(),
)
.map_err(|_| Error::BadRequest(ErrorKind::Unknown, "Failed to build authorization_url."))?;
let signed = services().globals.sign_claims(&ValidationData::new(
2024-07-11 22:24:22 +02:00
Borrow::<str>::borrow(provider).to_owned(),
2024-07-11 22:44:47 +02:00
redirect_url.to_string(),
2024-07-11 21:55:52 +02:00
validation_data,
));
Ok(sso_login_with_provider::v3::Response {
location: auth_url.to_string(),
cookie: Some(
utils::build_cookie(
SSO_SESSION_COOKIE,
&signed,
2024-07-11 22:44:47 +02:00
CALLBACK_PATH,
2024-07-11 21:55:52 +02:00
Some(SSO_AUTH_EXPIRATION_SECS),
)
.to_string(),
),
})
}
2024-07-11 22:24:22 +02:00
async fn handle_callback_helper(req: axum::extract::Request) -> Result<axum::response::Response> {
2024-07-11 21:55:52 +02:00
let query = req.uri().query().ok_or_else(|| {
Error::BadRequest(ErrorKind::MissingParam, "Empty authorization callback.")
})?;
let AuthorizationResponse {
code,
access_token,
token_type,
id_token,
expires_in,
2024-07-11 22:24:22 +02:00
} = serde_html_form::from_str(query).map_err(|_| {
serde_html_form::from_str(query)
.map(ClientError::into)
.unwrap_or_else(|_| {
error!("Failed to deserialize authorization callback: {}", query);
Error::BadRequest(
ErrorKind::Unknown,
"Failed to deserialize authorization callback.",
)
})
2024-07-11 21:55:52 +02:00
})?;
2024-07-11 22:24:22 +02:00
let Ok(Some(cookie)): Result<Option<TypedHeader<headers::Cookie>>, _> = req.extract().await
else {
return Err(Error::BadRequest(
ErrorKind::MissingParam,
"Missing session cookie.",
));
};
2024-07-11 21:55:52 +02:00
let ValidationData {
provider,
2024-07-11 22:44:47 +02:00
redirect_url,
2024-07-11 21:55:52 +02:00
inner: validation_data,
} = services()
.globals
.validate_claims(
cookie.get(SSO_SESSION_COOKIE).ok_or_else(|| {
Error::BadRequest(ErrorKind::MissingParam, "Missing value for session cookie.")
})?,
None,
)
2024-07-11 22:24:22 +02:00
.map_err(|_| {
2024-07-11 21:55:52 +02:00
Error::BadRequest(ErrorKind::InvalidParam, "Invalid value for session cookie.")
})?;
2024-07-11 22:24:22 +02:00
let provider = services().sso.get(&provider).ok_or_else(|| {
2024-07-11 21:55:52 +02:00
Error::BadRequest(
ErrorKind::InvalidParam,
"Unknown provider for session cookie.",
)
})?;
let IdpConfig {
client_id,
client_secret,
auth_method,
..
} = provider.config.clone();
2024-07-11 22:24:22 +02:00
let credentials = match &*auth_method {
2024-07-11 21:55:52 +02:00
"basic" => ClientCredentials::ClientSecretBasic {
client_id,
client_secret,
},
"post" => ClientCredentials::ClientSecretPost {
client_id,
client_secret,
},
_ => todo!(),
};
2024-07-11 22:24:22 +02:00
let ref jwks = jose::fetch_jwks(services().sso.service(), provider.metadata.jwks_uri())
.await
.map_err(|_| Error::bad_config("Failed to fetch signing keys for token endpoint."))?;
let jwt_verification_data = Some(JwtVerificationData {
jwks,
issuer: &provider.config.issuer,
client_id: &provider.config.client_id,
signing_algorithm: &JsonWebSignatureAlg::Rs256,
});
2024-07-11 21:55:52 +02:00
let (
AccessTokenResponse {
access_token,
refresh_token,
token_type,
expires_in,
scope,
..
},
Some(id_token),
) = authorization_code::access_token_with_authorization_code(
services().sso.service(),
2024-07-11 22:24:22 +02:00
credentials,
2024-07-11 21:55:52 +02:00
provider.metadata.token_endpoint(),
2024-07-11 22:24:22 +02:00
code.unwrap_or_default(),
2024-07-11 22:44:47 +02:00
validation_data,
2024-07-11 21:55:52 +02:00
jwt_verification_data,
SystemTime::now().into(),
&mut StdRng::from_entropy(),
)
.await
2024-07-11 22:24:22 +02:00
.map_err(|_| Error::bad_config("Failed to fetch access token."))?
2024-07-11 21:55:52 +02:00
else {
unreachable!("ID token should never be empty")
};
let mut userinfo = HashMap::default();
if let Some(endpoint) = &provider.metadata.userinfo_endpoint {
userinfo = userinfo::fetch_userinfo(
services().sso.service(),
endpoint,
&access_token,
jwt_verification_data,
&id_token,
)
.await
2024-07-11 22:24:22 +02:00
.map_err(|_| Error::bad_config("Failed to fetch claims for userinfo endpoint."))?;
2024-07-11 21:55:52 +02:00
};
2024-07-11 22:24:22 +02:00
let (_, id_token) = id_token.into_parts();
2024-07-11 21:55:52 +02:00
2024-07-11 22:24:22 +02:00
let subject = match id_token.get(SUBJECT_CLAIM_KEY) {
Some(Value::String(s)) => s.to_owned(),
Some(Value::Number(n)) => n.to_string(),
value => {
2024-07-11 21:55:52 +02:00
return Err(Error::BadRequest(
2024-07-11 22:24:22 +02:00
ErrorKind::Unknown,
value
.map(|_| {
error!("Subject claim is missing from ID token: {id_token:?}");
"Subject claim is missing from ID token."
})
.unwrap_or("Subject claim should be a string or number."),
));
2024-07-11 21:55:52 +02:00
}
};
2024-07-11 22:24:22 +02:00
let user_id = match services()
2024-07-11 21:55:52 +02:00
.sso
2024-07-11 22:24:22 +02:00
.user_from_subject(Borrow::<str>::borrow(provider), &subject)?
{
Some(user_id) => user_id,
None => {
let mut localpart = subject.clone();
let user_id = loop {
match UserId::parse_with_server_name(&*localpart, services().globals.server_name())
{
Ok(user_id) if services().users.exists(&user_id)? => break user_id,
_ => {
let n: u8 = rand::thread_rng().gen();
localpart = format!("{}{}", localpart, n % 10);
2024-07-11 21:55:52 +02:00
}
}
2024-07-11 22:24:22 +02:00
};
services().users.set_placeholder_password(&user_id)?;
let mut displayname = id_token
.get("preferred_username")
.or(id_token.get("nickname"))
.as_deref()
.map(Value::to_string)
.unwrap_or(user_id.localpart().to_owned());
// If enabled append lightning bolt to display name (default true)
if services().globals.enable_lightning_bolt() {
displayname.push_str(" ⚡️");
2024-07-11 21:55:52 +02:00
}
2024-07-11 22:24:22 +02:00
services()
.users
.set_displayname(&user_id, Some(displayname.clone()))?;
// Initial account data
services().account_data.update(
None,
&user_id,
GlobalAccountDataEventType::PushRules.to_string().into(),
&serde_json::to_value(ruma::events::push_rules::PushRulesEvent {
content: ruma::events::push_rules::PushRulesEventContent {
global: push::Ruleset::server_default(&user_id),
},
})
.expect("to json always works"),
)?;
info!("New user {} registered on this server.", user_id);
services()
.admin
.send_message(RoomMessageEventContent::notice_plain(format!(
"New user {user_id} registered on this server."
)));
if let Some(admin_room) = services().admin.get_admin_room()? {
if services()
.rooms
.state_cache
.room_joined_count(&admin_room)?
== Some(1)
{
services()
.admin
.make_user_admin(&user_id, displayname)
.await?;
warn!("Granting {} admin privileges as the first user", user_id);
2024-07-11 21:55:52 +02:00
}
}
2024-07-11 22:24:22 +02:00
user_id
2024-07-11 21:55:52 +02:00
}
};
2024-07-11 22:24:22 +02:00
let signed = services().globals.sign_claims(&LoginToken::new(
Borrow::<str>::borrow(provider).to_owned(),
user_id,
));
2024-07-11 21:55:52 +02:00
2024-07-11 22:44:47 +02:00
let mut redirect_url: Url = redirect_url.parse().expect("");
redirect_url
2024-07-11 22:24:22 +02:00
.query_pairs_mut()
.append_pair("loginToken", &signed);
2024-07-11 21:55:52 +02:00
Ok((
2024-07-11 22:24:22 +02:00
AppendHeaders(vec![(
2024-07-11 21:55:52 +02:00
header::SET_COOKIE,
2024-07-11 22:24:22 +02:00
utils::build_cookie(SSO_SESSION_COOKIE, "", CALLBACK_PATH, None).to_string(),
2024-07-11 21:55:52 +02:00
)]),
2024-07-11 22:44:47 +02:00
Redirect::temporary(redirect_url.as_str()),
2024-07-11 21:55:52 +02:00
)
.into_response())
}
2024-07-11 22:24:22 +02:00
/// # `GET /_conduit/client/sso/callback`
///
/// Validate the authorization response received from the identity provider.
/// On success, generate a login token, add it to `redirectUrl` as a query and perform the redirect.
/// If this is the first login, register the user, possibly interactively through a fallback page.
pub async fn handle_callback_route(req: axum::extract::Request) -> axum::response::Response {
match handle_callback_helper(req).await {
Ok(res) => res,
Err(e) => e.into_response(),
}
2024-07-11 21:55:52 +02:00
}