Draft: SSO login (OAuth 2.0 + OpenID Connect) #1012
6 changed files with 137 additions and 227 deletions
89
Cargo.lock
generated
89
Cargo.lock
generated
|
@ -540,7 +540,6 @@ dependencies = [
|
||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"lru-cache",
|
"lru-cache",
|
||||||
"macaroon",
|
|
||||||
"nix",
|
"nix",
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
"openidconnect",
|
"openidconnect",
|
||||||
|
@ -845,19 +844,10 @@ dependencies = [
|
||||||
"digest",
|
"digest",
|
||||||
"elliptic-curve",
|
"elliptic-curve",
|
||||||
"rfc6979",
|
"rfc6979",
|
||||||
"signature 2.2.0",
|
"signature",
|
||||||
"spki",
|
"spki",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ed25519"
|
|
||||||
version = "1.5.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7"
|
|
||||||
dependencies = [
|
|
||||||
"signature 1.6.4",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ed25519"
|
name = "ed25519"
|
||||||
version = "2.2.3"
|
version = "2.2.3"
|
||||||
|
@ -865,7 +855,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
|
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pkcs8",
|
"pkcs8",
|
||||||
"signature 2.2.0",
|
"signature",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -875,7 +865,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871"
|
checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"curve25519-dalek",
|
"curve25519-dalek",
|
||||||
"ed25519 2.2.3",
|
"ed25519",
|
||||||
"rand_core",
|
"rand_core",
|
||||||
"serde",
|
"serde",
|
||||||
"sha2",
|
"sha2",
|
||||||
|
@ -1678,18 +1668,6 @@ dependencies = [
|
||||||
"zstd-sys",
|
"zstd-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libsodium-sys"
|
|
||||||
version = "0.2.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6b779387cd56adfbc02ea4a668e704f729be8d6a6abd2c27ca5ee537849a92fd"
|
|
||||||
dependencies = [
|
|
||||||
"cc",
|
|
||||||
"libc",
|
|
||||||
"pkg-config",
|
|
||||||
"walkdir",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libsqlite3-sys"
|
name = "libsqlite3-sys"
|
||||||
version = "0.26.0"
|
version = "0.26.0"
|
||||||
|
@ -1753,19 +1731,6 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "macaroon"
|
|
||||||
version = "0.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3b15778101dd1d3a58a95fd2af33821b38bc44a408bcf03e0e5e194f42c08050"
|
|
||||||
dependencies = [
|
|
||||||
"base64 0.13.1",
|
|
||||||
"log",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"sodiumoxide",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "maplit"
|
name = "maplit"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
|
@ -2610,7 +2575,7 @@ dependencies = [
|
||||||
"pkcs1",
|
"pkcs1",
|
||||||
"pkcs8",
|
"pkcs8",
|
||||||
"rand_core",
|
"rand_core",
|
||||||
"signature 2.2.0",
|
"signature",
|
||||||
"spki",
|
"spki",
|
||||||
"subtle",
|
"subtle",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
|
@ -2907,15 +2872,6 @@ version = "1.0.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
|
checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "same-file"
|
|
||||||
version = "1.0.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
|
||||||
dependencies = [
|
|
||||||
"winapi-util",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "schannel"
|
name = "schannel"
|
||||||
version = "0.1.23"
|
version = "0.1.23"
|
||||||
|
@ -3184,12 +3140,6 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "signature"
|
|
||||||
version = "1.6.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signature"
|
name = "signature"
|
||||||
version = "2.2.0"
|
version = "2.2.0"
|
||||||
|
@ -3243,18 +3193,6 @@ dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sodiumoxide"
|
|
||||||
version = "0.2.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e26be3acb6c2d9a7aac28482586a7856436af4cfe7100031d219de2d2ecb0028"
|
|
||||||
dependencies = [
|
|
||||||
"ed25519 1.5.3",
|
|
||||||
"libc",
|
|
||||||
"libsodium-sys",
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spin"
|
name = "spin"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
|
@ -3899,16 +3837,6 @@ version = "0.9.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "walkdir"
|
|
||||||
version = "2.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
|
|
||||||
dependencies = [
|
|
||||||
"same-file",
|
|
||||||
"winapi-util",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "want"
|
name = "want"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
|
@ -4040,15 +3968,6 @@ version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "winapi-util"
|
|
||||||
version = "0.1.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
|
|
||||||
dependencies = [
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi-x86_64-pc-windows-gnu"
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
|
|
@ -55,8 +55,6 @@ bytes = "1.4.0"
|
||||||
http = "0.2.9"
|
http = "0.2.9"
|
||||||
# Used to find data directory for default db path
|
# Used to find data directory for default db path
|
||||||
directories = "4.0.1"
|
directories = "4.0.1"
|
||||||
# Used for SSO authorization
|
|
||||||
macaroon = "0.3.0"
|
|
||||||
# Used for ruma wrapper
|
# Used for ruma wrapper
|
||||||
serde_json = { version = "1.0.96", features = ["raw_value"] }
|
serde_json = { version = "1.0.96", features = ["raw_value"] }
|
||||||
# Used for appservice registration files
|
# Used for appservice registration files
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
service::sso::{templates, Provider, COOKIE_STATE_EXPIRATION_SECS},
|
service::sso::{macaroon::Macaroon, templates, COOKIE_STATE_EXPIRATION_SECS},
|
||||||
services, Error, Ruma, RumaResponse,
|
services, Error, Ruma,
|
||||||
};
|
};
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use axum::{body::Full, response::IntoResponse};
|
use axum::{body::Full, response::IntoResponse};
|
||||||
use axum_extra::extract::cookie::{Cookie, SameSite};
|
use axum_extra::extract::cookie::{Cookie, SameSite};
|
||||||
use bytes::BytesMut;
|
use bytes::BytesMut;
|
||||||
use http::StatusCode;
|
use http::{HeaderValue, StatusCode};
|
||||||
use macaroon::ByteString;
|
use openidconnect::{
|
||||||
use openidconnect::{reqwest::{http_client, async_http_client}, AuthorizationCode, CsrfToken, TokenResponse};
|
AuthorizationCode, CsrfToken,
|
||||||
|
};
|
||||||
use ruma::api::{
|
use ruma::api::{
|
||||||
client::{error::ErrorKind, session},
|
client::{error::ErrorKind, session},
|
||||||
OutgoingResponse,
|
OutgoingResponse,
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use time::macros::format_description;
|
|
||||||
|
|
||||||
/// # `GET /_matrix/client/v3/login/sso/redirect`
|
/// # `GET /_matrix/client/v3/login/sso/redirect`
|
||||||
///
|
///
|
||||||
|
@ -50,11 +50,13 @@ pub async fn get_sso_redirect_with_provider(
|
||||||
|
|
||||||
let location = Some(body.idp_id.clone());
|
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.as_deref().unwrap_or_default())) {
|
let (url, nonce, cookie) =
|
||||||
Ok(fut)=> fut.await,
|
match services().sso.find_one(&body.idp_id).map(|provider| {
|
||||||
Err(e)=> return e.into_response(),
|
provider.handle_redirect(body.redirect_url.unwrap())
|
||||||
};
|
}) {
|
||||||
|
Ok(fut) => fut.await,
|
||||||
|
Err(e) => return e.into_response(),
|
||||||
|
};
|
||||||
|
|
||||||
let cookie = Cookie::build("openid-state", cookie)
|
let cookie = Cookie::build("openid-state", cookie)
|
||||||
.path("/_conduit/client/sso")
|
.path("/_conduit/client/sso")
|
||||||
|
@ -107,8 +109,11 @@ fn get_sso_fallback_template(redirect_url: &str) -> axum::response::Response {
|
||||||
pub struct Callback {
|
pub struct Callback {
|
||||||
pub code: AuthorizationCode,
|
pub code: AuthorizationCode,
|
||||||
pub state: CsrfToken,
|
pub state: CsrfToken,
|
||||||
|
pub verifier: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct Session {}
|
||||||
|
|
||||||
/// # `GET /_conduit/client/oidc/callback`
|
/// # `GET /_conduit/client/oidc/callback`
|
||||||
///
|
///
|
||||||
/// Verify the response received from the identity provider.
|
/// Verify the response received from the identity provider.
|
||||||
|
@ -117,9 +122,16 @@ pub async fn get_sso_callback(
|
||||||
cookie: axum::extract::TypedHeader<axum::headers::Cookie>,
|
cookie: axum::extract::TypedHeader<axum::headers::Cookie>,
|
||||||
axum::extract::Query(callback): axum::extract::Query<Callback>,
|
axum::extract::Query(callback): axum::extract::Query<Callback>,
|
||||||
) -> axum::response::Response {
|
) -> axum::response::Response {
|
||||||
// TODO
|
let clear_cookie = Cookie::build("openid-state", "")
|
||||||
|
.path("/_conduit/client/sso")
|
||||||
|
.finish()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
let Callback { code, state } = callback;
|
let Callback {
|
||||||
|
code,
|
||||||
|
state,
|
||||||
|
verifier,
|
||||||
|
} = callback;
|
||||||
|
|
||||||
let Some(cookie) = cookie.get("openid-state") else {
|
let Some(cookie) = cookie.get("openid-state") else {
|
||||||
return Error::BadRequest(
|
return Error::BadRequest(
|
||||||
|
@ -129,26 +141,24 @@ pub async fn get_sso_callback(
|
||||||
.into_response();
|
.into_response();
|
||||||
};
|
};
|
||||||
|
|
||||||
let provider = match Provider::verify_macaroon(cookie.as_bytes(), state)
|
let macaroon = match Macaroon::verify(cookie, state.secret()) {
|
||||||
.and_then(|macaroon| services().sso.find_one(macaroon.identifier().into()))
|
Ok(macaroon) => macaroon,
|
||||||
{
|
|
||||||
Ok(provider) => provider,
|
|
||||||
Err(error) => return error.into_response(),
|
Err(error) => return error.into_response(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let provider = match services().sso.find_one(macaroon.idp_id.as_ref()) {
|
||||||
|
Ok(provider) => provider,
|
||||||
|
Err(error) => return error.into_response(),
|
||||||
|
};
|
||||||
|
let session = serde_json::to_string(cookie).unwrap();
|
||||||
|
|
||||||
|
let user_info = provider.handle_callback(code, macaroon.nonce).await;
|
||||||
|
|
||||||
|
(
|
||||||
let cookie = Cookie::build("openid-state", "")
|
axum::TypedHeader(axum::headers::Location(
|
||||||
.path("/_conduit/client/sso")
|
HeaderValue::from_str(clear_cookie.as_str()).unwrap(),
|
||||||
.finish()
|
)),
|
||||||
.to_string();
|
"Hello, World!",
|
||||||
|
)
|
||||||
let user_info = provider.handle_callback(code, nonce);
|
.into_response()
|
||||||
|
|
||||||
// if let Some(verifier) = pkce {
|
|
||||||
// macaroon.add_first_party_caveat(format!("verifier = {}", verifier).into());
|
|
||||||
// }
|
|
||||||
|
|
||||||
(TypedHeader(ContentType::text_utf8()), "Hello, World!").into_response()
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,7 +75,7 @@ pub struct Service {
|
||||||
pub rotate: RotationHandler,
|
pub rotate: RotationHandler,
|
||||||
|
|
||||||
pub shutdown: AtomicBool,
|
pub shutdown: AtomicBool,
|
||||||
pub macaroon: Option<macaroon::MacaroonKey>,
|
pub macaroon_key: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles "rotation" of long-polling requests. "Rotation" in this context is similar to "rotation" of log files and the like.
|
/// Handles "rotation" of long-polling requests. "Rotation" in this context is similar to "rotation" of log files and the like.
|
||||||
|
@ -183,11 +183,6 @@ impl Service {
|
||||||
// Experimental, partially supported room versions
|
// Experimental, partially supported room versions
|
||||||
let unstable_room_versions = vec![RoomVersionId::V3, RoomVersionId::V4, RoomVersionId::V5];
|
let unstable_room_versions = vec![RoomVersionId::V3, RoomVersionId::V4, RoomVersionId::V5];
|
||||||
|
|
||||||
let macaroon = config
|
|
||||||
.macaroon_key
|
|
||||||
.as_ref()
|
|
||||||
.map(|s| macaroon::MacaroonKey::generate(s.as_bytes()));
|
|
||||||
|
|
||||||
let mut s = Self {
|
let mut s = Self {
|
||||||
db,
|
db,
|
||||||
config,
|
config,
|
||||||
|
@ -218,7 +213,7 @@ impl Service {
|
||||||
sync_receivers: RwLock::new(HashMap::new()),
|
sync_receivers: RwLock::new(HashMap::new()),
|
||||||
rotate: RotationHandler::new(),
|
rotate: RotationHandler::new(),
|
||||||
shutdown: AtomicBool::new(false),
|
shutdown: AtomicBool::new(false),
|
||||||
macaroon,
|
macaroon_key: config.macaroon_key.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
fs::create_dir_all(s.get_media_folder())?;
|
fs::create_dir_all(s.get_media_folder())?;
|
||||||
|
|
44
src/service/sso/macaroon.rs
Normal file
44
src/service/sso/macaroon.rs
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation};
|
||||||
|
use openidconnect::{Nonce, PkceCodeVerifier, RedirectUrl, CsrfToken};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct Macaroon {
|
||||||
|
pub idp_id: String,
|
||||||
|
pub nonce: Nonce,
|
||||||
|
pub csrf: CsrfToken,
|
||||||
|
pub redirect_url: Option<RedirectUrl>,
|
||||||
|
pub pkce_verifier: Option<PkceCodeVerifier>,
|
||||||
|
pub time: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Macaroon {
|
||||||
|
pub fn encode(&self, macaroon: &str) -> Result<String, jsonwebtoken::errors::Error> {
|
||||||
|
jsonwebtoken::encode(
|
||||||
|
&Header::default(),
|
||||||
|
self,
|
||||||
|
&EncodingKey::from_secret(macaroon.as_bytes()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify(token: &str, macaroon: &str) -> Result<Self, Error> {
|
||||||
|
let decoded = jsonwebtoken::decode::<Self>(
|
||||||
|
token,
|
||||||
|
&DecodingKey::from_secret(macaroon.as_bytes()),
|
||||||
|
&Validation::new(Algorithm::HS256),
|
||||||
|
)
|
||||||
|
.map_err(|_| {
|
||||||
|
Error::BadRequest(
|
||||||
|
ruma::api::client::error::ErrorKind::Unauthorized,
|
||||||
|
"macaroon decoding",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Err(Error::BadRequest(
|
||||||
|
ruma::api::client::error::ErrorKind::Unauthorized,
|
||||||
|
"macaroon invalid",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,24 +1,32 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
mod session;
|
||||||
|
|
||||||
use futures_util::future::{self};
|
use futures_util::future::{self};
|
||||||
use macaroon::{Macaroon, Verifier};
|
|
||||||
use openidconnect::{
|
use openidconnect::{
|
||||||
core::{CoreAuthenticationFlow, CoreClient, CoreGenderClaim, CoreProviderMetadata},
|
core::{
|
||||||
|
CoreAuthenticationFlow, CoreClient, CoreGenderClaim, CoreIdTokenClaims,
|
||||||
|
CoreProviderMetadata, CoreUserInfoClaims,
|
||||||
|
},
|
||||||
reqwest::async_http_client,
|
reqwest::async_http_client,
|
||||||
AccessTokenHash, AdditionalClaims, AuthUrl, AuthorizationCode, ClientId, ClientSecret,
|
AccessTokenHash, AdditionalClaims, AuthUrl, AuthorizationCode, ClientId, ClientSecret,
|
||||||
CsrfToken, IssuerUrl, Nonce, NonceVerifier, OAuth2TokenResponse, PkceCodeChallenge,
|
CsrfToken, IssuerUrl, Nonce, NonceVerifier, OAuth2TokenResponse, PkceCodeChallenge,
|
||||||
RedirectUrl, Scope, SubjectIdentifier, TokenResponse, TokenUrl, UserInfoClaims, UserInfoUrl,
|
PkceCodeVerifier, RedirectUrl, Scope, SubjectIdentifier, TokenResponse, TokenUrl,
|
||||||
|
UserInfoClaims, UserInfoUrl,
|
||||||
};
|
};
|
||||||
use ruma::api::client::{error::ErrorKind, session::get_login_types::v3::IdentityProvider};
|
use ruma::api::client::{error::ErrorKind, session::get_login_types::v3::IdentityProvider};
|
||||||
use time::macros::format_description;
|
use time::{macros::format_description, OffsetDateTime};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{ClientConfig, DiscoveryConfig as Discovery, ProviderConfig},
|
config::{ClientConfig, DiscoveryConfig as Discovery, ProviderConfig},
|
||||||
services, Config, Error,
|
services, Config, Error,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use self::macaroon::Macaroon;
|
||||||
|
|
||||||
pub const COOKIE_STATE_EXPIRATION_SECS: i64 = 60 * 60;
|
pub const COOKIE_STATE_EXPIRATION_SECS: i64 = 60 * 60;
|
||||||
|
|
||||||
|
pub mod macaroon;
|
||||||
pub mod templates;
|
pub mod templates;
|
||||||
|
|
||||||
pub struct Service {
|
pub struct Service {
|
||||||
|
@ -128,116 +136,54 @@ impl Provider {
|
||||||
Ok(Arc::new(config))
|
Ok(Arc::new(config))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_redirect(&self, redirect_url: &str) -> (url::Url, String, String) {
|
pub async fn handle_redirect(&self, redirect_url: &RedirectUrl) -> (url::Url, String, String) {
|
||||||
let client = self.client.clone();
|
let client = self.client.clone();
|
||||||
let scopes = self.scopes.iter().map(ToOwned::to_owned).map(Scope::new);
|
let scopes = self.scopes.iter().map(ToOwned::to_owned).map(Scope::new);
|
||||||
|
|
||||||
let mut req = client
|
let mut req = client
|
||||||
.authorize_url(
|
.authorize_url(
|
||||||
CoreAuthenticationFlow::Implicit(true),
|
CoreAuthenticationFlow::Implicit(true),
|
||||||
|| CsrfToken::new_random_len(36),
|
|| CsrfToken::new_random_len(48),
|
||||||
|| Nonce::new_random_len(36),
|
|| Nonce::new_random_len(48),
|
||||||
)
|
)
|
||||||
.add_scopes(scopes);
|
.add_scopes(scopes);
|
||||||
|
|
||||||
let (challenge, verifier) = PkceCodeChallenge::new_random_sha256();
|
let pkce_verifier = match self.pkce {
|
||||||
if let Some(true) = self.pkce {
|
Some(true) => {
|
||||||
req = req.set_pkce_challenge(challenge);
|
let (challenge, verifier) = PkceCodeChallenge::new_random_sha256();
|
||||||
}
|
req = req.set_pkce_challenge(challenge);
|
||||||
|
|
||||||
|
Some(verifier)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
let (url, csrf, nonce) = req.url();
|
let (url, csrf, nonce) = req.url();
|
||||||
|
|
||||||
let cookie = self.generate_macaroon(
|
let key = services()
|
||||||
self.inner.id.as_str(),
|
.globals
|
||||||
csrf.secret(),
|
.macaroon_key
|
||||||
nonce.secret(),
|
.as_deref()
|
||||||
redirect_url,
|
.expect("macaroon key")
|
||||||
self.pkce.map(|_| verifier.secret().as_str()),
|
.to_owned();
|
||||||
);
|
let cookie = Macaroon {
|
||||||
|
idp_id: self.inner.id.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");
|
||||||
|
|
||||||
(url, nonce.secret().to_owned(), cookie)
|
(url, nonce.secret().to_owned(), cookie)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_macaroon(
|
|
||||||
&self,
|
|
||||||
idp_id: &str,
|
|
||||||
state: &str,
|
|
||||||
nonce: &str,
|
|
||||||
redirect_url: &str,
|
|
||||||
pkce: Option<&str>,
|
|
||||||
) -> String {
|
|
||||||
let key = services().globals.macaroon.unwrap();
|
|
||||||
|
|
||||||
let mut macaroon = Macaroon::create(None, &key, idp_id.into()).unwrap();
|
|
||||||
let expires = (time::OffsetDateTime::now_utc()
|
|
||||||
+ time::Duration::seconds(COOKIE_STATE_EXPIRATION_SECS))
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let idp_id = self.inner.id.as_str();
|
|
||||||
|
|
||||||
for caveat in [
|
|
||||||
format!("idp_id = {idp_id}"),
|
|
||||||
format!("state = {state}"),
|
|
||||||
format!("nonce = {nonce}"),
|
|
||||||
format!("redirect_url = {redirect_url}"),
|
|
||||||
format!("time < {expires}"),
|
|
||||||
] {
|
|
||||||
macaroon.add_first_party_caveat(caveat.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(verifier) = pkce {
|
|
||||||
macaroon.add_first_party_caveat(format!("verifier = {}", verifier).into());
|
|
||||||
}
|
|
||||||
|
|
||||||
macaroon.serialize(macaroon::Format::V2).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn verify_macaroon(cookie: &[u8], state: CsrfToken) -> Result<Macaroon, Error> {
|
|
||||||
let mut verifier = Verifier::default();
|
|
||||||
|
|
||||||
let macaroon = Macaroon::deserialize(cookie).map_err(|e| {
|
|
||||||
Error::BadRequest(ErrorKind::BadJson, "Could not deserialize SSO macaroon")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
verifier.satisfy_exact(format!("state = {}", state.secret()).into());
|
|
||||||
|
|
||||||
// let verification = |s: &ByteString, id: &str| {
|
|
||||||
// s.0.starts_with(format!("{id} =").as_bytes()); // TODO
|
|
||||||
// };
|
|
||||||
|
|
||||||
verifier.satisfy_general(|s| s.0.starts_with(b"idp_id ="));
|
|
||||||
verifier.satisfy_general(|s| s.0.starts_with(b"nonce ="));
|
|
||||||
verifier.satisfy_general(|s| s.0.starts_with(b"redirect_url ="));
|
|
||||||
|
|
||||||
verifier.satisfy_general(|s| {
|
|
||||||
let format_desc = format_description!(
|
|
||||||
"[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \
|
|
||||||
sign:mandatory]:[offset_minute]:[offset_second]"
|
|
||||||
);
|
|
||||||
|
|
||||||
let now = time::OffsetDateTime::now_utc();
|
|
||||||
|
|
||||||
time::OffsetDateTime::parse(std::str::from_utf8(&s.0).unwrap(), format_desc)
|
|
||||||
.map(|expires| now < expires)
|
|
||||||
.unwrap_or(false)
|
|
||||||
});
|
|
||||||
|
|
||||||
let key = services().globals.macaroon.unwrap();
|
|
||||||
|
|
||||||
verifier
|
|
||||||
.verify(&macaroon, &key, Default::default())
|
|
||||||
.map_err(|e| {
|
|
||||||
Error::BadRequest(ErrorKind::Unauthorized, "Macaroon verification failed")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(macaroon)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn handle_callback<Claims: AdditionalClaims>(
|
pub async fn handle_callback<Claims: AdditionalClaims>(
|
||||||
&self,
|
&self,
|
||||||
code: AuthorizationCode,
|
code: AuthorizationCode,
|
||||||
nonce: Nonce,
|
nonce: Nonce,
|
||||||
) -> Result<UserInfoClaims<Claims, CoreGenderClaim>, Error> {
|
) -> Result<(), Error> {
|
||||||
let resp = self
|
let resp = self
|
||||||
.client
|
.client
|
||||||
.exchange_code(code)
|
.exchange_code(code)
|
||||||
|
@ -260,14 +206,12 @@ impl Provider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let Ok(req) = self.client.user_info(
|
// match self.client.user_info(
|
||||||
resp.access_token().to_owned(),
|
// resp.access_token().to_owned(),
|
||||||
self.subject_claim.clone().map(SubjectIdentifier::new),
|
// self.subject_claim.clone().map(SubjectIdentifier::new),
|
||||||
) else {
|
// ).map(|req| req.request_async(async_http_client)) {
|
||||||
resp.extra_fields();
|
// Err(e) => Ok(claims),
|
||||||
panic!()
|
// Ok(req) => req.await,
|
||||||
};
|
// }
|
||||||
|
|
||||||
Ok(req.request_async(async_http_client).await.unwrap())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue