Draft: SSO login (OAuth 2.0 + OpenID Connect) #1012

Open
avdb13 wants to merge 11 commits from oidc into next
14 changed files with 515 additions and 174 deletions
Showing only changes of commit d22243a357 - Show all commits

341
Cargo.lock generated
View file

@ -65,6 +65,12 @@ version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
[[package]]
name = "anyhow"
version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1"
[[package]]
name = "arc-swap"
version = "1.6.0"
@ -150,6 +156,17 @@ version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f093eed78becd229346bf859eec0aa4dd7ddde0757287b2b4107a1f09c80002"
[[package]]
name = "async-recursion"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.51",
]
[[package]]
name = "async-trait"
version = "0.1.77"
@ -320,6 +337,15 @@ version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "base64-url"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb9fb9fb058cc3063b5fc88d9a21eefa2735871498a04e1650da76ed511c8569"
dependencies = [
"base64 0.21.7",
]
[[package]]
name = "base64ct"
version = "1.6.0"
@ -462,6 +488,33 @@ dependencies = [
"windows-targets 0.52.3",
]
[[package]]
name = "ciborium"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
dependencies = [
"ciborium-io",
"ciborium-ll",
"serde",
]
[[package]]
name = "ciborium-io"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
[[package]]
name = "ciborium-ll"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
dependencies = [
"ciborium-io",
"half",
]
[[package]]
name = "clang-sys"
version = "1.7.0"
@ -542,6 +595,7 @@ dependencies = [
"lru-cache",
"nix",
"num_cpus",
"openid-client",
"openidconnect",
"opentelemetry",
"opentelemetry-jaeger",
@ -671,6 +725,12 @@ version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
[[package]]
name = "crunchy"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "crypto-bigint"
version = "0.5.5"
@ -927,6 +987,16 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "fallible-iterator"
version = "0.2.0"
@ -939,6 +1009,12 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "fastrand"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
[[package]]
name = "fdeflate"
version = "0.3.4"
@ -994,6 +1070,21 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
version = "1.2.1"
@ -1178,6 +1269,16 @@ dependencies = [
"tracing",
]
[[package]]
name = "half"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5eceaaeec696539ddaf7b333340f1af35a5aa87ae3e4f3ead0532f72affab2e"
dependencies = [
"cfg-if",
"crunchy",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
@ -1395,6 +1496,19 @@ dependencies = [
"tokio-rustls",
]
[[package]]
name = "hyper-tls"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
"bytes",
"hyper",
"native-tls",
"tokio",
"tokio-native-tls",
]
[[package]]
name = "iana-time-zone"
version = "0.1.60"
@ -1536,6 +1650,24 @@ version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
[[package]]
name = "josekit"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd20997283339a19226445db97d632c8dc7adb6b8172537fe0e9e540fb141df2"
dependencies = [
"anyhow",
"base64 0.21.7",
"flate2",
"once_cell",
"openssl",
"regex",
"serde",
"serde_json",
"thiserror",
"time",
]
[[package]]
name = "jpeg-decoder"
version = "0.3.1"
@ -1584,6 +1716,35 @@ dependencies = [
"simple_asn1",
]
[[package]]
name = "jwt-compact"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25cb2458ca54de48ef237ac0d68d4e80e512ae81d6aeb9775f4c835da0d193d3"
dependencies = [
"anyhow",
"base64ct",
"chrono",
"ciborium",
"hmac",
"rand_core",
"serde",
"serde_json",
"sha2",
"smallvec",
"subtle",
"zeroize",
]
[[package]]
name = "keccak"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654"
dependencies = [
"cpufeatures",
]
[[package]]
name = "konst"
version = "0.3.8"
@ -1696,6 +1857,12 @@ version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linux-raw-sys"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
[[package]]
name = "lock_api"
version = "0.4.11"
@ -1721,6 +1888,12 @@ dependencies = [
"linked-hash-map",
]
[[package]]
name = "lru_time_cache"
version = "0.11.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9106e1d747ffd48e6be5bb2d97fa706ed25b144fbee4d5c02eae110cd8d6badd"
[[package]]
name = "lz4-sys"
version = "1.9.4"
@ -1822,6 +1995,24 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "native-tls"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
dependencies = [
"lazy_static",
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]]
name = "nix"
version = "0.26.4"
@ -1964,6 +2155,32 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "openid-client"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca96ef162fe17a8487ed6b8ffef725c0417b2554363f6a768770de1e326e6916"
dependencies = [
"async-recursion",
"base64 0.21.7",
"base64-url",
"josekit",
"jwt-compact",
"lazy_static",
"lru_time_cache",
"querystring",
"rand",
"regex",
"reqwest",
"serde",
"serde_json",
"sha2",
"sha3",
"tokio",
"url",
"urlencoding",
]
[[package]]
name = "openidconnect"
version = "3.5.0"
@ -1996,12 +2213,60 @@ dependencies = [
"url",
]
[[package]]
name = "openssl"
version = "0.10.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f"
dependencies = [
"bitflags 2.4.2",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.51",
]
[[package]]
name = "openssl-probe"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-src"
version = "300.2.3+3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cff92b6f71555b61bb9315f7c64da3ca43d87531622120fea0195fc761b4843"
dependencies = [
"cc",
]
[[package]]
name = "openssl-sys"
version = "0.9.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff"
dependencies = [
"cc",
"libc",
"openssl-src",
"pkg-config",
"vcpkg",
]
[[package]]
name = "opentelemetry"
version = "0.18.0"
@ -2364,6 +2629,12 @@ dependencies = [
"yansi",
]
[[package]]
name = "querystring"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9318ead08c799aad12a55a3e78b82e0b6167271ffd1f627b758891282f739187"
[[package]]
name = "quick-error"
version = "1.2.3"
@ -2489,10 +2760,12 @@ dependencies = [
"http-body 0.4.6",
"hyper",
"hyper-rustls",
"hyper-tls",
"ipnet",
"js-sys",
"log",
"mime",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
@ -2505,6 +2778,7 @@ dependencies = [
"sync_wrapper",
"system-configuration",
"tokio",
"tokio-native-tls",
"tokio-rustls",
"tokio-socks",
"tower-service",
@ -2817,6 +3091,19 @@ dependencies = [
"semver",
]
[[package]]
name = "rustix"
version = "0.38.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949"
dependencies = [
"bitflags 2.4.2",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
]
[[package]]
name = "rustls"
version = "0.21.10"
@ -2995,6 +3282,7 @@ version = "1.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
dependencies = [
"indexmap 2.2.3",
"itoa",
"ryu",
"serde",
@ -3116,6 +3404,16 @@ dependencies = [
"digest",
]
[[package]]
name = "sha3"
version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60"
dependencies = [
"digest",
"keccak",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
@ -3285,6 +3583,18 @@ dependencies = [
"libc",
]
[[package]]
name = "tempfile"
version = "3.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
dependencies = [
"cfg-if",
"fastrand",
"rustix",
"windows-sys 0.52.0",
]
[[package]]
name = "thiserror"
version = "1.0.57"
@ -3425,6 +3735,7 @@ dependencies = [
"libc",
"mio",
"num_cpus",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
@ -3443,6 +3754,16 @@ dependencies = [
"syn 2.0.51",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
"native-tls",
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.24.1"
@ -3810,6 +4131,12 @@ dependencies = [
"serde",
]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "uuid"
version = "1.7.0"
@ -4165,6 +4492,20 @@ name = "zeroize"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
dependencies = [
"zeroize_derive",
]
[[package]]
name = "zeroize_derive"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.51",
]
[[package]]
name = "zigzag"

View file

@ -117,15 +117,14 @@ lazy_static = "1.4.0"
async-trait = "0.1.68"
sd-notify = { version = "0.4.1", optional = true }
# Used for SSO through OIDC
openidconnect = { version = "3.5.0", features = ["jwk-alg", "accept-string-booleans"] }
url = { version = "2.5.0", features = ["serde"] }
# Used for SSO as fallback for non-web clients
askama = { version = "0.12.1", features = ["with-axum"] }
askama_axum = { version = "0.4.0", features = ["urlencode", "config"] }
time = "0.3.34"
openid-client = "0.1.2"
openidconnect = "3.5.0"
[target.'cfg(unix)'.dependencies]
nix = { version = "0.26.2", features = ["resource"] }
@ -176,7 +175,7 @@ systemd-units = { unit-name = "matrix-conduit" }
[profile.dev]
lto = 'off'
incremental = false
incremental = true
[profile.release]
lto = 'thin'

View file

@ -58,15 +58,41 @@ address = "127.0.0.1" # This makes sure Conduit can only be reached using the re
macaroon_key = "this is the key" # Currently only used in SSO as short-term login token
[[global.sso]]
id = "authentik"
name = "authentik"
issuer = "https://your.authentik.example.org/application/o/your-app-slug"
scopes = ["openid", "profile", "email"]
[global.sso.client]
id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
[[global.sso]]
id = "github"
name = "Github"
subject_claim = "id"
# localpart = "{{ user.login }}"
# display_name = "{{ user.name }}"
scopes = ["read:user"]
issuer = "https://github.com"
[global.sso.client]
id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
[global.sso.discover.manual]
auth = "https://github.com/login/oauth/authorize"
token = "https://github.com/login/oauth/access_token"
userinfo = "https://api.github.com/user"
[[global.sso]]
id = "gitlab"
name = "Gitlab"
icon = "mxc://kurosaki.cx/KKbSvyoUEYXdrzXBJoOJoLpZbqFYrCpW"
issuer = "https://gitlab.com"
scopes = ["openid", "profile"]
issuer = "https://gitlab.com"
[global.sso.client]
id = "12dd00d057420beda06fd5edcd21287026dc0c66ba5c02d40c2eff8b559c6709"
secret = "3a806573cacf5da560b8c720cf32019255908c83e31ba78d280cf08a4eb619fd"
auth_method = "post"
auth_method = "client_secret_post"

View file

@ -52,7 +52,7 @@
<img src="{{ icon }}"/>
{% when None %}
{% endmatch %}
<span>{{ idp.name.as_deref().unwrap_or(idp.id) }}</span>
<span>{{ idp.name }}</span>
</a>
</li>
{% endfor %}

View file

@ -152,7 +152,7 @@
<img src="{{ icon }}"/>
{% when None %}
{% endmatch %}
Optional data from {{ idp.name.as_deref().unwrap_or(idp.id) }}</h2>
Optional data from {{ idp.name }}</h2>
{% if let Some(avatar_url) = user.avatar_url %}
<label class="idp-detail idp-avatar" for="idp-avatar">
<div class="check-row">

View file

@ -28,7 +28,7 @@ pub async fn get_login_types_route(
.sso
.inner
.iter()
.map(|p| p.config.clone().into())
.map(|p| p.inner.clone())
.collect();
Ok(get_login_types::v3::Response::new(vec![

View file

@ -6,10 +6,8 @@ use askama::Template;
use axum::{body::Full, response::IntoResponse};
use axum_extra::extract::cookie::{Cookie, SameSite};
use bytes::BytesMut;
use http::{HeaderValue, StatusCode};
use openidconnect::{
AuthorizationCode, CsrfToken,
};
use http::{header::SET_COOKIE, HeaderValue, StatusCode};
use openidconnect::{AuthorizationCode, CsrfToken, RedirectUrl};
use ruma::api::{
client::{error::ErrorKind, session},
OutgoingResponse,
@ -38,39 +36,30 @@ pub async fn get_sso_redirect_with_provider(
// State(uiaa_session): State<Option<()>>,
body: Ruma<session::sso_login_with_provider::v3::Request>,
) -> axum::response::Response {
let redirect_url = RedirectUrl::new(body.redirect_url.clone().unwrap()).unwrap();
if services().sso.get_all().is_empty() {
return Error::BadRequest(ErrorKind::NotFound, "SSO has not been configured")
.into_response();
}
if body.idp_id.is_empty() {
return get_sso_fallback_template(body.redirect_url.as_deref().unwrap_or_default())
return get_sso_fallback_template(redirect_url.as_str())
.into_response();
};
let location = Some(body.idp_id.clone());
let (url, nonce, cookie) =
match services().sso.find_one(&body.idp_id).map(|provider| {
provider.handle_redirect(body.redirect_url.unwrap())
}) {
Ok(fut) => fut.await,
let provider = match services()
.sso
.find_one(&body.idp_id)
{
Ok(provider) => provider,
Err(e) => return e.into_response(),
};
let cookie = Cookie::build("openid-state", cookie)
.path("/_conduit/client/sso")
// .secure(false) //FIXME
.secure(true)
.http_only(true)
.same_site(SameSite::None)
.max_age(time::Duration::seconds(COOKIE_STATE_EXPIRATION_SECS))
.finish()
.to_string();
let (url, _, cookie) = provider.handle_redirect(redirect_url).await;
let mut res = session::sso_login_with_provider::v3::Response {
location,
cookie: Some(cookie),
location: Some(url.to_string()),
cookie: Some(cookie.to_string()),
}
.try_into_http_response::<BytesMut>()
.unwrap();
@ -82,8 +71,13 @@ pub async fn get_sso_redirect_with_provider(
fn get_sso_fallback_template(redirect_url: &str) -> axum::response::Response {
let server_name = services().globals.server_name().to_string();
let metadata = services().sso.inner.iter().map(Into::into).collect();
let redirect_url = redirect_url.to_string();
let metadata = services()
.sso
.inner
.iter()
.map(|p| p.inner.clone())
.collect();
let t = templates::IdpPicker {
server_name,
@ -146,7 +140,7 @@ pub async fn get_sso_callback(
Err(error) => return error.into_response(),
};
let provider = match services().sso.find_one(macaroon.idp_id.as_ref()) {
let provider = match services().sso.find_one(&macaroon.idp_id) {
Ok(provider) => provider,
Err(error) => return error.into_response(),
};
@ -155,9 +149,7 @@ pub async fn get_sso_callback(
let user_info = provider.handle_callback(code, macaroon.nonce).await;
(
axum::TypedHeader(axum::headers::Location(
HeaderValue::from_str(clear_cookie.as_str()).unwrap(),
)),
axum::response::AppendHeaders([(SET_COOKIE, cookie)]),
"Hello, World!",
)
.into_response()

View file

@ -1,6 +1,5 @@
use openidconnect::JsonWebKeyId;
use ruma::OwnedMxcUri;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize)]
pub struct ProviderConfig {
@ -37,7 +36,7 @@ pub struct ProviderConfig {
// Should be enabled when the authorization response does not contain userinfo
#[serde(default)]
pub userinfo_override: bool,
pub force_userinfo: bool,
#[serde(default)]
pub discovery: DiscoveryConfig,
@ -48,9 +47,14 @@ pub struct ClientConfig {
pub id: String,
// Mandatory for the following `ClientAuthMethod`s:
// [`Basic`,`Post`,`SharedJwt`]
#[serde(default)]
pub secret: Option<String>,
#[serde(default)]
pub auth_method: AuthMethod,
// #[serde(default)]
// pub private_jwt: Option<PrivateJwt>,
}
#[derive(Clone, Debug, Deserialize)]
@ -59,6 +63,7 @@ pub struct Endpoints {
pub token: Option<url::Url>,
pub userinfo: Option<url::Url>,
pub jwk: Option<url::Url>,
pub ok: AuthMethod,
}
#[derive(Clone, Debug, Default, Deserialize)]
@ -70,30 +75,32 @@ pub enum DiscoveryConfig {
Manual(Endpoints),
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "snake_case")]
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub enum AuthMethod {
#[serde(rename = "none")]
None,
// Provide the client combo in the Authorization header
#[default]
#[serde(rename = "client_secret_basic")]
Basic,
// Provide the client combo as in the POST request body
// Provide the client combo in the POST request body
#[serde(rename = "client_secret_post")]
Post,
// Provide a JWT signed with client secret
#[serde(rename = "client_secret_jwt")]
SharedJwt,
// Provide a JWT signed with a private key (OP needs to know the public key)
#[serde(rename = "private_key_jwt")]
PrivateJwt,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Algorithm {
Rsa,
EdDsa,
}
#[derive(Clone, Debug, Deserialize)]
pub struct PrivateSigningKey {
pub kind: Algorithm,
pub path: String,
pub kid: Option<JsonWebKeyId>,
}
// #[derive(Clone, Debug, Deserialize)]
// pub struct PrivateJwt {
// pub payload:
// pub header: jsonwebtoken::Header,
// pub key: jsonwebtoken::EncodingKey,
// }

View file

@ -24,7 +24,7 @@ use ruma::api::{
IncomingRequest,
};
use tokio::signal;
use tower::{ServiceBuilder, ServiceExt};
use tower::{ServiceBuilder};
use tower_http::{
cors::{self, CorsLayer},
trace::TraceLayer,

View file

@ -160,6 +160,8 @@ impl Service {
}
};
let macaroon_key = config.macaroon_key.clone();
let tls_name_override = Arc::new(RwLock::new(TlsNameMap::new()));
let jwt_decoding_key = config
@ -213,7 +215,7 @@ impl Service {
sync_receivers: RwLock::new(HashMap::new()),
rotate: RotationHandler::new(),
shutdown: AtomicBool::new(false),
macaroon_key: config.macaroon_key.clone(),
macaroon_key,
};
fs::create_dir_all(s.get_media_folder())?;

View file

@ -1,6 +1,7 @@
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation};
use openidconnect::{Nonce, PkceCodeVerifier, RedirectUrl, CsrfToken};
use openidconnect::{CsrfToken, Nonce, RedirectUrl};
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use crate::Error;
@ -9,12 +10,21 @@ pub struct Macaroon {
pub idp_id: String,
pub nonce: Nonce,
pub csrf: CsrfToken,
pub redirect_url: Option<RedirectUrl>,
pub pkce_verifier: Option<PkceCodeVerifier>,
pub redirect_url: RedirectUrl,
pub time: i64,
}
impl Macaroon {
pub fn new(idp_id: String, nonce: Nonce, csrf: CsrfToken, redirect_url: RedirectUrl) -> Self {
Self {
idp_id,
nonce,
csrf,
redirect_url,
time: OffsetDateTime::now_utc().unix_timestamp(),
}
}
pub fn encode(&self, macaroon: &str) -> Result<String, jsonwebtoken::errors::Error> {
jsonwebtoken::encode(
&Header::default(),
@ -38,7 +48,7 @@ impl Macaroon {
Err(Error::BadRequest(
ruma::api::client::error::ErrorKind::Unauthorized,
"macaroon invalid",
"macaroon encoding",
))
}
}

View file

@ -1,21 +1,15 @@
use std::sync::Arc;
mod session;
use std::{collections::HashMap, sync::Arc};
use axum_extra::extract::cookie::{Cookie, SameSite};
use futures_util::future::{self};
use openidconnect::{
core::{
CoreAuthenticationFlow, CoreClient, CoreGenderClaim, CoreIdTokenClaims,
CoreProviderMetadata, CoreUserInfoClaims,
},
reqwest::async_http_client,
AccessTokenHash, AdditionalClaims, AuthUrl, AuthorizationCode, ClientId, ClientSecret,
CsrfToken, IssuerUrl, Nonce, NonceVerifier, OAuth2TokenResponse, PkceCodeChallenge,
PkceCodeVerifier, RedirectUrl, Scope, SubjectIdentifier, TokenResponse, TokenUrl,
UserInfoClaims, UserInfoUrl,
core::{CoreAuthenticationFlow, CoreClient, CoreIdTokenClaims, CoreProviderMetadata},
reqwest::{async_http_client, http_client},
AdditionalClaims, AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, EndUserName,
IdTokenClaims, IssuerUrl, Nonce, RedirectUrl, Scope, StandardClaims, TokenResponse, TokenUrl,
UserInfoUrl,
};
use ruma::api::client::{error::ErrorKind, session::get_login_types::v3::IdentityProvider};
use time::{macros::format_description, OffsetDateTime};
use crate::{
config::{ClientConfig, DiscoveryConfig as Discovery, ProviderConfig},
@ -35,9 +29,10 @@ pub struct Service {
impl Service {
pub async fn build(config: &Config) -> Arc<Self> {
Arc::new(Self {
inner: future::join_all(config.sso.clone().into_iter().map(Provider::new)).await,
})
// let inner = future::join_all(config.sso.clone().into_iter().map(Provider::new)).await;
let inner = config.sso.clone().into_iter().map(Provider::new).collect();
Arc::new(Self { inner })
}
pub fn find_one(&self, idp_id: impl AsRef<str>) -> Result<Provider, Error> {
@ -60,60 +55,47 @@ pub struct Provider {
pub inner: IdentityProvider,
pub client: Arc<CoreClient>,
pub scopes: Vec<String>,
pub pkce: Option<bool>,
pub subject_claim: Option<String>,
}
impl Provider {
pub async fn new(config: ProviderConfig) -> Self {
let inner = IdentityProvider {
pub fn new(config: ProviderConfig) -> Self {
Self {
client: Provider::create_client(config.discovery, config.issuer, config.client)
.unwrap(),
inner: IdentityProvider {
id: config.id.clone(),
name: config.name.unwrap_or(config.id),
icon: config.icon,
brand: None,
};
Self {
inner,
client: Provider::create_client(config.discovery, config.issuer, config.client)
.await
.unwrap(),
},
scopes: config.scopes,
pkce: config.pkce,
subject_claim: config.subject_claim,
}
}
async fn create_client(
fn create_client(
discovery: Discovery,
issuer: url::Url,
config: ClientConfig,
) -> Result<Arc<CoreClient>, Error> {
let mut base_url = url::Url::try_from(
services()
let base_url = services()
.globals
.well_known_client()
.as_deref()
.unwrap_or(services().globals.server_name().as_str()),
)
.expect("server_name should be a valid URL");
.unwrap_or(services().globals.server_name().as_str());
base_url.set_path("_conduit/config/sso/callback");
let redirect_url = RedirectUrl::from_url(base_url);
let redirect_url =
RedirectUrl::new(format!("https://{base_url}/_conduit/config/sso/callback"))
.expect("server_name should be a valid URL");
let config = match discovery {
Discovery::Automatic => {
let url = issuer.to_string();
let url = url.strip_suffix("/").unwrap();
let discovery = CoreProviderMetadata::discover_async(
// https://github.com/ramosbugs/openidconnect-rs/issues/77
IssuerUrl::new(url.to_owned()).unwrap(),
async_http_client,
let discovery = CoreProviderMetadata::discover(
&IssuerUrl::from_url(issuer),
http_client,
)
.await
.unwrap();
// .map_err(|e| Error::BadConfig(&e.to_string()))?;
CoreClient::from_provider_metadata(
discovery,
@ -136,50 +118,41 @@ impl Provider {
Ok(Arc::new(config))
}
pub async fn handle_redirect(&self, redirect_url: &RedirectUrl) -> (url::Url, String, String) {
let client = self.client.clone();
let scopes = self.scopes.iter().map(ToOwned::to_owned).map(Scope::new);
let mut req = client
pub async fn handle_redirect(&self, redirect_url: RedirectUrl) -> (url::Url, Nonce, Cookie) {
let req = self
.client
.authorize_url(
CoreAuthenticationFlow::Implicit(true),
CoreAuthenticationFlow::AuthorizationCode,
|| CsrfToken::new_random_len(48),
|| Nonce::new_random_len(48),
)
.add_scopes(scopes);
let pkce_verifier = match self.pkce {
Some(true) => {
let (challenge, verifier) = PkceCodeChallenge::new_random_sha256();
req = req.set_pkce_challenge(challenge);
Some(verifier)
}
_ => None,
};
.add_scopes(self.scopes.iter().map(ToOwned::to_owned).map(Scope::new));
let (url, csrf, nonce) = req.url();
let key = services()
.globals
.macaroon_key
.as_deref()
.expect("macaroon key")
.to_owned();
let cookie = Macaroon {
idp_id: self.inner.id.clone(),
let mac = Macaroon::new(
self.inner.id.clone(),
nonce.clone(),
csrf,
nonce: nonce.clone(),
time: OffsetDateTime::now_utc().unix_timestamp(),
redirect_url: Some(redirect_url.clone()),
pkce_verifier,
};
let cookie = cookie.encode(&key).expect("bad key");
redirect_url.clone(),
);
(url, nonce.secret().to_owned(), cookie)
let cookie = Cookie::build(
"sso-state",
mac.encode(&services().globals.macaroon_key.as_deref().unwrap())
.expect("bad key"),
)
.path("/_conduit/client/sso")
.secure(true)
.http_only(true)
.same_site(SameSite::None)
.max_age(time::Duration::seconds(COOKIE_STATE_EXPIRATION_SECS))
.finish();
(url, nonce, cookie)
}
pub async fn handle_callback<Claims: AdditionalClaims>(
pub async fn handle_callback(
&self,
code: AuthorizationCode,
nonce: Nonce,
@ -196,15 +169,22 @@ impl Provider {
.claims(&self.client.id_token_verifier(), &nonce)
.unwrap();
if let Some(expected) = claims.access_token_hash() {
let found =
AccessTokenHash::from_token(resp.access_token(), &id_token.signing_alg().unwrap())
let claims_map: HashMap<String, String> = serde_json::to_string(claims)
.map(|s| serde_json::from_str(&s).unwrap())
.unwrap();
tracing::info!(?claims_map);
if &found != expected {
panic!()
}
}
Ok(())
// if let Some(expected) = claims.access_token_hash() {
// let found =
// AccessTokenHash::from_token(resp.access_token(), &id_token.signing_alg().unwrap())
// .unwrap();
// if &found != expected {
// panic!()
// }
// }
// match self.client.user_info(
// resp.access_token().to_owned(),

View file

@ -1,6 +1,6 @@
use askama::Template;
use ruma::{
api::client::search::search_events::v3::UserProfile, OwnedMxcUri, OwnedServerName, OwnedUserId,
api::client::{search::search_events::v3::UserProfile, session::get_login_types::v3::IdentityProvider}, OwnedMxcUri, OwnedServerName, OwnedUserId,
};
use super::Provider;
@ -13,22 +13,6 @@ pub struct AuthConfirmation {
idp_name: String,
}
pub struct Metadata {
id: String,
name: Option<String>,
icon: Option<OwnedMxcUri>,
}
impl From<&Provider> for Metadata {
fn from(value: &Provider) -> Self {
Self {
id: value.config.id.clone(),
name: value.config.name.clone(),
icon: value.config.icon.clone(),
}
}
}
#[derive(Template)]
#[template(path = "auth_failure.html", escape = "none")]
pub struct AuthFailure {
@ -46,7 +30,7 @@ pub struct Deactivated {}
#[template(path = "idp_picker.html", escape = "none")]
pub struct IdpPicker {
pub server_name: String,
pub metadata: Vec<Metadata>,
pub metadata: Vec<IdentityProvider>,
pub redirect_url: String,
}
@ -54,7 +38,7 @@ pub struct IdpPicker {
#[template(path = "registration.html", escape = "none")]
pub struct Registration {
pub server_name: OwnedServerName,
pub idp: Metadata,
pub idp: IdentityProvider,
pub user: Attributes,
}

View file

@ -5,7 +5,7 @@ use cmp::Ordering;
use rand::prelude::*;
use ring::digest;
use ruma::{
canonical_json::try_from_json_map, CanonicalJsonError, CanonicalJsonObject, MxcUri, MxcUriError,
canonical_json::try_from_json_map, CanonicalJsonError, CanonicalJsonObject, MxcUri, MxcUriError, OwnedMxcUri,
};
use std::{
cmp, fmt,