diff --git a/Cargo.lock b/Cargo.lock index bc1805ac..b2126e3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2132,6 +2132,7 @@ dependencies = [ "thiserror", "tokio", "tracing", + "url", "warp", ] diff --git a/crates/config/src/sections/http.rs b/crates/config/src/sections/http.rs index c1a5d4bb..36f9155f 100644 --- a/crates/config/src/sections/http.rs +++ b/crates/config/src/sections/http.rs @@ -17,6 +17,7 @@ use std::path::PathBuf; use async_trait::async_trait; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use url::Url; use super::ConfigurationSection; @@ -24,6 +25,10 @@ fn default_http_address() -> String { "[::]:8080".into() } +fn default_public_base() -> Url { + "http://[::]:8080".parse().unwrap() +} + fn http_address_example_1() -> &'static str { "[::1]:8080" } @@ -54,6 +59,9 @@ pub struct HttpConfig { /// the static files embedded in the server binary #[serde(default)] pub web_root: Option, + + /// Public URL base from where the authentication service is reachable + pub public_base: Url, } impl Default for HttpConfig { @@ -61,6 +69,7 @@ impl Default for HttpConfig { Self { address: default_http_address(), web_root: None, + public_base: default_public_base(), } } } diff --git a/crates/config/src/sections/oauth2.rs b/crates/config/src/sections/oauth2.rs index 05fcb290..d5fdd45b 100644 --- a/crates/config/src/sections/oauth2.rs +++ b/crates/config/src/sections/oauth2.rs @@ -126,15 +126,8 @@ impl OAuth2ClientConfig { } } -fn default_oauth2_issuer() -> Url { - "http://[::]:8080".parse().unwrap() -} - #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct OAuth2Config { - #[serde(default = "default_oauth2_issuer")] - pub issuer: Url, - #[serde(default)] pub clients: Vec, @@ -143,13 +136,6 @@ pub struct OAuth2Config { } impl OAuth2Config { - #[must_use] - pub fn discovery_url(&self) -> Url { - self.issuer - .join(".well-known/openid-configuration") - .expect("could not build discovery url") - } - pub async fn key_store(&self) -> anyhow::Result { let mut store = StaticKeystore::new(); @@ -251,7 +237,6 @@ impl ConfigurationSection<'_> for OAuth2Config { }; Ok(Self { - issuer: default_oauth2_issuer(), clients: Vec::new(), keys: vec![rsa_key, ecdsa_key], }) @@ -291,7 +276,6 @@ impl ConfigurationSection<'_> for OAuth2Config { }; Self { - issuer: default_oauth2_issuer(), clients: Vec::new(), keys: vec![rsa_key, ecdsa_key], } @@ -331,7 +315,6 @@ mod tests { NaiDiepgUJ2GI5eq2V8D8nahRANCAARMK9aKUd/H28qaU+0qvS6bSJItzAge1VHn OhBAAUVci1RpmUA+KdCL5sw9nadAEiONeiGr+28RYHZmlB9qXnjC -----END PRIVATE KEY----- - issuer: https://example.com clients: - client_id: public client_auth_method: none @@ -372,7 +355,6 @@ mod tests { let config = OAuth2Config::load_from_file("config.yaml")?; - assert_eq!(config.issuer, "https://example.com".parse().unwrap()); assert_eq!(config.clients.len(), 5); assert_eq!(config.clients[0].client_id, "public"); diff --git a/crates/handlers/src/lib.rs b/crates/handlers/src/lib.rs index 2a0474f3..1aac2ae7 100644 --- a/crates/handlers/src/lib.rs +++ b/crates/handlers/src/lib.rs @@ -47,12 +47,19 @@ pub fn root( config: &RootConfig, ) -> BoxedFilter<(impl Reply,)> { let health = health(pool); - let oauth2 = oauth2(pool, templates, key_store, &config.oauth2, &config.cookies); + let oauth2 = oauth2( + pool, + templates, + key_store, + &config.oauth2, + &config.http, + &config.cookies, + ); let views = views( pool, templates, mailer, - &config.oauth2, + &config.http, &config.csrf, &config.cookies, ); diff --git a/crates/handlers/src/oauth2/discovery.rs b/crates/handlers/src/oauth2/discovery.rs index 45e0597b..45ff103b 100644 --- a/crates/handlers/src/oauth2/discovery.rs +++ b/crates/handlers/src/oauth2/discovery.rs @@ -14,7 +14,7 @@ use std::collections::HashSet; -use mas_config::OAuth2Config; +use mas_config::HttpConfig; use mas_iana::{ jose::JsonWebSignatureAlg, oauth::{ @@ -23,6 +23,7 @@ use mas_iana::{ }, }; use mas_jose::SigningKeystore; +use mas_warp_utils::filters::url_builder::UrlBuilder; use oauth2_types::{ oidc::{ClaimType, Metadata, SubjectType}, requests::{Display, GrantType, ResponseMode}, @@ -32,9 +33,9 @@ use warp::{filters::BoxedFilter, Filter, Reply}; #[allow(clippy::too_many_lines)] pub(super) fn filter( key_store: impl SigningKeystore, - config: &OAuth2Config, + http_config: &HttpConfig, ) -> BoxedFilter<(Box,)> { - let base = config.issuer.clone(); + let builder = UrlBuilder::from(http_config); // This is how clients can authenticate let client_auth_methods_supported = Some({ @@ -62,12 +63,12 @@ pub(super) fn filter( let jwt_signing_alg_values_supported = Some(key_store.supported_algorithms()); // Prepare all the endpoints - let issuer = Some(base.clone()); - let authorization_endpoint = base.join("oauth2/authorize").ok(); - let token_endpoint = base.join("oauth2/token").ok(); - let jwks_uri = base.join("oauth2/keys.json").ok(); - let introspection_endpoint = base.join("oauth2/introspect").ok(); - let userinfo_endpoint = base.join("oauth2/userinfo").ok(); + let issuer = Some(builder.oidc_issuer()); + let authorization_endpoint = Some(builder.oauth_authorization_endpoint()); + let token_endpoint = Some(builder.oauth_token_endpoint()); + let jwks_uri = Some(builder.jwks_uri()); + let introspection_endpoint = Some(builder.oauth_introspection_endpoint()); + let userinfo_endpoint = Some(builder.oidc_userinfo_endpoint()); let scopes_supported = Some({ let mut s = HashSet::new(); diff --git a/crates/handlers/src/oauth2/introspection.rs b/crates/handlers/src/oauth2/introspection.rs index 4dda8634..b894c9f3 100644 --- a/crates/handlers/src/oauth2/introspection.rs +++ b/crates/handlers/src/oauth2/introspection.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use mas_config::{OAuth2ClientConfig, OAuth2Config}; +use mas_config::{HttpConfig, OAuth2ClientConfig, OAuth2Config}; use mas_data_model::TokenType; use mas_iana::oauth::{OAuthClientAuthenticationMethod, OAuthTokenTypeHint}; use mas_storage::oauth2::{ @@ -20,18 +20,20 @@ use mas_storage::oauth2::{ }; use mas_warp_utils::{ errors::WrapError, - filters::{client::client_authentication, database::connection}, + filters::{client::client_authentication, database::connection, url_builder::UrlBuilder}, }; use oauth2_types::requests::{IntrospectionRequest, IntrospectionResponse}; use sqlx::{pool::PoolConnection, PgPool, Postgres}; use tracing::{info, warn}; use warp::{filters::BoxedFilter, Filter, Rejection, Reply}; -pub fn filter(pool: &PgPool, oauth2_config: &OAuth2Config) -> BoxedFilter<(Box,)> { - let audience = oauth2_config - .issuer - .join("/oauth2/introspect") - .unwrap() +pub fn filter( + pool: &PgPool, + oauth2_config: &OAuth2Config, + http_config: &HttpConfig, +) -> BoxedFilter<(Box,)> { + let audience = UrlBuilder::from(http_config) + .oauth_introspection_endpoint() .to_string(); warp::path!("oauth2" / "introspect") diff --git a/crates/handlers/src/oauth2/mod.rs b/crates/handlers/src/oauth2/mod.rs index 8cdfcd66..2e6c2a9e 100644 --- a/crates/handlers/src/oauth2/mod.rs +++ b/crates/handlers/src/oauth2/mod.rs @@ -15,7 +15,7 @@ use std::sync::Arc; use hyper::Method; -use mas_config::{CookiesConfig, OAuth2Config}; +use mas_config::{CookiesConfig, HttpConfig, OAuth2Config}; use mas_jose::StaticKeystore; use mas_templates::Templates; use mas_warp_utils::filters::cors::cors; @@ -41,14 +41,15 @@ pub fn filter( templates: &Templates, key_store: &Arc, oauth2_config: &OAuth2Config, + http_config: &HttpConfig, cookies_config: &CookiesConfig, ) -> BoxedFilter<(impl Reply,)> { - let discovery = discovery(key_store.as_ref(), oauth2_config); + let discovery = discovery(key_store.as_ref(), http_config); let keys = keys(key_store); let authorization = authorization(pool, templates, oauth2_config, cookies_config); let userinfo = userinfo(pool, oauth2_config); - let introspection = introspection(pool, oauth2_config); - let token = token(pool, key_store, oauth2_config); + let introspection = introspection(pool, oauth2_config, http_config); + let token = token(pool, key_store, oauth2_config, http_config); let filter = discovery .or(keys) diff --git a/crates/handlers/src/oauth2/token.rs b/crates/handlers/src/oauth2/token.rs index a9abbe5c..55693e91 100644 --- a/crates/handlers/src/oauth2/token.rs +++ b/crates/handlers/src/oauth2/token.rs @@ -19,7 +19,7 @@ use chrono::{DateTime, Duration, Utc}; use data_encoding::BASE64URL_NOPAD; use headers::{CacheControl, Pragma}; use hyper::StatusCode; -use mas_config::{OAuth2ClientConfig, OAuth2Config}; +use mas_config::{HttpConfig, OAuth2ClientConfig, OAuth2Config}; use mas_data_model::{AuthorizationGrantStage, TokenType}; use mas_iana::{jose::JsonWebSignatureAlg, oauth::OAuthClientAuthenticationMethod}; use mas_jose::{ @@ -37,7 +37,7 @@ use mas_storage::{ }; use mas_warp_utils::{ errors::WrapError, - filters::{client::client_authentication, database::connection}, + filters::{client::client_authentication, database::connection, url_builder::UrlBuilder}, reply::with_typed_header, }; use oauth2_types::{ @@ -99,14 +99,13 @@ pub fn filter( pool: &PgPool, key_store: &Arc, oauth2_config: &OAuth2Config, + http_config: &HttpConfig, ) -> BoxedFilter<(Box,)> { let key_store = key_store.clone(); - let audience = oauth2_config - .issuer - .join("/oauth2/token") - .unwrap() - .to_string(); - let issuer = oauth2_config.issuer.clone(); + let builder = UrlBuilder::from(http_config); + let audience = builder.oauth_token_endpoint().to_string(); + + let issuer = builder.oidc_issuer(); warp::path!("oauth2" / "token") .and( diff --git a/crates/handlers/src/views/account/emails.rs b/crates/handlers/src/views/account/emails.rs index 518c4553..a8962e0e 100644 --- a/crates/handlers/src/views/account/emails.rs +++ b/crates/handlers/src/views/account/emails.rs @@ -13,7 +13,7 @@ // limitations under the License. use lettre::{message::Mailbox, Address}; -use mas_config::{CookiesConfig, CsrfConfig, OAuth2Config}; +use mas_config::{CookiesConfig, CsrfConfig, HttpConfig}; use mas_data_model::{BrowserSession, User, UserEmail}; use mas_email::Mailer; use mas_storage::{ @@ -31,6 +31,7 @@ use mas_warp_utils::{ csrf::{protected_form, updated_csrf_token}, database::{connection, transaction}, session::session, + url_builder::{url_builder, UrlBuilder}, with_templates, CsrfToken, }, }; @@ -38,21 +39,18 @@ use rand::{distributions::Alphanumeric, thread_rng, Rng}; use serde::Deserialize; use sqlx::{pool::PoolConnection, PgExecutor, PgPool, Postgres, Transaction}; use tracing::info; -use url::Url; use warp::{filters::BoxedFilter, reply::html, Filter, Rejection, Reply}; pub(super) fn filter( pool: &PgPool, templates: &Templates, mailer: &Mailer, - oauth2_config: &OAuth2Config, + http_config: &HttpConfig, csrf_config: &CsrfConfig, cookies_config: &CookiesConfig, ) -> BoxedFilter<(Box,)> { let mailer = mailer.clone(); - let base = oauth2_config.issuer.clone(); - let get = with_templates(templates) .and(encrypted_cookie_saver(cookies_config)) .and(updated_csrf_token(cookies_config, csrf_config)) @@ -62,7 +60,7 @@ pub(super) fn filter( let post = with_templates(templates) .and(warp::any().map(move || mailer.clone())) - .and(warp::any().map(move || base.clone())) + .and(url_builder(http_config)) .and(encrypted_cookie_saver(cookies_config)) .and(updated_csrf_token(cookies_config, csrf_config)) .and(session(pool, cookies_config)) @@ -120,7 +118,7 @@ async fn render( async fn start_email_verification( mailer: &Mailer, - base: &Url, + url_builder: &UrlBuilder, executor: impl PgExecutor<'_>, user: &User, user_email: &UserEmail, @@ -139,8 +137,7 @@ async fn start_email_verification( let mailbox = Mailbox::new(Some(user.username.clone()), address); - let link = base.join("./verify/")?; - let link = link.join(&code)?; + let link = url_builder.email_verification(&code); let context = EmailVerificationContext::new(user.clone().into(), link); @@ -154,7 +151,7 @@ async fn start_email_verification( async fn post( templates: Templates, mailer: Mailer, - base: Url, + url_builder: UrlBuilder, cookie_saver: EncryptedCookieSaver, csrf_token: CsrfToken, mut session: BrowserSession, @@ -166,7 +163,7 @@ async fn post( let user_email = add_user_email(&mut txn, &session.user, email) .await .wrap_error()?; - start_email_verification(&mailer, &base, &mut txn, &session.user, &user_email) + start_email_verification(&mailer, &url_builder, &mut txn, &session.user, &user_email) .await .wrap_error()?; } @@ -184,7 +181,7 @@ async fn post( .await .wrap_error()?; - start_email_verification(&mailer, &base, &mut txn, &session.user, &user_email) + start_email_verification(&mailer, &url_builder, &mut txn, &session.user, &user_email) .await .wrap_error()?; } diff --git a/crates/handlers/src/views/account/mod.rs b/crates/handlers/src/views/account/mod.rs index 0c041f1f..7ff981c1 100644 --- a/crates/handlers/src/views/account/mod.rs +++ b/crates/handlers/src/views/account/mod.rs @@ -15,7 +15,7 @@ mod emails; mod password; -use mas_config::{CookiesConfig, CsrfConfig, OAuth2Config}; +use mas_config::{CookiesConfig, CsrfConfig, HttpConfig}; use mas_data_model::BrowserSession; use mas_email::Mailer; use mas_storage::{ @@ -42,7 +42,7 @@ pub(super) fn filter( pool: &PgPool, templates: &Templates, mailer: &Mailer, - oauth2_config: &OAuth2Config, + http_config: &HttpConfig, csrf_config: &CsrfConfig, cookies_config: &CookiesConfig, ) -> BoxedFilter<(Box,)> { @@ -60,7 +60,7 @@ pub(super) fn filter( pool, templates, mailer, - oauth2_config, + http_config, csrf_config, cookies_config, ); diff --git a/crates/handlers/src/views/index.rs b/crates/handlers/src/views/index.rs index cf49758c..4061a5e2 100644 --- a/crates/handlers/src/views/index.rs +++ b/crates/handlers/src/views/index.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use mas_config::{CookiesConfig, CsrfConfig, OAuth2Config}; +use mas_config::{CookiesConfig, CsrfConfig, HttpConfig}; use mas_data_model::BrowserSession; use mas_storage::PostgresqlBackend; use mas_templates::{IndexContext, TemplateContext, Templates}; @@ -20,23 +20,22 @@ use mas_warp_utils::filters::{ cookies::{encrypted_cookie_saver, EncryptedCookieSaver}, csrf::updated_csrf_token, session::optional_session, + url_builder::{url_builder, UrlBuilder}, with_templates, CsrfToken, }; use sqlx::PgPool; -use url::Url; use warp::{filters::BoxedFilter, reply::html, Filter, Rejection, Reply}; pub(super) fn filter( pool: &PgPool, templates: &Templates, - oauth2_config: &OAuth2Config, + http_config: &HttpConfig, csrf_config: &CsrfConfig, cookies_config: &CookiesConfig, ) -> BoxedFilter<(Box,)> { - let discovery_url = oauth2_config.discovery_url(); warp::path::end() .and(warp::get()) - .map(move || discovery_url.clone()) + .and(url_builder(http_config)) .and(with_templates(templates)) .and(encrypted_cookie_saver(cookies_config)) .and(updated_csrf_token(cookies_config, csrf_config)) @@ -46,13 +45,13 @@ pub(super) fn filter( } async fn get( - discovery_url: Url, + url_builder: UrlBuilder, templates: Templates, cookie_saver: EncryptedCookieSaver, csrf_token: CsrfToken, maybe_session: Option>, ) -> Result, Rejection> { - let ctx = IndexContext::new(discovery_url) + let ctx = IndexContext::new(url_builder.oidc_discovery()) .maybe_with_session(maybe_session) .with_csrf(csrf_token.form_value()); diff --git a/crates/handlers/src/views/mod.rs b/crates/handlers/src/views/mod.rs index 7f16ffb0..f1f9262f 100644 --- a/crates/handlers/src/views/mod.rs +++ b/crates/handlers/src/views/mod.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use mas_config::{CookiesConfig, CsrfConfig, OAuth2Config}; +use mas_config::{CookiesConfig, CsrfConfig, HttpConfig}; use mas_email::Mailer; use mas_templates::Templates; use sqlx::PgPool; @@ -40,16 +40,16 @@ pub(super) fn filter( pool: &PgPool, templates: &Templates, mailer: &Mailer, - oauth2_config: &OAuth2Config, + http_config: &HttpConfig, csrf_config: &CsrfConfig, cookies_config: &CookiesConfig, ) -> BoxedFilter<(Box,)> { - let index = index(pool, templates, oauth2_config, csrf_config, cookies_config); + let index = index(pool, templates, http_config, csrf_config, cookies_config); let account = account( pool, templates, mailer, - oauth2_config, + http_config, csrf_config, cookies_config, ); diff --git a/crates/warp-utils/Cargo.toml b/crates/warp-utils/Cargo.toml index a32bdc9c..23f075b7 100644 --- a/crates/warp-utils/Cargo.toml +++ b/crates/warp-utils/Cargo.toml @@ -36,3 +36,4 @@ mas-data-model = { path = "../data-model" } mas-storage = { path = "../storage" } mas-jose = { path = "../jose" } mas-iana = { path = "../iana" } +url = "2.2.2" diff --git a/crates/warp-utils/src/filters/mod.rs b/crates/warp-utils/src/filters/mod.rs index ed9f29af..71ef66bb 100644 --- a/crates/warp-utils/src/filters/mod.rs +++ b/crates/warp-utils/src/filters/mod.rs @@ -25,6 +25,7 @@ pub mod csrf; pub mod database; pub mod headers; pub mod session; +pub mod url_builder; use std::convert::Infallible; diff --git a/crates/warp-utils/src/filters/url_builder.rs b/crates/warp-utils/src/filters/url_builder.rs new file mode 100644 index 00000000..2df2b5c0 --- /dev/null +++ b/crates/warp-utils/src/filters/url_builder.rs @@ -0,0 +1,92 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Utility to build URLs + +// TODO: move this somewhere else + +use std::convert::Infallible; + +use mas_config::HttpConfig; +use url::Url; +use warp::Filter; + +impl From<&HttpConfig> for UrlBuilder { + fn from(config: &HttpConfig) -> Self { + let base = config.public_base.clone(); + Self { base } + } +} + +/// Helps building absolute URLs +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct UrlBuilder { + base: Url, +} + +impl UrlBuilder { + /// OIDC issuer + pub fn oidc_issuer(&self) -> Url { + self.base.clone() + } + + /// OIDC dicovery document URL + pub fn oidc_discovery(&self) -> Url { + self.base + .join(".well-known/openid-configuration") + .expect("build URL") + } + + /// OAuth 2.0 authorization endpoint + pub fn oauth_authorization_endpoint(&self) -> Url { + self.base.join("oauth2/authorize").expect("build URL") + } + + /// OAuth 2.0 token endpoint + pub fn oauth_token_endpoint(&self) -> Url { + self.base.join("oauth2/token").expect("build URL") + } + + /// OAuth 2.0 introspection endpoint + pub fn oauth_introspection_endpoint(&self) -> Url { + self.base.join("oauth2/introspect").expect("build URL") + } + + /// OAuth 2.0 introspection endpoint + pub fn oidc_userinfo_endpoint(&self) -> Url { + self.base.join("oauth2/userinfo").expect("build URL") + } + + /// JWKS URI + pub fn jwks_uri(&self) -> Url { + self.base.join("oauth2/jwks.json").expect("build URL") + } + + /// Email verification URL + pub fn email_verification(&self, code: &str) -> Url { + self.base + .join("verify") + .expect("build URL") + .join(code) + .expect("build URL") + } +} + +/// Injects an [`UrlBuilder`] to help building absolute URLs +pub fn url_builder( + config: &HttpConfig, +) -> impl Filter + Clone + Send + Sync + 'static { + let builder: UrlBuilder = config.into(); + warp::any().map(move || builder.clone()) +}