You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-07-29 22:01:14 +03:00
Allow running the authentication service on a different base path
This commit is contained in:
@ -16,6 +16,7 @@ use serde::{Deserialize, Serialize};
|
||||
use ulid::Ulid;
|
||||
|
||||
pub use crate::traits::*;
|
||||
use crate::UrlBuilder;
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
#[serde(rename_all = "snake_case", tag = "kind")]
|
||||
@ -57,16 +58,19 @@ impl PostAuthAction {
|
||||
PostAuthAction::ManageAccount { action }
|
||||
}
|
||||
|
||||
pub fn go_next(&self) -> axum::response::Redirect {
|
||||
pub fn go_next(&self, url_builder: &UrlBuilder) -> axum::response::Redirect {
|
||||
match self {
|
||||
Self::ContinueAuthorizationGrant { id } => ContinueAuthorizationGrant(*id).go(),
|
||||
Self::ContinueCompatSsoLogin { id } => CompatLoginSsoComplete::new(*id, None).go(),
|
||||
Self::ChangePassword => AccountPassword.go(),
|
||||
Self::LinkUpstream { id } => UpstreamOAuth2Link::new(*id).go(),
|
||||
Self::ManageAccount { action } => Account {
|
||||
action: action.clone(),
|
||||
Self::ContinueAuthorizationGrant { id } => {
|
||||
url_builder.redirect(&ContinueAuthorizationGrant(*id))
|
||||
}
|
||||
.go(),
|
||||
Self::ContinueCompatSsoLogin { id } => {
|
||||
url_builder.redirect(&CompatLoginSsoComplete::new(*id, None))
|
||||
}
|
||||
Self::ChangePassword => url_builder.redirect(&AccountPassword),
|
||||
Self::LinkUpstream { id } => url_builder.redirect(&UpstreamOAuth2Link::new(*id)),
|
||||
Self::ManageAccount { action } => url_builder.redirect(&Account {
|
||||
action: action.clone(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -219,10 +223,10 @@ impl Login {
|
||||
self.post_auth_action.as_ref()
|
||||
}
|
||||
|
||||
pub fn go_next(&self) -> axum::response::Redirect {
|
||||
pub fn go_next(&self, url_builder: &UrlBuilder) -> axum::response::Redirect {
|
||||
match &self.post_auth_action {
|
||||
Some(action) => action.go_next(),
|
||||
None => Index.go(),
|
||||
Some(action) => action.go_next(url_builder),
|
||||
None => url_builder.redirect(&Index),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -268,10 +272,10 @@ impl Reauth {
|
||||
self.post_auth_action.as_ref()
|
||||
}
|
||||
|
||||
pub fn go_next(&self) -> axum::response::Redirect {
|
||||
pub fn go_next(&self, url_builder: &UrlBuilder) -> axum::response::Redirect {
|
||||
match &self.post_auth_action {
|
||||
Some(action) => action.go_next(),
|
||||
None => Index.go(),
|
||||
Some(action) => action.go_next(url_builder),
|
||||
None => url_builder.redirect(&Index),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -328,10 +332,10 @@ impl Register {
|
||||
self.post_auth_action.as_ref()
|
||||
}
|
||||
|
||||
pub fn go_next(&self) -> axum::response::Redirect {
|
||||
pub fn go_next(&self, url_builder: &UrlBuilder) -> axum::response::Redirect {
|
||||
match &self.post_auth_action {
|
||||
Some(action) => action.go_next(),
|
||||
None => Index.go(),
|
||||
Some(action) => action.go_next(url_builder),
|
||||
None => url_builder.redirect(&Index),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -38,12 +38,12 @@ mod tests {
|
||||
#[test]
|
||||
fn test_relative_urls() {
|
||||
assert_eq!(
|
||||
OidcConfiguration.relative_url(),
|
||||
OidcConfiguration.path_and_query(),
|
||||
Cow::Borrowed("/.well-known/openid-configuration")
|
||||
);
|
||||
assert_eq!(Index.relative_url(), Cow::Borrowed("/"));
|
||||
assert_eq!(Index.path_and_query(), Cow::Borrowed("/"));
|
||||
assert_eq!(
|
||||
Login::and_continue_grant(Ulid::nil()).relative_url(),
|
||||
Login::and_continue_grant(Ulid::nil()).path_and_query(),
|
||||
Cow::Borrowed("/login?kind=continue_authorization_grant&id=00000000000000000000000000")
|
||||
);
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ pub trait Route {
|
||||
Cow::Borrowed(Self::route())
|
||||
}
|
||||
|
||||
fn relative_url(&self) -> Cow<'static, str> {
|
||||
fn path_and_query(&self) -> Cow<'static, str> {
|
||||
let path = self.path();
|
||||
if let Some(query) = self.query() {
|
||||
let query = serde_urlencoded::to_string(query).unwrap();
|
||||
@ -39,17 +39,10 @@ pub trait Route {
|
||||
}
|
||||
|
||||
fn absolute_url(&self, base: &Url) -> Url {
|
||||
let relative = self.relative_url();
|
||||
let relative = self.path_and_query();
|
||||
let relative = relative.trim_start_matches('/');
|
||||
base.join(relative.borrow()).unwrap()
|
||||
}
|
||||
|
||||
fn go(&self) -> axum::response::Redirect {
|
||||
axum::response::Redirect::to(&self.relative_url())
|
||||
}
|
||||
|
||||
fn go_absolute(&self, base: &Url) -> axum::response::Redirect {
|
||||
axum::response::Redirect::to(self.absolute_url(base).as_str())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait SimpleRoute {
|
||||
|
@ -14,8 +14,6 @@
|
||||
|
||||
//! Utility to build URLs
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use ulid::Ulid;
|
||||
use url::Url;
|
||||
|
||||
@ -24,32 +22,91 @@ use crate::traits::Route;
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct UrlBuilder {
|
||||
http_base: Url,
|
||||
assets_base: Cow<'static, str>,
|
||||
prefix: String,
|
||||
assets_base: String,
|
||||
issuer: Url,
|
||||
}
|
||||
|
||||
impl UrlBuilder {
|
||||
fn url_for<U>(&self, destination: &U) -> Url
|
||||
fn absolute_url_for<U>(&self, destination: &U) -> Url
|
||||
where
|
||||
U: Route,
|
||||
{
|
||||
destination.absolute_url(&self.http_base)
|
||||
}
|
||||
|
||||
/// Create a relative URL for a route, prefixed with the base URL
|
||||
#[must_use]
|
||||
pub fn relative_url_for<U>(&self, destination: &U) -> String
|
||||
where
|
||||
U: Route,
|
||||
{
|
||||
format!(
|
||||
"{prefix}{destination}",
|
||||
prefix = self.prefix,
|
||||
destination = destination.path_and_query()
|
||||
)
|
||||
}
|
||||
|
||||
/// The prefix added to all relative URLs
|
||||
#[must_use]
|
||||
pub fn prefix(&self) -> Option<&str> {
|
||||
if self.prefix.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(&self.prefix)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a (relative) redirect response to a route
|
||||
pub fn redirect<U>(&self, destination: &U) -> axum::response::Redirect
|
||||
where
|
||||
U: Route,
|
||||
{
|
||||
let uri = self.relative_url_for(destination);
|
||||
axum::response::Redirect::to(&uri)
|
||||
}
|
||||
|
||||
/// Create an absolute redirect response to a route
|
||||
pub fn absolute_redirect<U>(&self, destination: &U) -> axum::response::Redirect
|
||||
where
|
||||
U: Route,
|
||||
{
|
||||
destination.go_absolute(&self.http_base)
|
||||
let uri = self.absolute_url_for(destination);
|
||||
axum::response::Redirect::to(uri.as_str())
|
||||
}
|
||||
|
||||
/// Create a new [`UrlBuilder`] from a base URL
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the base URL contains a fragment, a query, credentials or
|
||||
/// isn't HTTP/HTTPS;
|
||||
#[must_use]
|
||||
pub fn new(base: Url, issuer: Option<Url>, assets_base: Option<String>) -> Self {
|
||||
assert!(
|
||||
base.scheme() == "http" || base.scheme() == "https",
|
||||
"base URL must be HTTP/HTTPS"
|
||||
);
|
||||
assert_eq!(base.query(), None, "base URL must not contain a query");
|
||||
assert_eq!(
|
||||
base.fragment(),
|
||||
None,
|
||||
"base URL must not contain a fragment"
|
||||
);
|
||||
assert_eq!(base.username(), "", "base URL must not contain credentials");
|
||||
assert_eq!(
|
||||
base.password(),
|
||||
None,
|
||||
"base URL must not contain credentials"
|
||||
);
|
||||
|
||||
let issuer = issuer.unwrap_or_else(|| base.clone());
|
||||
let assets_base = assets_base.map_or(Cow::Borrowed("/assets/"), Cow::Owned);
|
||||
let prefix = base.path().trim_end_matches('/').to_owned();
|
||||
let assets_base = assets_base.unwrap_or_else(|| format!("{prefix}/assets/"));
|
||||
Self {
|
||||
http_base: base,
|
||||
prefix,
|
||||
assets_base,
|
||||
issuer,
|
||||
}
|
||||
@ -70,49 +127,49 @@ impl UrlBuilder {
|
||||
/// OAuth 2.0 authorization endpoint
|
||||
#[must_use]
|
||||
pub fn oauth_authorization_endpoint(&self) -> Url {
|
||||
self.url_for(&crate::endpoints::OAuth2AuthorizationEndpoint)
|
||||
self.absolute_url_for(&crate::endpoints::OAuth2AuthorizationEndpoint)
|
||||
}
|
||||
|
||||
/// OAuth 2.0 token endpoint
|
||||
#[must_use]
|
||||
pub fn oauth_token_endpoint(&self) -> Url {
|
||||
self.url_for(&crate::endpoints::OAuth2TokenEndpoint)
|
||||
self.absolute_url_for(&crate::endpoints::OAuth2TokenEndpoint)
|
||||
}
|
||||
|
||||
/// OAuth 2.0 introspection endpoint
|
||||
#[must_use]
|
||||
pub fn oauth_introspection_endpoint(&self) -> Url {
|
||||
self.url_for(&crate::endpoints::OAuth2Introspection)
|
||||
self.absolute_url_for(&crate::endpoints::OAuth2Introspection)
|
||||
}
|
||||
|
||||
/// OAuth 2.0 revocation endpoint
|
||||
#[must_use]
|
||||
pub fn oauth_revocation_endpoint(&self) -> Url {
|
||||
self.url_for(&crate::endpoints::OAuth2Revocation)
|
||||
self.absolute_url_for(&crate::endpoints::OAuth2Revocation)
|
||||
}
|
||||
|
||||
/// OAuth 2.0 client registration endpoint
|
||||
#[must_use]
|
||||
pub fn oauth_registration_endpoint(&self) -> Url {
|
||||
self.url_for(&crate::endpoints::OAuth2RegistrationEndpoint)
|
||||
self.absolute_url_for(&crate::endpoints::OAuth2RegistrationEndpoint)
|
||||
}
|
||||
|
||||
// OIDC userinfo endpoint
|
||||
#[must_use]
|
||||
pub fn oidc_userinfo_endpoint(&self) -> Url {
|
||||
self.url_for(&crate::endpoints::OidcUserinfo)
|
||||
self.absolute_url_for(&crate::endpoints::OidcUserinfo)
|
||||
}
|
||||
|
||||
/// JWKS URI
|
||||
#[must_use]
|
||||
pub fn jwks_uri(&self) -> Url {
|
||||
self.url_for(&crate::endpoints::OAuth2Keys)
|
||||
self.absolute_url_for(&crate::endpoints::OAuth2Keys)
|
||||
}
|
||||
|
||||
/// Static asset
|
||||
#[must_use]
|
||||
pub fn static_asset(&self, path: String) -> Url {
|
||||
self.url_for(&crate::endpoints::StaticAsset::new(path))
|
||||
self.absolute_url_for(&crate::endpoints::StaticAsset::new(path))
|
||||
}
|
||||
|
||||
/// Static asset base
|
||||
@ -124,18 +181,83 @@ impl UrlBuilder {
|
||||
/// GraphQL endpoint
|
||||
#[must_use]
|
||||
pub fn graphql_endpoint(&self) -> Url {
|
||||
self.url_for(&crate::endpoints::GraphQL)
|
||||
self.absolute_url_for(&crate::endpoints::GraphQL)
|
||||
}
|
||||
|
||||
/// Upstream redirect URI
|
||||
#[must_use]
|
||||
pub fn upstream_oauth_callback(&self, id: Ulid) -> Url {
|
||||
self.url_for(&crate::endpoints::UpstreamOAuth2Callback::new(id))
|
||||
self.absolute_url_for(&crate::endpoints::UpstreamOAuth2Callback::new(id))
|
||||
}
|
||||
|
||||
/// Upstream authorize URI
|
||||
#[must_use]
|
||||
pub fn upstream_oauth_authorize(&self, id: Ulid) -> Url {
|
||||
self.url_for(&crate::endpoints::UpstreamOAuth2Authorize::new(id))
|
||||
self.absolute_url_for(&crate::endpoints::UpstreamOAuth2Authorize::new(id))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_invalid_base_url_scheme() {
|
||||
let _ = super::UrlBuilder::new(url::Url::parse("file:///tmp/").unwrap(), None, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_invalid_base_url_query() {
|
||||
let _ = super::UrlBuilder::new(
|
||||
url::Url::parse("https://example.com/?foo=bar").unwrap(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_invalid_base_url_fragment() {
|
||||
let _ = super::UrlBuilder::new(
|
||||
url::Url::parse("https://example.com/#foo").unwrap(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_invalid_base_url_credentials() {
|
||||
let _ = super::UrlBuilder::new(
|
||||
url::Url::parse("https://foo@example.com/").unwrap(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_url_prefix() {
|
||||
let builder = super::UrlBuilder::new(
|
||||
url::Url::parse("https://example.com/foo/").unwrap(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert_eq!(builder.prefix, "/foo");
|
||||
|
||||
let builder =
|
||||
super::UrlBuilder::new(url::Url::parse("https://example.com/").unwrap(), None, None);
|
||||
assert_eq!(builder.prefix, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_absolute_uri_prefix() {
|
||||
let builder = super::UrlBuilder::new(
|
||||
url::Url::parse("https://example.com/foo/").unwrap(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let uri = builder.absolute_url_for(&crate::endpoints::OAuth2AuthorizationEndpoint);
|
||||
assert_eq!(uri.as_str(), "https://example.com/foo/authorize");
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user