mirror of
https://gitlab.com/famedly/conduit.git
synced 2025-06-27 16:35:59 +00:00
feat: base support
This commit is contained in:
parent
139588b64c
commit
67c23d6dd4
9 changed files with 148 additions and 178 deletions
28
Cargo.toml
28
Cargo.toml
|
@ -35,7 +35,7 @@ axum = { version = "0.7", default-features = false, features = [
|
||||||
"json",
|
"json",
|
||||||
"matched-path",
|
"matched-path",
|
||||||
], optional = true }
|
], optional = true }
|
||||||
axum-extra = { version = "0.9", features = ["typed-header", "cookie"] }
|
axum-extra = { version = "0.9", features = ["cookie", "typed-header"] }
|
||||||
axum-server = { version = "0.6", features = ["tls-rustls"] }
|
axum-server = { version = "0.6", features = ["tls-rustls"] }
|
||||||
tower = { version = "0.4.13", features = ["util"] }
|
tower = { version = "0.4.13", features = ["util"] }
|
||||||
tower-http = { version = "0.5", features = [
|
tower-http = { version = "0.5", features = [
|
||||||
|
@ -49,15 +49,6 @@ tower-http = { version = "0.5", features = [
|
||||||
"trace",
|
"trace",
|
||||||
"util",
|
"util",
|
||||||
] }
|
] }
|
||||||
# tower-http = { version = "0.5", features = [
|
|
||||||
# "add-extension",
|
|
||||||
# "cors",
|
|
||||||
# "decompression-full",
|
|
||||||
# "sensitive-headers",
|
|
||||||
# "set-header",
|
|
||||||
# "trace",
|
|
||||||
# "util",
|
|
||||||
# ] }
|
|
||||||
tower-service = "0.3"
|
tower-service = "0.3"
|
||||||
|
|
||||||
# Async runtime and utilities
|
# Async runtime and utilities
|
||||||
|
@ -153,11 +144,6 @@ figment = { version = "0.10.8", features = ["env", "toml"] }
|
||||||
# Validating urls in config
|
# Validating urls in config
|
||||||
url = { version = "2", features = ["serde"] }
|
url = { version = "2", features = ["serde"] }
|
||||||
|
|
||||||
# HTML
|
|
||||||
mas-oidc-client = { git = "https://github.com/matrix-org/matrix-authentication-service", default-features = false }
|
|
||||||
mas-http = { git = "https://github.com/matrix-org/matrix-authentication-service", features = ["client"] }
|
|
||||||
maud = { version = "0.26.0", default-features = false, features = ["axum"] }
|
|
||||||
|
|
||||||
async-trait = "0.1.68"
|
async-trait = "0.1.68"
|
||||||
tikv-jemallocator = { version = "0.5.0", features = [
|
tikv-jemallocator = { version = "0.5.0", features = [
|
||||||
"unprefixed_malloc_on_supported_platforms",
|
"unprefixed_malloc_on_supported_platforms",
|
||||||
|
@ -190,11 +176,21 @@ optional = true
|
||||||
package = "rust-rocksdb"
|
package = "rust-rocksdb"
|
||||||
version = "0.25"
|
version = "0.25"
|
||||||
|
|
||||||
|
[dependencies.mas-http]
|
||||||
|
features = ["client"]
|
||||||
|
git = "https://github.com/matrix-org/matrix-authentication-service"
|
||||||
|
rev = "fbc360d1a94ef2ebf63d979bb403228a700f43c8"
|
||||||
|
|
||||||
|
[dependencies.mas-oidc-client]
|
||||||
|
features = []
|
||||||
|
git = "https://github.com/matrix-org/matrix-authentication-service"
|
||||||
|
rev = "fbc360d1a94ef2ebf63d979bb403228a700f43c8"
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
nix = { version = "0.28", features = ["resource"] }
|
nix = { version = "0.28", features = ["resource"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["backend_sqlite", "conduit_bin"]
|
default = ["backend_rocksdb", "backend_sqlite", "conduit_bin", "systemd"]
|
||||||
#backend_sled = ["sled"]
|
#backend_sled = ["sled"]
|
||||||
backend_persy = ["parking_lot", "persy"]
|
backend_persy = ["parking_lot", "persy"]
|
||||||
backend_sqlite = ["sqlite"]
|
backend_sqlite = ["sqlite"]
|
||||||
|
|
|
@ -124,25 +124,6 @@ Identity providers using OAuth such as Github are not supported yet.
|
||||||
| `name` | `string` | The name displayed on fallback pages. | `issuer` |
|
| `name` | `string` | The name displayed on fallback pages. | `issuer` |
|
||||||
| `icon` | `Url` OR `MxcUri` | The icon displayed on fallback pages. | N/A |
|
| `icon` | `Url` OR `MxcUri` | The icon displayed on fallback pages. | N/A |
|
||||||
| `scopes` | `array` | The scopes used to obtain extra claims which can be used for templates. | `["openid"]` |
|
| `scopes` | `array` | The scopes used to obtain extra claims which can be used for templates. | `["openid"]` |
|
||||||
<!-- | `pkce` | `bool` | | `true` | -->
|
|
||||||
<!-- | `backchannel_logout` | `bool` | | `true` | -->
|
|
||||||
<!-- | `unique_claim` | `string` | The key of the claim, used to uniquely identify users | `"sub"` | <!-1- TODO: claim_correlation? -1-> -->
|
|
||||||
<!-- | `credentials`* | `table` | See [Client Credentials](#client-credentials) | N/A | -->
|
|
||||||
| `client_id`* | `string` | The provider-supplied, unique ID for the client. | N/A |
|
| `client_id`* | `string` | The provider-supplied, unique ID for the client. | N/A |
|
||||||
| `client_secret`* | `string` | The provider-supplied, unique ID for the client. | N/A |
|
| `client_secret`* | `string` | The provider-supplied, unique ID for the client. | N/A |
|
||||||
| `authentication_method`* | `"basic" | "post"` | The method used for client authentication. | N/A |
|
| `authentication_method`* | `"basic" OR "post"` | The method used for client authentication. | N/A |
|
||||||
|
|
||||||
<!-- TODO -->
|
|
||||||
<!-- #### Example -->
|
|
||||||
<!-- ```toml -->
|
|
||||||
<!-- [global.sso.keycloak] -->
|
|
||||||
<!-- name = "A Mysterious KeyCloak Server" -->
|
|
||||||
<!-- icon = "mxc://matrix.org/tuKmXlmbHzYPFmdHafbZHOWj" -->
|
|
||||||
<!-- issuer = "https://oidc.conduit.rs:8443/realms/dev_team_realm" -->
|
|
||||||
<!-- scopes = ["openid", "profile"] -->
|
|
||||||
<!-- ``` -->
|
|
||||||
<!-- localpart = "userinfo.preferred_username" -->
|
|
||||||
<!-- displayname = "id_token.name" -->
|
|
||||||
<!-- avatar_url = "userinfo.picture" -->
|
|
||||||
<!-- email = "userinfo.email" -->
|
|
||||||
<!-- msisdn = "userinfo.phone_number" -->
|
|
||||||
|
|
|
@ -322,8 +322,6 @@ pub async fn change_password_route(
|
||||||
.ok_or_else(|| Error::BadRequest(ErrorKind::MissingToken, "Missing access token."))?;
|
.ok_or_else(|| Error::BadRequest(ErrorKind::MissingToken, "Missing access token."))?;
|
||||||
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
|
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
|
||||||
|
|
||||||
// if services().users.password_hash(sender_user)? == Some("");
|
|
||||||
|
|
||||||
let mut uiaainfo = UiaaInfo {
|
let mut uiaainfo = UiaaInfo {
|
||||||
flows: vec![AuthFlow {
|
flows: vec![AuthFlow {
|
||||||
stages: vec![AuthType::Password],
|
stages: vec![AuthType::Password],
|
||||||
|
|
|
@ -100,6 +100,12 @@ pub async fn upload_signing_keys_route(
|
||||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||||
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
|
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
|
||||||
|
|
||||||
|
let master_key = services()
|
||||||
|
.users
|
||||||
|
.get_master_key(Some(sender_user), sender_user, &|other| {
|
||||||
|
sender_user == other
|
||||||
|
})?;
|
||||||
|
|
||||||
// UIAA
|
// UIAA
|
||||||
let mut uiaainfo = UiaaInfo {
|
let mut uiaainfo = UiaaInfo {
|
||||||
flows: vec![AuthFlow {
|
flows: vec![AuthFlow {
|
||||||
|
@ -111,11 +117,15 @@ pub async fn upload_signing_keys_route(
|
||||||
auth_error: None,
|
auth_error: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let master_key = services()
|
if let (Some(master_key), None) = (&body.master_key, master_key) {
|
||||||
.users
|
services().users.add_cross_signing_keys(
|
||||||
.get_master_key(None, sender_user, &|user_id| user_id == sender_user)?;
|
sender_user,
|
||||||
|
master_key,
|
||||||
if let Some(auth) = &body.auth {
|
&body.self_signing_key,
|
||||||
|
&body.user_signing_key,
|
||||||
|
true,
|
||||||
|
)?;
|
||||||
|
} else if let Some(auth) = &body.auth {
|
||||||
let (worked, uiaainfo) =
|
let (worked, uiaainfo) =
|
||||||
services()
|
services()
|
||||||
.uiaa
|
.uiaa
|
||||||
|
@ -130,20 +140,10 @@ pub async fn upload_signing_keys_route(
|
||||||
.uiaa
|
.uiaa
|
||||||
.create(sender_user, sender_device, &uiaainfo, &json)?;
|
.create(sender_user, sender_device, &uiaainfo, &json)?;
|
||||||
return Err(Error::Uiaa(uiaainfo));
|
return Err(Error::Uiaa(uiaainfo));
|
||||||
} else if master_key.is_some() {
|
} else {
|
||||||
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
|
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(master_key) = &body.master_key {
|
|
||||||
services().users.add_cross_signing_keys(
|
|
||||||
sender_user,
|
|
||||||
master_key,
|
|
||||||
&body.self_signing_key,
|
|
||||||
&body.user_signing_key,
|
|
||||||
true, // notify so that other users see the new keys
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(upload_signing_keys::v3::Response {})
|
Ok(upload_signing_keys::v3::Response {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -113,20 +113,24 @@ pub async fn login_route(body: Ruma<login::v3::Request>) -> Result<login::v3::Re
|
||||||
login::v3::LoginInfo::Token(login::v3::Token { token }) => {
|
login::v3::LoginInfo::Token(login::v3::Token { token }) => {
|
||||||
match (
|
match (
|
||||||
services().globals.jwt_decoding_key(),
|
services().globals.jwt_decoding_key(),
|
||||||
services().sso.login_type().next().is_some(),
|
services().globals.config.idps.is_empty(),
|
||||||
) {
|
) {
|
||||||
(_, false) => {
|
(_, false) => {
|
||||||
let mut validation = Validation::new(Algorithm::HS256);
|
let mut v = Validation::new(Algorithm::HS256);
|
||||||
validation.validate_nbf = false;
|
|
||||||
validation.set_required_spec_claims(&["sub", "exp", "aud", "iss"]);
|
v.set_required_spec_claims(&["sub", "exp", "aud", "iss"]);
|
||||||
|
v.validate_aud = false;
|
||||||
|
v.validate_nbf = false;
|
||||||
|
|
||||||
services()
|
services()
|
||||||
.globals
|
.globals
|
||||||
.validate_claims::<LoginToken>(token, Some(validation))
|
.validate_claims::<LoginToken>(token, Some(&v))
|
||||||
.as_ref()
|
|
||||||
.map(LoginToken::audience)
|
.map(LoginToken::audience)
|
||||||
.map(ToOwned::to_owned)
|
.map_err(|e| {
|
||||||
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid token."))?
|
tracing::warn!("Invalid token: {}", e);
|
||||||
|
|
||||||
|
Error::BadRequest(ErrorKind::InvalidParam, "Invalid token.")
|
||||||
|
})?
|
||||||
}
|
}
|
||||||
(Some(jwt_decoding_key), _) => {
|
(Some(jwt_decoding_key), _) => {
|
||||||
let token = jsonwebtoken::decode::<Claims>(
|
let token = jsonwebtoken::decode::<Claims>(
|
||||||
|
|
|
@ -7,15 +7,7 @@ use crate::{
|
||||||
},
|
},
|
||||||
services, utils, Error, Result, Ruma,
|
services, utils, Error, Result, Ruma,
|
||||||
};
|
};
|
||||||
use axum::{
|
use futures_util::TryFutureExt;
|
||||||
response::{AppendHeaders, IntoResponse, Redirect},
|
|
||||||
RequestExt,
|
|
||||||
};
|
|
||||||
use axum_extra::{
|
|
||||||
headers::{self},
|
|
||||||
TypedHeader,
|
|
||||||
};
|
|
||||||
use http::header::{self};
|
|
||||||
use mas_oidc_client::{
|
use mas_oidc_client::{
|
||||||
requests::{
|
requests::{
|
||||||
authorization_code::{self, AuthorizationRequestData},
|
authorization_code::{self, AuthorizationRequestData},
|
||||||
|
@ -24,7 +16,6 @@ use mas_oidc_client::{
|
||||||
},
|
},
|
||||||
types::{
|
types::{
|
||||||
client_credentials::ClientCredentials,
|
client_credentials::ClientCredentials,
|
||||||
errors::ClientError,
|
|
||||||
iana::jose::JsonWebSignatureAlg,
|
iana::jose::JsonWebSignatureAlg,
|
||||||
requests::{AccessTokenResponse, AuthorizationResponse},
|
requests::{AccessTokenResponse, AuthorizationResponse},
|
||||||
},
|
},
|
||||||
|
@ -33,6 +24,7 @@ use rand::{rngs::StdRng, Rng, SeedableRng};
|
||||||
use ruma::{
|
use ruma::{
|
||||||
api::client::{
|
api::client::{
|
||||||
error::ErrorKind,
|
error::ErrorKind,
|
||||||
|
media::create_content,
|
||||||
session::{sso_login, sso_login_with_provider},
|
session::{sso_login, sso_login_with_provider},
|
||||||
},
|
},
|
||||||
events::{room::message::RoomMessageEventContent, GlobalAccountDataEventType},
|
events::{room::message::RoomMessageEventContent, GlobalAccountDataEventType},
|
||||||
|
@ -46,7 +38,7 @@ pub const CALLBACK_PATH: &str = "/_matrix/client/unstable/conduit/callback";
|
||||||
|
|
||||||
/// # `GET /_matrix/client/v3/login/sso/redirect`
|
/// # `GET /_matrix/client/v3/login/sso/redirect`
|
||||||
///
|
///
|
||||||
/// Redirect the user to the SSO interface.
|
/// Redirect the user to the SSO interfa.
|
||||||
/// TODO: this should be removed once Ruma supports trailing slashes.
|
/// TODO: this should be removed once Ruma supports trailing slashes.
|
||||||
pub async fn get_sso_redirect_route(
|
pub async fn get_sso_redirect_route(
|
||||||
Ruma {
|
Ruma {
|
||||||
|
@ -148,37 +140,25 @@ pub async fn get_sso_redirect_with_provider_route(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_callback_helper(req: axum::extract::Request) -> Result<axum::response::Response> {
|
/// # `GET /_conduit/client/sso/callback`
|
||||||
let query = req.uri().query().ok_or_else(|| {
|
///
|
||||||
Error::BadRequest(ErrorKind::MissingParam, "Empty authorization 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.
|
||||||
let AuthorizationResponse {
|
pub async fn handle_callback_route(
|
||||||
code,
|
body: Ruma<sso_callback::Request>,
|
||||||
access_token: _,
|
) -> Result<sso_login_with_provider::v3::Response> {
|
||||||
token_type: _,
|
let sso_callback::Request {
|
||||||
id_token: _,
|
response:
|
||||||
expires_in: _,
|
AuthorizationResponse {
|
||||||
} = serde_html_form::from_str(query).map_err(|_| {
|
code,
|
||||||
serde_html_form::from_str(query)
|
access_token: _,
|
||||||
.map(ClientError::into)
|
token_type: _,
|
||||||
.unwrap_or_else(|_| {
|
id_token: _,
|
||||||
error!("Failed to deserialize authorization callback: {}", query);
|
expires_in: _,
|
||||||
|
},
|
||||||
Error::BadRequest(
|
cookie,
|
||||||
ErrorKind::Unknown,
|
} = body.body;
|
||||||
"Failed to deserialize authorization callback.",
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let Ok(Some(cookie)): Result<Option<TypedHeader<headers::Cookie>>, _> = req.extract().await
|
|
||||||
else {
|
|
||||||
return Err(Error::BadRequest(
|
|
||||||
ErrorKind::MissingParam,
|
|
||||||
"Missing session cookie.",
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
let ValidationData {
|
let ValidationData {
|
||||||
provider,
|
provider,
|
||||||
|
@ -186,12 +166,7 @@ async fn handle_callback_helper(req: axum::extract::Request) -> Result<axum::res
|
||||||
inner: validation_data,
|
inner: validation_data,
|
||||||
} = services()
|
} = services()
|
||||||
.globals
|
.globals
|
||||||
.validate_claims(
|
.validate_claims(&cookie, None)
|
||||||
cookie.get(SSO_SESSION_COOKIE).ok_or_else(|| {
|
|
||||||
Error::BadRequest(ErrorKind::MissingParam, "Missing value for session cookie.")
|
|
||||||
})?,
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
Error::BadRequest(ErrorKind::InvalidParam, "Invalid value for session cookie.")
|
Error::BadRequest(ErrorKind::InvalidParam, "Invalid value for session cookie.")
|
||||||
})?;
|
})?;
|
||||||
|
@ -224,7 +199,7 @@ async fn handle_callback_helper(req: axum::extract::Request) -> Result<axum::res
|
||||||
let ref jwks = jose::fetch_jwks(services().sso.service(), provider.metadata.jwks_uri())
|
let ref jwks = jose::fetch_jwks(services().sso.service(), provider.metadata.jwks_uri())
|
||||||
.await
|
.await
|
||||||
.map_err(|_| Error::bad_config("Failed to fetch signing keys for token endpoint."))?;
|
.map_err(|_| Error::bad_config("Failed to fetch signing keys for token endpoint."))?;
|
||||||
let jwt_verification_data = Some(JwtVerificationData {
|
let idt_verification_data = Some(JwtVerificationData {
|
||||||
jwks,
|
jwks,
|
||||||
issuer: &provider.config.issuer,
|
issuer: &provider.config.issuer,
|
||||||
client_id: &provider.config.client_id,
|
client_id: &provider.config.client_id,
|
||||||
|
@ -247,7 +222,7 @@ async fn handle_callback_helper(req: axum::extract::Request) -> Result<axum::res
|
||||||
provider.metadata.token_endpoint(),
|
provider.metadata.token_endpoint(),
|
||||||
code.unwrap_or_default(),
|
code.unwrap_or_default(),
|
||||||
validation_data,
|
validation_data,
|
||||||
jwt_verification_data,
|
idt_verification_data,
|
||||||
SystemTime::now().into(),
|
SystemTime::now().into(),
|
||||||
&mut StdRng::from_entropy(),
|
&mut StdRng::from_entropy(),
|
||||||
)
|
)
|
||||||
|
@ -257,21 +232,28 @@ async fn handle_callback_helper(req: axum::extract::Request) -> Result<axum::res
|
||||||
unreachable!("ID token should never be empty")
|
unreachable!("ID token should never be empty")
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut _userinfo = HashMap::default();
|
let mut userinfo = HashMap::default();
|
||||||
if let Some(endpoint) = provider.metadata.userinfo_endpoint.as_ref() {
|
if let Some(endpoint) = provider.metadata.userinfo_endpoint.as_ref() {
|
||||||
_userinfo = userinfo::fetch_userinfo(
|
userinfo = userinfo::fetch_userinfo(
|
||||||
services().sso.service(),
|
services().sso.service(),
|
||||||
endpoint,
|
endpoint,
|
||||||
&access_token,
|
&access_token,
|
||||||
jwt_verification_data,
|
None,
|
||||||
&id_token,
|
&id_token,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| Error::bad_config("Failed to fetch claims for userinfo endpoint."))?;
|
.map_err(|e| {
|
||||||
|
tracing::error!("Failed to fetch claims for userinfo endpoint: {:?}", e);
|
||||||
|
|
||||||
|
Error::bad_config("Failed to fetch claims for userinfo endpoint.")
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let (_, id_token) = id_token.into_parts();
|
let (_, id_token) = id_token.into_parts();
|
||||||
|
|
||||||
|
info!("userinfo: {:?}", &userinfo);
|
||||||
|
info!("id_token: {:?}", &id_token);
|
||||||
|
|
||||||
let subject = match id_token.get(SUBJECT_CLAIM_KEY) {
|
let subject = match id_token.get(SUBJECT_CLAIM_KEY) {
|
||||||
Some(Value::String(s)) => s.to_owned(),
|
Some(Value::String(s)) => s.to_owned(),
|
||||||
Some(Value::Number(n)) => n.to_string(),
|
Some(Value::Number(n)) => n.to_string(),
|
||||||
|
@ -299,8 +281,13 @@ async fn handle_callback_helper(req: axum::extract::Request) -> Result<axum::res
|
||||||
|
|
||||||
let user_id = loop {
|
let user_id = loop {
|
||||||
match UserId::parse_with_server_name(&*localpart, services().globals.server_name())
|
match UserId::parse_with_server_name(&*localpart, services().globals.server_name())
|
||||||
{
|
.map(|user_id| {
|
||||||
Ok(user_id) if services().users.exists(&user_id)? => break user_id,
|
(
|
||||||
|
user_id.clone(),
|
||||||
|
services().users.exists(&user_id).unwrap_or(true),
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
Ok((user_id, false)) => break user_id,
|
||||||
_ => {
|
_ => {
|
||||||
let n: u8 = rand::thread_rng().gen();
|
let n: u8 = rand::thread_rng().gen();
|
||||||
|
|
||||||
|
@ -310,12 +297,15 @@ async fn handle_callback_helper(req: axum::extract::Request) -> Result<axum::res
|
||||||
};
|
};
|
||||||
|
|
||||||
services().users.set_placeholder_password(&user_id)?;
|
services().users.set_placeholder_password(&user_id)?;
|
||||||
let mut displayname = id_token
|
let displayname = id_token
|
||||||
.get("preferred_username")
|
.get("preferred_username")
|
||||||
.or(id_token.get("nickname"))
|
.or(id_token.get("nickname"));
|
||||||
|
let mut displayname = displayname
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.map(Value::to_string)
|
.map(Value::as_str)
|
||||||
.unwrap_or(user_id.localpart().to_owned());
|
.flatten()
|
||||||
|
.unwrap_or(user_id.localpart())
|
||||||
|
.to_owned();
|
||||||
|
|
||||||
// If enabled append lightning bolt to display name (default true)
|
// If enabled append lightning bolt to display name (default true)
|
||||||
if services().globals.enable_lightning_bolt() {
|
if services().globals.enable_lightning_bolt() {
|
||||||
|
@ -326,6 +316,34 @@ async fn handle_callback_helper(req: axum::extract::Request) -> Result<axum::res
|
||||||
.users
|
.users
|
||||||
.set_displayname(&user_id, Some(displayname.clone()))?;
|
.set_displayname(&user_id, Some(displayname.clone()))?;
|
||||||
|
|
||||||
|
if let Some(Value::String(url)) = userinfo.get("picture").or(id_token.get("picture")) {
|
||||||
|
let req = services()
|
||||||
|
.globals
|
||||||
|
.default_client()
|
||||||
|
.get(url)
|
||||||
|
.send()
|
||||||
|
.and_then(reqwest::Response::bytes);
|
||||||
|
|
||||||
|
if let Ok(file) = req.await {
|
||||||
|
let _ = crate::api::client_server::create_content_route(Ruma {
|
||||||
|
body: create_content::v3::Request::new(file.to_vec()),
|
||||||
|
sender_user: None,
|
||||||
|
sender_device: None,
|
||||||
|
sender_servername: None,
|
||||||
|
json_body: None,
|
||||||
|
appservice_info: None,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.and_then(|res| {
|
||||||
|
tracing::info!("successfully imported avatar for {}", &user_id);
|
||||||
|
|
||||||
|
services()
|
||||||
|
.users
|
||||||
|
.set_avatar_url(&user_id, Some(res.content_uri))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Initial account data
|
// Initial account data
|
||||||
services().account_data.update(
|
services().account_data.update(
|
||||||
None,
|
None,
|
||||||
|
@ -355,7 +373,7 @@ async fn handle_callback_helper(req: axum::extract::Request) -> Result<axum::res
|
||||||
{
|
{
|
||||||
services()
|
services()
|
||||||
.admin
|
.admin
|
||||||
.make_user_admin(&user_id, displayname)
|
.make_user_admin(&user_id, displayname.to_owned())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
warn!("Granting {} admin privileges as the first user", user_id);
|
warn!("Granting {} admin privileges as the first user", user_id);
|
||||||
|
@ -376,26 +394,10 @@ async fn handle_callback_helper(req: axum::extract::Request) -> Result<axum::res
|
||||||
.query_pairs_mut()
|
.query_pairs_mut()
|
||||||
.append_pair("loginToken", &signed);
|
.append_pair("loginToken", &signed);
|
||||||
|
|
||||||
Ok((
|
Ok(sso_login_with_provider::v3::Response {
|
||||||
AppendHeaders(vec![(
|
location: redirect_url.to_string(),
|
||||||
header::SET_COOKIE,
|
cookie: Some(utils::build_cookie(SSO_SESSION_COOKIE, "", CALLBACK_PATH, None).to_string()),
|
||||||
utils::build_cookie(SSO_SESSION_COOKIE, "", CALLBACK_PATH, None).to_string(),
|
})
|
||||||
)]),
|
|
||||||
Redirect::temporary(redirect_url.as_str()),
|
|
||||||
)
|
|
||||||
.into_response())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// # `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(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mod sso_callback {
|
mod sso_callback {
|
||||||
|
@ -404,9 +406,9 @@ mod sso_callback {
|
||||||
use mas_oidc_client::types::requests::AuthorizationResponse;
|
use mas_oidc_client::types::requests::AuthorizationResponse;
|
||||||
use ruma::{
|
use ruma::{
|
||||||
api::{
|
api::{
|
||||||
client::Error,
|
client::{session::sso_login_with_provider, Error},
|
||||||
error::{FromHttpRequestError, HeaderDeserializationError},
|
error::{FromHttpRequestError, HeaderDeserializationError},
|
||||||
IncomingRequest, Metadata, OutgoingResponse,
|
IncomingRequest, Metadata,
|
||||||
},
|
},
|
||||||
metadata,
|
metadata,
|
||||||
};
|
};
|
||||||
|
@ -423,15 +425,13 @@ mod sso_callback {
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Request {
|
pub struct Request {
|
||||||
response: AuthorizationResponse,
|
pub response: AuthorizationResponse,
|
||||||
cookie: String,
|
pub cookie: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Response {}
|
|
||||||
|
|
||||||
impl IncomingRequest for Request {
|
impl IncomingRequest for Request {
|
||||||
type EndpointError = Error;
|
type EndpointError = Error;
|
||||||
type OutgoingResponse = Response;
|
type OutgoingResponse = sso_login_with_provider::v3::Response;
|
||||||
|
|
||||||
const METADATA: Metadata = METADATA;
|
const METADATA: Metadata = METADATA;
|
||||||
|
|
||||||
|
@ -470,12 +470,4 @@ mod sso_callback {
|
||||||
Ok(Self { response, cookie })
|
Ok(Self { response, cookie })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OutgoingResponse for Response {
|
|
||||||
fn try_into_http_response<T: Default + bytes::BufMut>(
|
|
||||||
self,
|
|
||||||
) -> Result<http::Response<T>, ruma::api::error::IntoHttpError> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
10
src/main.rs
10
src/main.rs
|
@ -9,10 +9,7 @@ use axum::{
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
use axum_server::{bind, bind_rustls, tls_rustls::RustlsConfig, Handle as ServerHandle};
|
use axum_server::{bind, bind_rustls, tls_rustls::RustlsConfig, Handle as ServerHandle};
|
||||||
use conduit::api::{
|
use conduit::api::{client_server, server_server};
|
||||||
client_server::{self, CALLBACK_PATH},
|
|
||||||
server_server,
|
|
||||||
};
|
|
||||||
use figment::{
|
use figment::{
|
||||||
providers::{Env, Format, Toml},
|
providers::{Env, Format, Toml},
|
||||||
Figment,
|
Figment,
|
||||||
|
@ -283,10 +280,7 @@ fn routes(config: &Config) -> Router {
|
||||||
.ruma_route(client_server::get_sso_redirect_with_provider_route)
|
.ruma_route(client_server::get_sso_redirect_with_provider_route)
|
||||||
// The specification will likely never introduce any endpoint for handling authorization callbacks.
|
// The specification will likely never introduce any endpoint for handling authorization callbacks.
|
||||||
// As a workaround, we use custom path that redirects the user to the default login handler.
|
// As a workaround, we use custom path that redirects the user to the default login handler.
|
||||||
.route(
|
.ruma_route(client_server::handle_callback_route)
|
||||||
&format!("/{CALLBACK_PATH}"),
|
|
||||||
get(client_server::handle_callback_route),
|
|
||||||
)
|
|
||||||
.ruma_route(client_server::get_capabilities_route)
|
.ruma_route(client_server::get_capabilities_route)
|
||||||
.ruma_route(client_server::get_pushrules_all_route)
|
.ruma_route(client_server::get_pushrules_all_route)
|
||||||
.ruma_route(client_server::set_pushrule_route)
|
.ruma_route(client_server::set_pushrule_route)
|
||||||
|
|
|
@ -18,7 +18,7 @@ use ruma::{
|
||||||
DeviceId, RoomVersionId, ServerName, UserId,
|
DeviceId, RoomVersionId, ServerName, UserId,
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
collections::{BTreeMap, HashMap},
|
collections::{BTreeMap, HashMap, HashSet},
|
||||||
error::Error as StdError,
|
error::Error as StdError,
|
||||||
fs,
|
fs,
|
||||||
future::{self, Future},
|
future::{self, Future},
|
||||||
|
@ -522,7 +522,7 @@ impl Service {
|
||||||
pub fn validate_claims<T: DeserializeOwned>(
|
pub fn validate_claims<T: DeserializeOwned>(
|
||||||
&self,
|
&self,
|
||||||
token: &str,
|
token: &str,
|
||||||
validation_data: Option<jsonwebtoken::Validation>,
|
validation_data: Option<&jsonwebtoken::Validation>,
|
||||||
) -> jsonwebtoken::errors::Result<T> {
|
) -> jsonwebtoken::errors::Result<T> {
|
||||||
let key = jsonwebtoken::DecodingKey::from_secret(
|
let key = jsonwebtoken::DecodingKey::from_secret(
|
||||||
self.keypair().sign(PROBLEMATIC_CONST).as_bytes(),
|
self.keypair().sign(PROBLEMATIC_CONST).as_bytes(),
|
||||||
|
@ -533,9 +533,9 @@ impl Service {
|
||||||
// these validations are redundant as all JWTs are stored in cookies
|
// these validations are redundant as all JWTs are stored in cookies
|
||||||
v.validate_exp = false;
|
v.validate_exp = false;
|
||||||
v.validate_nbf = false;
|
v.validate_nbf = false;
|
||||||
v.required_spec_claims = Default::default();
|
v.required_spec_claims = HashSet::new();
|
||||||
|
|
||||||
jsonwebtoken::decode::<T>(token, &key, &validation_data.unwrap_or(v))
|
jsonwebtoken::decode::<T>(token, &key, validation_data.unwrap_or(&v))
|
||||||
.map(|data| data.claims)
|
.map(|data| data.claims)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ use crate::{
|
||||||
utils, Error, Result,
|
utils, Error, Result,
|
||||||
};
|
};
|
||||||
use futures_util::future::{self};
|
use futures_util::future::{self};
|
||||||
|
use http::HeaderValue;
|
||||||
use mas_oidc_client::{
|
use mas_oidc_client::{
|
||||||
http_service::HttpService,
|
http_service::HttpService,
|
||||||
requests::{authorization_code::AuthorizationValidationData, discovery},
|
requests::{authorization_code::AuthorizationValidationData, discovery},
|
||||||
|
@ -20,6 +21,7 @@ use ruma::{api::client::session::get_login_types::v3::IdentityProvider, OwnedUse
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::sync::OnceCell;
|
use tokio::sync::OnceCell;
|
||||||
use tower::BoxError;
|
use tower::BoxError;
|
||||||
|
use tower_http::{set_header::SetRequestHeaderLayer, ServiceBuilderExt};
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
@ -43,14 +45,17 @@ impl Service {
|
||||||
pub fn build(db: &'static dyn Data) -> Result<Arc<Self>> {
|
pub fn build(db: &'static dyn Data) -> Result<Arc<Self>> {
|
||||||
let client = tower::ServiceBuilder::new()
|
let client = tower::ServiceBuilder::new()
|
||||||
.map_err(BoxError::from)
|
.map_err(BoxError::from)
|
||||||
|
.layer(tower_http::timeout::TimeoutLayer::new(
|
||||||
|
std::time::Duration::from_secs(10),
|
||||||
|
))
|
||||||
.layer(mas_http::BytesToBodyRequestLayer)
|
.layer(mas_http::BytesToBodyRequestLayer)
|
||||||
.layer(mas_http::BodyToBytesResponseLayer)
|
.layer(mas_http::BodyToBytesResponseLayer)
|
||||||
// .override_request_header(http::header::USER_AGENT, "conduit".to_owned())
|
.layer(SetRequestHeaderLayer::overriding(
|
||||||
// .concurrency_limit(10)
|
http::header::USER_AGENT,
|
||||||
// .follow_redirects()
|
HeaderValue::from_static("conduit/0.9-alpha"),
|
||||||
// .layer(tower_http::timeout::TimeoutLayer::new(
|
))
|
||||||
// std::time::Duration::from_secs(10),
|
.concurrency_limit(10)
|
||||||
// ))
|
.follow_redirects()
|
||||||
.service(mas_http::make_untraced_client());
|
.service(mas_http::make_untraced_client());
|
||||||
|
|
||||||
Ok(Arc::new(Self {
|
Ok(Arc::new(Self {
|
||||||
|
@ -157,8 +162,8 @@ impl LoginToken {
|
||||||
.expect("time overflow"),
|
.expect("time overflow"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn audience(&self) -> &UserId {
|
pub fn audience(self) -> OwnedUserId {
|
||||||
&self.aud
|
self.aud
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue