1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-07-31 09:24:31 +03:00

Have a unified URL builder/router

This commit is contained in:
Quentin Gliech
2022-05-10 09:52:27 +02:00
parent 0ac4fddee4
commit f4353b660e
28 changed files with 684 additions and 371 deletions

13
crates/router/Cargo.toml Normal file
View File

@ -0,0 +1,13 @@
[package]
name = "mas-router"
version = "0.1.0"
authors = ["Quentin Gliech <quenting@element.io>"]
edition = "2021"
license = "Apache-2.0"
[dependencies]
axum = { version = "0.5.4", default-features = false }
serde = { version = "1.0.137", features = ["derive"] }
serde_urlencoded = "0.7.1"
serde_with = "1.13.0"
url = "2.2.2"

View File

@ -0,0 +1,359 @@
// 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.
use serde::{Deserialize, Serialize};
pub use crate::traits::*;
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(rename_all = "snake_case", tag = "next")]
pub enum PostAuthAction {
ContinueAuthorizationGrant {
#[serde(deserialize_with = "serde_with::rust::display_fromstr::deserialize")]
data: i64,
},
}
impl PostAuthAction {
#[must_use]
pub fn continue_grant(data: i64) -> Self {
PostAuthAction::ContinueAuthorizationGrant { data }
}
#[must_use]
pub fn go_next(&self) -> axum::response::Redirect {
match self {
Self::ContinueAuthorizationGrant { data } => ContinueAuthorizationGrant(*data).go(),
}
}
}
/// `GET /.well-known/openid-configuration`
#[derive(Debug, Clone)]
pub struct OidcConfiguration;
impl SimpleRoute for OidcConfiguration {
const PATH: &'static str = "/.well-known/openid-configuration";
}
/// `GET /.well-known/webfinger`
#[derive(Debug, Clone)]
pub struct Webfinger;
impl SimpleRoute for Webfinger {
const PATH: &'static str = "/.well-known/webfinger";
}
/// `GET /oauth2/keys.json`
#[derive(Debug, Clone)]
pub struct OAuth2Keys;
impl SimpleRoute for OAuth2Keys {
const PATH: &'static str = "/oauth2/keys.json";
}
/// `GET /oauth2/userinfo`
#[derive(Debug, Clone)]
pub struct OidcUserinfo;
impl SimpleRoute for OidcUserinfo {
const PATH: &'static str = "/oauth2/userinfo";
}
/// `POST /oauth2/userinfo`
#[derive(Debug, Clone)]
pub struct OAuth2Introspection;
impl SimpleRoute for OAuth2Introspection {
const PATH: &'static str = "/oauth2/introspect";
}
/// `POST /oauth2/token`
#[derive(Debug, Clone)]
pub struct OAuth2TokenEndpoint;
impl SimpleRoute for OAuth2TokenEndpoint {
const PATH: &'static str = "/oauth2/token";
}
/// `POST /oauth2/registration`
#[derive(Debug, Clone)]
pub struct OAuth2RegistrationEndpoint;
impl SimpleRoute for OAuth2RegistrationEndpoint {
const PATH: &'static str = "/oauth2/registration";
}
/// `GET /authorize`
#[derive(Debug, Clone)]
pub struct OAuth2AuthorizationEndpoint;
impl SimpleRoute for OAuth2AuthorizationEndpoint {
const PATH: &'static str = "/authorize";
}
/// `GET /`
#[derive(Debug, Clone)]
pub struct Index;
impl SimpleRoute for Index {
const PATH: &'static str = "/";
}
/// `GET /health`
#[derive(Debug, Clone)]
pub struct Healthcheck;
impl SimpleRoute for Healthcheck {
const PATH: &'static str = "/health";
}
/// `GET|POST /login`
#[derive(Default, Debug, Clone)]
pub struct Login {
post_auth_action: Option<PostAuthAction>,
}
impl Route for Login {
type Query = PostAuthAction;
fn route() -> &'static str {
"/login"
}
fn query(&self) -> Option<&Self::Query> {
self.post_auth_action.as_ref()
}
}
impl Login {
#[must_use]
pub fn and_then(action: PostAuthAction) -> Self {
Self {
post_auth_action: Some(action),
}
}
#[must_use]
pub fn and_continue_grant(data: i64) -> Self {
Self {
post_auth_action: Some(PostAuthAction::continue_grant(data)),
}
}
/// Get a reference to the login's post auth action.
#[must_use]
pub fn post_auth_action(&self) -> Option<&PostAuthAction> {
self.post_auth_action.as_ref()
}
#[must_use]
pub fn go_next(&self) -> axum::response::Redirect {
match &self.post_auth_action {
Some(action) => action.go_next(),
None => Index.go(),
}
}
}
impl From<Option<PostAuthAction>> for Login {
fn from(post_auth_action: Option<PostAuthAction>) -> Self {
Self { post_auth_action }
}
}
/// `POST /logout`
#[derive(Debug, Clone)]
pub struct Logout;
impl SimpleRoute for Logout {
const PATH: &'static str = "/logout";
}
/// `GET|POST /reauth`
#[derive(Default, Debug, Clone)]
pub struct Reauth {
post_auth_action: Option<PostAuthAction>,
}
impl Reauth {
#[must_use]
pub fn and_then(action: PostAuthAction) -> Self {
Self {
post_auth_action: Some(action),
}
}
#[must_use]
pub fn and_continue_grant(data: i64) -> Self {
Self {
post_auth_action: Some(PostAuthAction::continue_grant(data)),
}
}
/// Get a reference to the reauth's post auth action.
#[must_use]
pub fn post_auth_action(&self) -> Option<&PostAuthAction> {
self.post_auth_action.as_ref()
}
#[must_use]
pub fn go_next(&self) -> axum::response::Redirect {
match &self.post_auth_action {
Some(action) => action.go_next(),
None => Index.go(),
}
}
}
impl Route for Reauth {
type Query = PostAuthAction;
fn route() -> &'static str {
"/reauth"
}
fn query(&self) -> Option<&Self::Query> {
self.post_auth_action.as_ref()
}
}
impl From<Option<PostAuthAction>> for Reauth {
fn from(post_auth_action: Option<PostAuthAction>) -> Self {
Self { post_auth_action }
}
}
/// `GET|POST /register`
#[derive(Default, Debug, Clone)]
pub struct Register {
post_auth_action: Option<PostAuthAction>,
}
impl Register {
#[must_use]
pub fn and_then(action: PostAuthAction) -> Self {
Self {
post_auth_action: Some(action),
}
}
#[must_use]
pub fn and_continue_grant(data: i64) -> Self {
Self {
post_auth_action: Some(PostAuthAction::continue_grant(data)),
}
}
/// Get a reference to the reauth's post auth action.
#[must_use]
pub fn post_auth_action(&self) -> Option<&PostAuthAction> {
self.post_auth_action.as_ref()
}
#[must_use]
pub fn go_next(&self) -> axum::response::Redirect {
match &self.post_auth_action {
Some(action) => action.go_next(),
None => Index.go(),
}
}
}
impl Route for Register {
type Query = PostAuthAction;
fn route() -> &'static str {
"/register"
}
fn query(&self) -> Option<&Self::Query> {
self.post_auth_action.as_ref()
}
}
impl From<Option<PostAuthAction>> for Register {
fn from(post_auth_action: Option<PostAuthAction>) -> Self {
Self { post_auth_action }
}
}
/// `GET /verify/:code`
#[derive(Debug, Clone)]
pub struct VerifyEmail(pub String);
impl Route for VerifyEmail {
type Query = ();
fn route() -> &'static str {
"/verify/:code"
}
fn path(&self) -> std::borrow::Cow<'static, str> {
format!("/verify/{}", self.0).into()
}
}
/// `GET /account`
#[derive(Debug, Clone)]
pub struct Account;
impl SimpleRoute for Account {
const PATH: &'static str = "/account";
}
/// `GET|POST /account/password`
#[derive(Debug, Clone)]
pub struct AccountPassword;
impl SimpleRoute for AccountPassword {
const PATH: &'static str = "/account/password";
}
/// `GET|POST /account/emails`
#[derive(Debug, Clone)]
pub struct AccountEmails;
impl SimpleRoute for AccountEmails {
const PATH: &'static str = "/account/emails";
}
/// `GET /authorize/:grant_id`
#[derive(Debug, Clone)]
pub struct ContinueAuthorizationGrant(pub i64);
impl Route for ContinueAuthorizationGrant {
type Query = ();
fn route() -> &'static str {
"/authorize/:grant_id"
}
fn path(&self) -> std::borrow::Cow<'static, str> {
format!("/authorize/{}", self.0).into()
}
}
/// `GET /consent/:grant_id`
#[derive(Debug, Clone)]
pub struct Consent(pub i64);
impl Route for Consent {
type Query = ();
fn route() -> &'static str {
"/consent/:grant_id"
}
fn path(&self) -> std::borrow::Cow<'static, str> {
format!("/consent/{}", self.0).into()
}
}

53
crates/router/src/lib.rs Normal file
View File

@ -0,0 +1,53 @@
// 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.
#![deny(clippy::pedantic)]
pub(crate) mod endpoints;
pub(crate) mod traits;
mod url_builder;
pub use self::{endpoints::*, traits::Route, url_builder::UrlBuilder};
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use url::Url;
use super::*;
#[test]
fn test_relative_urls() {
assert_eq!(
OidcConfiguration.relative_url(),
Cow::Borrowed("/.well-known/openid-configuration")
);
assert_eq!(Index.relative_url(), Cow::Borrowed("/"));
assert_eq!(
Login::and_continue_grant(42).relative_url(),
Cow::Borrowed("/login?next=continue_authorization_grant&data=42")
);
}
#[test]
fn test_absolute_urls() {
let base = Url::try_from("https://example.com/").unwrap();
assert_eq!(Index.absolute_url(&base).as_str(), "https://example.com/");
assert_eq!(
OidcConfiguration.absolute_url(&base).as_str(),
"https://example.com/.well-known/openid-configuration"
);
}
}

View File

@ -0,0 +1,60 @@
// 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.
use std::borrow::{Borrow, Cow};
use serde::Serialize;
use url::Url;
pub trait Route {
type Query: Serialize;
fn route() -> &'static str;
fn query(&self) -> Option<&Self::Query> {
None
}
fn path(&self) -> Cow<'static, str> {
Cow::Borrowed(Self::route())
}
fn relative_url(&self) -> Cow<'static, str> {
let path = self.path();
if let Some(query) = self.query() {
let query = serde_urlencoded::to_string(query).unwrap();
format!("{}?{}", path, query).into()
} else {
path
}
}
fn absolute_url(&self, base: &Url) -> Url {
let relative = self.relative_url();
base.join(relative.borrow()).unwrap()
}
fn go(&self) -> axum::response::Redirect {
axum::response::Redirect::to(&self.relative_url())
}
}
pub trait SimpleRoute {
const PATH: &'static str;
}
impl<T: SimpleRoute> Route for T {
type Query = ();
fn route() -> &'static str {
Self::PATH
}
}

View File

@ -0,0 +1,108 @@
// 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
use url::Url;
use crate::traits::Route;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct UrlBuilder {
base: Url,
}
impl UrlBuilder {
fn url_for<U>(&self, destination: &U) -> Url
where
U: Route,
{
destination.absolute_url(&self.base)
}
/// Create a new [`UrlBuilder`] from a base URL
#[must_use]
pub fn new(base: Url) -> Self {
Self { base }
}
/// OIDC issuer
#[must_use]
pub fn oidc_issuer(&self) -> Url {
self.base.clone()
}
/// OIDC dicovery document URL
#[must_use]
pub fn oidc_discovery(&self) -> Url {
self.url_for(&crate::endpoints::OidcConfiguration)
}
/// OAuth 2.0 authorization endpoint
#[must_use]
pub fn oauth_authorization_endpoint(&self) -> Url {
self.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)
}
/// OAuth 2.0 introspection endpoint
#[must_use]
pub fn oauth_introspection_endpoint(&self) -> Url {
self.url_for(&crate::endpoints::OAuth2Introspection)
}
/// OAuth 2.0 client registration endpoint
#[must_use]
pub fn oauth_registration_endpoint(&self) -> Url {
self.url_for(&crate::endpoints::OAuth2RegistrationEndpoint)
}
// OIDC userinfo endpoint
#[must_use]
pub fn oidc_userinfo_endpoint(&self) -> Url {
self.url_for(&crate::endpoints::OidcUserinfo)
}
/// JWKS URI
#[must_use]
pub fn jwks_uri(&self) -> Url {
self.url_for(&crate::endpoints::OAuth2Keys)
}
/// Email verification URL
#[must_use]
pub fn email_verification(&self, code: String) -> Url {
self.url_for(&crate::endpoints::VerifyEmail(code))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_email_verification_url() {
let base = Url::parse("https://example.com/").unwrap();
let builder = UrlBuilder::new(base);
assert_eq!(
builder.email_verification("123456abcdef".into()).as_str(),
"https://example.com/verify/123456abcdef"
);
}
}