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

Open
avdb13 wants to merge 11 commits from oidc into next
34 changed files with 2823 additions and 242 deletions

1453
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -30,14 +30,16 @@ workspace = true
[dependencies]
# Web framework
axum = { version = "0.6.18", default-features = false, features = ["form", "headers", "http1", "http2", "json", "matched-path"], optional = true }
axum = { version = "0.6.18", default-features = false, features = ["form", "headers", "http1", "http2", "json", "matched-path", "query"], optional = true }
axum-extra = { version = "0.8.0", features = ["cookie"] }
axum-server = { version = "0.5.1", features = ["tls-rustls"] }
tower = { version = "0.4.13", features = ["util"] }
tower-http = { version = "0.4.1", features = ["add-extension", "cors", "sensitive-headers", "trace", "util"] }
# Used for matrix spec type definitions and helpers
#ruma = { version = "0.4.0", features = ["compat", "rand", "appservice-api-c", "client-api", "federation-api", "push-gateway-api-c", "state-res", "unstable-pre-spec", "unstable-exhaustive-types"] }
ruma = { git = "https://github.com/ruma/ruma", rev = "1a1c61ee1e8f0936e956a3b69c931ce12ee28475", features = ["compat", "rand", "appservice-api-c", "client-api", "federation-api", "push-gateway-api-c", "state-res", "unstable-msc2448", "unstable-msc3575", "unstable-exhaustive-types", "ring-compat", "unstable-unspecified" ] }
#ruma = { git = "https://github.com/ruma/ruma", rev = "1a1c61ee1e8f0936e956a3b69c931ce12ee28475", features = ["compat", "rand", "appservice-api-c", "client-api", "federation-api", "push-gateway-api-c", "state-res", "unstable-msc2448", "unstable-msc3575", "unstable-exhaustive-types", "ring-compat", "unstable-unspecified" ] }
ruma = { git = "https://github.com/avdb13/ruma", branch = "main", features = ["compat", "rand", "appservice-api-c", "client-api", "federation-api", "push-gateway-api-c", "state-res", "unstable-msc2448", "unstable-msc3575", "unstable-exhaustive-types", "ring-compat", "unstable-unspecified" ] }
#ruma = { git = "https://github.com/timokoesters/ruma", rev = "4ec9c69bb7e09391add2382b3ebac97b6e8f4c64", features = ["compat", "rand", "appservice-api-c", "client-api", "federation-api", "push-gateway-api-c", "state-res", "unstable-msc2448", "unstable-msc3575", "unstable-exhaustive-types", "ring-compat", "unstable-unspecified" ] }
#ruma = { path = "../ruma/crates/ruma", features = ["compat", "rand", "appservice-api-c", "client-api", "federation-api", "push-gateway-api-c", "state-res", "unstable-msc2448", "unstable-msc3575", "unstable-exhaustive-types", "ring-compat", "unstable-unspecified" ] }
@ -115,12 +117,21 @@ lazy_static = "1.4.0"
async-trait = "0.1.68"
sd-notify = { version = "0.4.1", optional = true }
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"] }
[features]
default = ["conduit_bin", "backend_sqlite", "backend_rocksdb", "systemd"]
#default = ["conduit_bin", "backend_sqlite", "backend_rocksdb", "systemd"]
default = ["conduit_bin", "backend_rocksdb"]
#backend_sled = ["sled"]
backend_persy = ["persy", "parking_lot"]
backend_sqlite = ["sqlite"]

2
askama.toml Normal file
View file

@ -0,0 +1,2 @@
[general]
dirs = ["public/templates"]

View file

@ -20,10 +20,10 @@
# for more information
# YOU NEED TO EDIT THIS
#server_name = "your.server.name"
server_name = "localhost:6167"
# This is the only directory where Conduit will save its data
database_path = "/var/lib/matrix-conduit/"
database_path = "./db"
database_backend = "rocksdb"
# The port Conduit will be running on. You need to set up a reverse proxy in
@ -51,7 +51,48 @@ enable_lightning_bolt = true
trusted_servers = ["matrix.org"]
#max_concurrent_requests = 100 # How many requests Conduit sends to other servers at the same time
#log = "warn,state_res=warn,rocket=off,_=off,sled=off"
log = "info"
address = "127.0.0.1" # This makes sure Conduit can only be reached using the reverse proxy
#address = "0.0.0.0" # If Conduit is running in a container, make sure the reverse proxy (ie. Traefik) can reach it.
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"
scopes = ["openid", "profile"]
issuer = "https://gitlab.com"
[global.sso.client]
id = "12dd00d057420beda06fd5edcd21287026dc0c66ba5c02d40c2eff8b559c6709"
secret = "3a806573cacf5da560b8c720cf32019255908c83e31ba78d280cf08a4eb619fd"
auth_method = "client_secret_post"

View file

@ -253,6 +253,10 @@
# Needed for our script for Complement
jq
# Needed for SSO
pkgs.openssl
pkgs.pkg-config
]);
};
});

6
public/conduit.svg Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="windows-1252"?>
<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
<path fill="#FFB636" d="M459.866,218.346l-186.7,0.701c-4.619,0.017-7.618-4.861-5.517-8.975L370.845,8.024 c3.103-6.075-4.493-11.949-9.592-7.417L39.948,286.141c-4.221,3.751-1.602,10.732,4.045,10.78l170.444,1.457 c4.443,0.038,7.391,4.619,5.583,8.679L133.317,501.73c-2.688,6.035,4.709,11.501,9.689,7.16l320.937-279.725 C468.25,225.412,465.58,218.325,459.866,218.346z"/>
</svg>

After

Width:  |  Height:  |  Size: 821 B

View file

@ -0,0 +1,24 @@
{% extends "base.html" %}
{% block title %}Confirm it's you{% endblock %}
{% block header %}
{% endblock %}
{% block body %}
<header>
<h1>Confirm it's you to continue</h1>
<p>
A client is trying to {{ description }}. To confirm this action
re-authorize your account with single sign-on.
</p>
<p><strong>
If you did not expect this, your account may be compromised.
</strong></p>
</header>
<main>
<a href="{{ redirect_url }}" class="primary-button">
Continue with {{ idp_name }}
</a>
</main>
{% include "footer.html" %}
{% endblock %}

View file

@ -0,0 +1,23 @@
{% extends "base.html" %}
{% block title %}Authentication failed{% endblock %}
{% block header %}
{% endblock %}
{% block body %}
<div class="error_page">
<header>
<h1>That doesn't look right</h1>
<p>
<strong>We were unable to validate your {{ server_name }} account</strong>
via single&nbsp;sign&#8209;on&nbsp;(SSO), because the SSO Identity
Provider returned different details than when you logged in.
</p>
<p>
Try the operation again, and ensure that you use the same details on
the Identity Provider as when you log into your account.
</p>
</header>
</div>
{% include "footer.html" %}
{% endblock %}

View file

@ -0,0 +1,25 @@
{% extends "base.html" %}
{% block title %}Authentication successful{% endblock %}
{% block header %}
<script>
{{"
if (window.onAuthDone) {
window.onAuthDone();
} else if (window.opener && window.opener.postMessage) {
window.opener.postMessage(\"authDone\", \"*\");
}
"}}
</script>
{% endblock %}
{% block body %}
<header>
<h1>Thank you</h1>
<p>
Now we know its you, you can close this window and return to the
application.
</p>
</header>
{% include "footer.html" %}
{% endblock %}

152
public/templates/base.html Normal file
View file

@ -0,0 +1,152 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{% endblock %}</title>
<!-- <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> -->
<style type="text/css">
{{"
body, input, select, textarea {
font-family: \"Inter\", \"Helvetica\", \"Arial\", sans-serif;
font-size: 14px;
color: #17191C;
}
header, footer {
max-width: 480px;
width: 100%;
margin: 24px auto;
text-align: center;
}
@media screen and (min-width: 800px) {
header {
margin-top: 90px;
}
}
header {
min-height: 60px;
}
header p {
color: #737D8C;
line-height: 24px;
}
h1 {
font-size: 24px;
}
a {
color: #418DED;
}
.error_page h1 {
color: #FE2928;
}
h2 {
font-size: 14px;
}
h2 img {
vertical-align: middle;
margin-right: 8px;
width: 24px;
height: 24px;
}
label {
cursor: pointer;
}
main {
max-width: 360px;
width: 100%;
margin: 24px auto;
}
.primary-button {
border: none;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
text-decoration: none;
padding: 12px;
color: white;
background-color: #418DED;
font-weight: bold;
display: block;
border-radius: 12px;
width: 100%;
box-sizing: border-box;
margin: 16px 0;
cursor: pointer;
text-align: center;
}
.profile {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 24px;
padding: 13px;
border: 1px solid #E9ECF1;
border-radius: 4px;
}
.profile.with-avatar {
margin-top: 42px; /* (36px / 2) + 24px*/
}
.profile .avatar {
width: 36px;
height: 36px;
border-radius: 100%;
display: block;
margin-top: -32px;
margin-bottom: 8px;
}
.profile .display-name {
font-weight: bold;
margin-bottom: 4px;
font-size: 15px;
line-height: 18px;
}
.profile .user-id {
color: #737D8C;
font-size: 12px;
line-height: 12px;
}
footer {
margin-top: 80px;
}
footer svg {
display: block;
width: 46px;
margin: 0px auto 12px auto;
}
footer p {
color: #737D8C;
}
"}}
{% block style %}{% endblock %}
</style>
</head>
<body>
<header class="mx_Header">
<img src="https://matrix.org/img/matrix-120x51.png" width="120" height="51" alt="[matrix]"/>
</header>
{% block body %}{% endblock %}
</body>
</html>

View file

@ -0,0 +1,22 @@
{% extends "base.html" %}
{% block title %}SSO account deactivated{% endblock %}
{% block header %}
{% endblock %}
{% block body %}
<div class="error_page">
<header>
<h1>Your account has been deactivated</h1>
<p>
<strong>No account found</strong>
</p>
<p>
Your account might have been deactivated by the server administrator.
You can either try to create a new account or contact the servers
administrator.
</p>
</header>
</div>
{% include "footer.html" %}
{% endblock %}

View file

@ -0,0 +1,71 @@
{% extends "base.html" %}
{% block title %}Authentication failed{% endblock %}
{% block header %}
{% if error == "unauthorised" %}
<style type="text/css">
{{"
#error_code {
margin-top: 56px;
}
"}}
</style>
{% endif %}
{% endblock %}
{% block body %}
<div class="error_page">
{# If an error of unauthorised is returned it means we have actively rejected their login #}
{% if error == "unauthorised" %}
<header>
<p>You are not allowed to log in here.</p>
</header>
{% else %}
<header>
<h1>There was an error</h1>
<p>
<strong id="errormsg">{{ error_description }}</strong>
</p>
<p>
If you are seeing this page after clicking a link sent to you via email,
make sure you only click the confirmation link once, and that you open
the validation link in the same client you're logging in from.
</p>
<p>
Try logging in again from your Matrix client and if the problem persists
please contact the server's administrator.
</p>
<div id="error_code">
<p><strong>Error code</strong></p>
<p>{{ error }}</p>
</div>
</header>
{% include "footer.html" %}
<script type="text/javascript">
// Error handling to support Auth0 errors that we might get through a GET request
// to the validation endpoint. If an error is provided, it's either going to be
// located in the query string or in a query string-like URI fragment.
// We try to locate the error from any of these two locations, but if we can't
// we just don't print anything specific.
let searchStr = "";
if (window.location.search) {
// window.location.searchParams isn't always defined when
// window.location.search is, so it's more reliable to parse the latter.
searchStr = window.location.search;
} else if (window.location.hash) {
// Replace the # with a ? so that URLSearchParams does the right thing and
// doesn't parse the first parameter incorrectly.
searchStr = window.location.hash.replace("#", "?");
}
// We might end up with no error in the URL, so we need to check if we have one
// to print one.
let errorDesc = new URLSearchParams(searchStr).get("error_description")
if (errorDesc) {
document.getElementById("errormsg").innerText = errorDesc;
}
</script>
{% endif %}
</div>
{% endblock %}

View file

@ -0,0 +1,19 @@
<footer>
<svg role="img" aria-label="[Matrix logo]" viewBox="0 0 200 85" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="parent" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="child" transform="translate(-122.000000, -6.000000)" fill="#000000" fill-rule="nonzero">
<g id="matrix-logo" transform="translate(122.000000, 6.000000)">
<polygon id="left-bracket" points="2.24708861 1.93811009 2.24708861 82.7268844 8.10278481 82.7268844 8.10278481 84.6652459 0 84.6652459 0 0 8.10278481 0 8.10278481 1.93811009"></polygon>
<path d="M24.8073418,27.5493174 L24.8073418,31.6376991 L24.924557,31.6376991 C26.0227848,30.0814294 27.3455696,28.8730642 28.8951899,28.0163743 C30.4437975,27.1611927 32.2189873,26.7318422 34.218481,26.7318422 C36.1394937,26.7318422 37.8946835,27.102622 39.4825316,27.8416679 C41.0708861,28.5819706 42.276962,29.8856073 43.1005063,31.7548404 C44.0017722,30.431345 45.2270886,29.2629486 46.7767089,28.2506569 C48.3253165,27.2388679 50.158481,26.7318422 52.2764557,26.7318422 C53.8843038,26.7318422 55.3736709,26.9269101 56.7473418,27.3162917 C58.1189873,27.7056734 59.295443,28.3285835 60.2759494,29.185022 C61.255443,30.0422147 62.02,31.1615927 62.5701266,32.5426532 C63.1187342,33.9262275 63.3936709,35.5898349 63.3936709,37.5372459 L63.3936709,57.7443688 L55.0410127,57.7441174 L55.0410127,40.6319376 C55.0410127,39.6201486 55.0020253,38.6661761 54.9232911,37.7700202 C54.8440506,36.8751211 54.6293671,36.0968606 54.2764557,35.4339817 C53.9232911,34.772611 53.403038,34.2464807 52.7177215,33.8568477 C52.0313924,33.4689743 51.0997468,33.2731523 49.9235443,33.2731523 C48.7473418,33.2731523 47.7962025,33.4983853 47.0706329,33.944578 C46.344557,34.393033 45.7764557,34.9774826 45.3650633,35.6969211 C44.9534177,36.4181193 44.6787342,37.2353431 44.5417722,38.150855 C44.4037975,39.0653615 44.3356962,39.9904257 44.3356962,40.9247908 L44.3356962,57.7443688 L35.9835443,57.7443688 L35.9835443,40.8079009 C35.9835443,39.9124991 35.963038,39.0263982 35.9253165,38.150855 C35.8853165,37.2743064 35.7192405,36.4666349 35.424557,35.7263321 C35.1303797,34.9872862 34.64,34.393033 33.9539241,33.944578 C33.2675949,33.4983853 32.2579747,33.2731523 30.9248101,33.2731523 C30.5321519,33.2731523 30.0126582,33.3608826 29.3663291,33.5365945 C28.7192405,33.7118037 28.0913924,34.0433688 27.4840506,34.5292789 C26.875443,35.0164459 26.3564557,35.7172826 25.9250633,36.6315376 C25.4934177,37.5470495 25.2779747,38.7436 25.2779747,40.2229486 L25.2779747,57.7441174 L16.9260759,57.7443688 L16.9260759,27.5493174 L24.8073418,27.5493174 Z" id="m"></path>
<path d="M68.7455696,31.9886202 C69.6075949,30.7033339 70.7060759,29.672189 72.0397468,28.8926716 C73.3724051,28.1141596 74.8716456,27.5596239 76.5387342,27.2283101 C78.2050633,26.8977505 79.8817722,26.7315908 81.5678481,26.7315908 C83.0974684,26.7315908 84.6458228,26.8391798 86.2144304,27.0525982 C87.7827848,27.2675248 89.2144304,27.6865688 90.5086076,28.3087248 C91.8025316,28.9313835 92.8610127,29.7983798 93.6848101,30.9074514 C94.5083544,32.0170257 94.92,33.4870734 94.92,35.3173431 L94.92,51.026844 C94.92,52.3913138 94.998481,53.6941963 95.1556962,54.9400165 C95.3113924,56.1865908 95.5863291,57.120956 95.9787342,57.7436147 L87.5091139,57.7436147 C87.3518987,57.276055 87.2240506,56.7996972 87.1265823,56.3125303 C87.0278481,55.8266202 86.9592405,55.3301523 86.9207595,54.8236294 C85.5873418,56.1865908 84.0182278,57.1405633 82.2156962,57.6857982 C80.4113924,58.2295248 78.5683544,58.503022 76.6860759,58.503022 C75.2346835,58.503022 73.8817722,58.3275615 72.6270886,57.9776459 C71.3718987,57.6269761 70.2744304,57.082244 69.3334177,56.3411872 C68.3921519,55.602644 67.656962,54.6680275 67.1275949,53.5390972 C66.5982278,52.410167 66.3331646,51.065556 66.3331646,49.5087835 C66.3331646,47.7961578 66.6367089,46.384178 67.2455696,45.2756092 C67.8529114,44.1652807 68.6367089,43.2799339 69.5987342,42.6173064 C70.5589873,41.9556844 71.6567089,41.4592165 72.8924051,41.1284055 C74.1273418,40.7978459 75.3721519,40.5356606 76.6270886,40.3398385 C77.8820253,40.1457761 79.116962,39.9896716 80.3329114,39.873033 C81.5483544,39.7558917 82.6270886,39.5804312 83.5681013,39.3469028 C84.5093671,39.1133743 85.2536709,38.7732624 85.8032911,38.3250587 C86.3513924,37.8773578 86.6063291,37.2252881 86.5678481,36.3680954 C86.5678481,35.4731963 86.4210127,34.7620532 86.1268354,34.2366771 C85.8329114,33.7113009 85.4405063,33.3018092 84.9506329,33.0099615 C84.4602532,32.7181138 83.8916456,32.5232972 83.2450633,32.4255119 C82.5977215,32.3294862 81.9010127,32.2797138 81.156962,32.2797138 C79.5098734,32.2797138 78.2159494,32.6303835 77.2746835,33.3312202 C76.3339241,34.0320569 75.7837975,35.2007046 75.6275949,36.8354037 L67.275443,36.8354037 C67.3924051,34.8892495 67.8817722,33.2726495 68.7455696,31.9886202 Z M85.2440506,43.6984752 C84.7149367,43.873433 84.1460759,44.0189798 83.5387342,44.1361211 C82.9306329,44.253011 82.2936709,44.350545 81.6270886,44.4279688 C80.96,44.5066495 80.2934177,44.6034294 79.6273418,44.7203193 C78.9994937,44.8362037 78.3820253,44.9933138 77.7749367,45.1871248 C77.1663291,45.3829468 76.636962,45.6451321 76.1865823,45.9759431 C75.7349367,46.3070055 75.3724051,46.7263009 75.0979747,47.2313156 C74.8232911,47.7375872 74.6863291,48.380356 74.6863291,49.1588679 C74.6863291,49.8979138 74.8232911,50.5218294 75.0979747,51.026844 C75.3724051,51.5338697 75.7455696,51.9328037 76.2159494,52.2246514 C76.6863291,52.5164991 77.2349367,52.7213706 77.8632911,52.8375064 C78.4898734,52.9546477 79.136962,53.012967 79.8037975,53.012967 C81.4506329,53.012967 82.724557,52.740978 83.6273418,52.1952404 C84.5288608,51.6507596 85.1949367,50.9981872 85.6270886,50.2382771 C86.0579747,49.4793725 86.323038,48.7119211 86.4212658,47.9321523 C86.518481,47.1536404 86.5681013,46.5304789 86.5681013,46.063422 L86.5681013,42.9677248 C86.2146835,43.2799339 85.7736709,43.5230147 85.2440506,43.6984752 Z" id="a"></path>
<path d="M116.917975,27.5493174 L116.917975,33.0976917 L110.801266,33.0976917 L110.801266,48.0492936 C110.801266,49.4502128 111.036203,50.3850807 111.507089,50.8518862 C111.976962,51.3191945 112.918734,51.5527229 114.33038,51.5527229 C114.801013,51.5527229 115.251392,51.5336183 115.683038,51.4944037 C116.114177,51.4561945 116.526076,51.3968697 116.917975,51.3194459 L116.917975,57.7438661 C116.212152,57.860756 115.427595,57.9381798 114.565316,57.9778972 C113.702785,58.0153523 112.859747,58.0357138 112.036203,58.0357138 C110.742278,58.0357138 109.516456,57.9477321 108.36,57.7722716 C107.202785,57.5975651 106.183544,57.2577046 105.301519,56.7509303 C104.418987,56.2454128 103.722785,55.5242147 103.213418,54.5898495 C102.703038,53.6562385 102.448608,52.4292716 102.448608,50.9099541 L102.448608,33.0976917 L97.3903797,33.0976917 L97.3903797,27.5493174 L102.448608,27.5493174 L102.448608,18.4967596 L110.801013,18.4967596 L110.801013,27.5493174 L116.917975,27.5493174 Z" id="t"></path>
<path d="M128.857975,27.5493174 L128.857975,33.1565138 L128.975696,33.1565138 C129.367089,32.2213945 129.896203,31.3559064 130.563544,30.557033 C131.23038,29.7596679 131.99443,29.0776844 132.857215,28.5130936 C133.719241,27.9495083 134.641266,27.5113596 135.622532,27.1988991 C136.601772,26.8879468 137.622025,26.7315908 138.681013,26.7315908 C139.229873,26.7315908 139.836962,26.8296275 140.504304,27.0239413 L140.504304,34.7336477 C140.111646,34.6552183 139.641013,34.586844 139.092658,34.5290275 C138.543291,34.4704569 138.014177,34.4410459 137.504304,34.4410459 C135.974937,34.4410459 134.681013,34.6949358 133.622785,35.2004532 C132.564051,35.7067248 131.711392,36.397255 131.064051,37.2735523 C130.417215,38.1501009 129.955443,39.1714422 129.681266,40.3398385 C129.407089,41.5074807 129.269873,42.7736624 129.269873,44.1361211 L129.269873,57.7438661 L120.917722,57.7438661 L120.917722,27.5493174 L128.857975,27.5493174 Z" id="r"></path>
<path d="M144.033165,22.8767376 L144.033165,16.0435798 L152.386076,16.0435798 L152.386076,22.8767376 L144.033165,22.8767376 Z M152.386076,27.5493174 L152.386076,57.7438661 L144.033165,57.7438661 L144.033165,27.5493174 L152.386076,27.5493174 Z" id="i"></path>
<polygon id="x" points="156.738228 27.5493174 166.266582 27.5493174 171.619494 35.4337303 176.913418 27.5493174 186.147848 27.5493174 176.148861 41.6831927 187.383544 57.7441174 177.85443 57.7441174 171.501772 48.2245028 165.148861 57.7441174 155.797468 57.7441174 166.737468 41.8589046"></polygon>
<polygon id="right-bracket" points="197.580759 82.7268844 197.580759 1.93811009 191.725063 1.93811009 191.725063 0 199.828354 0 199.828354 84.6652459 191.725063 84.6652459 191.725063 82.7268844"></polygon>
</g>
</g>
</g>
</svg>
<p>An open network for secure, decentralized communication.<br>© 2023 The Matrix.org Foundation C.I.C.</p>
</footer>

View file

@ -0,0 +1,62 @@
{% extends "base.html" %}
{% block title %}Choose identity provider{% endblock %}
{% block style %}
<style type="text/css">
{{"
.providers {
list-style: none;
padding: 0;
}
.providers li {
margin: 12px;
}
.providers a {
display: block;
border-radius: 4px;
border: 1px solid #17191C;
padding: 8px;
text-align: center;
text-decoration: none;
color: #17191C;
display: flex;
align-items: center;
font-weight: bold;
}
.providers a img {
width: 24px;
height: 24px;
}
.providers a span {
flex: 1;
}
"}}
</style>
{% endblock %}
{% block body %}
<header>
<h1>Log in to {{ server_name }} </h1>
<p>Choose an identity provider to log in</p>
</header>
<main>
<ul class="providers">
{% for idp in metadata %}
<li>
<a href="redirect/{{ idp.id }}&redirectUrl={{ redirect_url|urlencode_strict }}">
{% match crate::utils::mxc_to_http_or_none(idp.icon.as_deref(), "32", "32") %}
{% when Some with (icon) %}
<img src="{{ icon }}"/>
{% when None %}
{% endmatch %}
<span>{{ idp.name }}</span>
</a>
</li>
{% endfor %}
</ul>
</main>
{% include "footer.html" %}
{% endblock %}

View file

@ -0,0 +1,20 @@
{# html fragment to be included in SSO pages, to show the user's profile #}
<div class="profile
{% if user_profile.avatar_url.is_some() %}
with-avatar
{% endif %}
">
{% match crate::utils::mxc_to_http_or_none(user_profile.avatar_url.as_deref(), "64", "64") %}
{% when Some with (avatar_url) %}
<img src="{{ avatar_url }}" class="avatar" />
{% when None %}
{% endmatch %}
{% match user_profile.displayname %}
{% when Some with (displayname) %}
<div class="display-name">{{ displayname }}</div>
{% when None %}
<div class="display-name">{{ user_id.localpart() }}</div>
{% endmatch %}
<div class="user-id">{{ user_id }}</div>
</div>

View file

@ -0,0 +1,39 @@
{% extends "base.html" %}
{% block title %}Continue to your account{% endblock %}
{% block header %}
<style type="text/css">
{{"
.confirm-trust {
margin: 34px 0;
color: #8D99A5;
}
.confirm-trust strong {
color: #17191C;
}
.confirm-trust::before {
content: \"\";
background-image: url(\"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xNi41IDlDMTYuNSAxMy4xNDIxIDEzLjE0MjEgMTYuNSA5IDE2LjVDNC44NTc4NiAxNi41IDEuNSAxMy4xNDIxIDEuNSA5QzEuNSA0Ljg1Nzg2IDQuODU3ODYgMS41IDkgMS41QzEzLjE0MjEgMS41IDE2LjUgNC44NTc4NiAxNi41IDlaTTcuMjUgOUM3LjI1IDkuNDY1OTYgNy41Njg2OSA5Ljg1NzQ4IDggOS45Njg1VjEyLjM3NUM4IDEyLjkyNzMgOC40NDc3MiAxMy4zNzUgOSAxMy4zNzVIMTAuMTI1QzEwLjY3NzMgMTMuMzc1IDExLjEyNSAxMi45MjczIDExLjEyNSAxMi4zNzVDMTEuMTI1IDExLjgyMjcgMTAuNjc3MyAxMS4zNzUgMTAuMTI1IDExLjM3NUgxMFY5QzEwIDguOTY1NDggOS45OTgyNSA4LjkzMTM3IDkuOTk0ODQgOC44OTc3NkM5Ljk0MzYzIDguMzkzNSA5LjUxNzc3IDggOSA4SDguMjVDNy42OTc3MiA4IDcuMjUgOC40NDc3MiA3LjI1IDlaTTkgNy41QzkuNjIxMzIgNy41IDEwLjEyNSA2Ljk5NjMyIDEwLjEyNSA2LjM3NUMxMC4xMjUgNS43NTM2OCA5LjYyMTMyIDUuMjUgOSA1LjI1QzguMzc4NjggNS4yNSA3Ljg3NSA1Ljc1MzY4IDcuODc1IDYuMzc1QzcuODc1IDYuOTk2MzIgOC4zNzg2OCA3LjUgOSA3LjVaIiBmaWxsPSIjQzFDNkNEIi8+Cjwvc3ZnPgoK\");
background-repeat: no-repeat;
width: 24px;
height: 24px;
display: block;
float: left;
}
"}}
</style>
{% endblock %}
{% block body %}
<header>
<h1>Continue to your account</h1>
</header>
<main>
{% include "partial_profile.html" %}
<p class="confirm-trust">Continuing will grant <strong>{{ display_url }}</strong> access to your account.</p>
<a href="{{ redirect_url }}" class="primary-button">Continue</a>
</main>
{% include "footer.html" %}
{% endblock %}

View file

@ -0,0 +1,194 @@
{% extends "base.html" %}
{% block title %}Create your account{% endblock %}
{% block header %}
<script type="text/javascript">
{{"
let wasKeyboard = false;
document.addEventListener(\"mousedown\", function() { wasKeyboard = false; });
document.addEventListener(\"keydown\", function() { wasKeyboard = true; });
document.addEventListener(\"focusin\", function() {
if (wasKeyboard) {
document.body.classList.add(\"keyboard-focus\");
} else {
document.body.classList.remove(\"keyboard-focus\");
}
});
"}}
</script>
<style type="text/css">
{{"
body.keyboard-focus :focus, body.keyboard-focus .username_input:focus-within {
outline: 3px solid #17191C;
outline-offset: 4px;
}
.username_input {
display: flex;
border: 2px solid #418DED;
border-radius: 8px;
padding: 12px;
position: relative;
margin: 16px 0;
align-items: center;
font-size: 12px;
}
.username_input.invalid {
border-color: #FE2928;
}
.username_input.invalid input, .username_input.invalid label {
color: #FE2928;
}
.username_input div, .username_input input {
line-height: 18px;
font-size: 14px;
}
.username_input label {
position: absolute;
top: -5px;
left: 14px;
font-size: 10px;
line-height: 10px;
background: white;
padding: 0 2px;
}
.username_input input {
flex: 1;
display: block;
min-width: 0;
border: none;
}
/* only clear the outline if we know it will be shown on the parent div using :focus-within */
@supports selector(:focus-within) {
.username_input input {
outline: none !important;
}
}
.username_input div {
color: #8D99A5;
}
.idp-pick-details {
border: 1px solid #E9ECF1;
border-radius: 8px;
margin: 24px 0;
}
.idp-pick-details h2 {
margin: 0;
padding: 8px 12px;
}
.idp-pick-details .idp-detail {
border-top: 1px solid #E9ECF1;
padding: 12px;
display: block;
}
.idp-pick-details .check-row {
display: flex;
align-items: center;
}
.idp-pick-details .check-row .name {
flex: 1;
}
.idp-pick-details .use, .idp-pick-details .idp-value {
color: #737D8C;
}
.idp-pick-details .idp-value {
margin: 0;
margin-top: 8px;
}
.idp-pick-details .avatar {
width: 53px;
height: 53px;
border-radius: 100%;
display: block;
margin-top: 8px;
}
output {
padding: 0 14px;
display: block;
}
output.error {
color: #FE2928;
}
"}}
</style>
{% endblock %}
{% block body %}
<header>
<h1>Create your account</h1>
<p>This is required. Continue to create your account on {{ server_name }}. You can't change this later.</p>
</header>
<main>
<form method="post" class="form__input" id="form">
<div class="username_input" id="username_input">
<label for="field-username">Username (required)</label>
<div class="prefix">@</div>
<input type="text" name="username" id="field-username" value="{{ user.localpart }}" autofocus autocorrect="off" autocapitalize="none">
<div class="postfix">:{{ server_name }}</div>
</div>
<output for="username_input" id="field-username-output"></output>
<input type="submit" value="Continue" class="primary-button">
{%- if (user.avatar_url.is_some() || user.displayname.is_some() || (user.emails.len() > 0)) -%}
<section class="idp-pick-details">
<h2>
{% match crate::utils::mxc_to_http_or_none(idp.icon.as_deref(), "32", "32") %}
{% when Some with (icon) %}
<img src="{{ icon }}"/>
{% when None %}
{% endmatch %}
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">
<span class="name">Avatar</span>
<span class="use">Use</span>
<input type="checkbox" name="use_avatar" id="idp-avatar" value="true" checked>
</div>
<img src="{{ avatar_url }}" class="avatar" />
</label>
{% endif %}
{% if let Some(displayname) = user.displayname %}
<label class="idp-detail" for="idp-displayname">
<div class="check-row">
<span class="name">Display name</span>
<span class="use">Use</span>
<input type="checkbox" name="use_displayname" id="idp-displayname" value="true" checked>
</div>
<p class="idp-value">{{ displayname }}</p>
</label>
{% endif %}
{% for email in user.emails %}
<label class="idp-detail" for="idp-email{{ loop.index }}">
<div class="check-row">
<span class="name">E-mail</span>
<span class="use">Use</span>
<input type="checkbox" name="use_email" id="idp-email{{ loop.index }}" value="{{ email }}" checked>
</div>
<p class="idp-value">{{ email }}</p>
</label>
{% endfor %}
</section>
{% endif %}
</form>
</main>
{% include "footer.html" %}
{% include "registration_js.html" %}
{% endblock %}

View file

@ -0,0 +1,120 @@
<script type="text/javascript">
{{"
const usernameField = document.getElementById(\"field-username\");
const usernameOutput = document.getElementById(\"field-username-output\");
const form = document.getElementById(\"form\");
// needed to validate on change event when no input was changed
let needsValidation = true;
let isValid = false;
function throttle(fn, wait) {
let timeout;
const throttleFn = function() {
const args = Array.from(arguments);
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(fn.bind.apply(fn, [null].concat(args)), wait);
};
throttleFn.cancelQueued = function() {
clearTimeout(timeout);
};
return throttleFn;
}
function checkUsernameAvailable(username) {
let check_uri = \"check?username=\" + encodeURIComponent(username);
return fetch(check_uri, {
// include the cookie
\"credentials\": \"same-origin\",
}).then(function(response) {
if(!response.ok) {
// for non-200 responses, raise the body of the response as an exception
return response.text().then((text) => { throw new Error(text); });
} else {
return response.json();
}
}).then(function(json) {
if(json.error) {
return {message: json.error};
} else if(json.available) {
return {available: true};
} else {
return {message: username + \" is not available, please choose another.\"};
}
});
}
const allowedUsernameCharacters = new RegExp(\"^[a-z0-9\\.\\_\\-\\/\\=]+$\");
const allowedCharactersString = \"lowercase letters, digits, ., _, -, /, =\";
function reportError(error) {
throttledCheckUsernameAvailable.cancelQueued();
usernameOutput.innerText = error;
usernameOutput.classList.add(\"error\");
usernameField.parentElement.classList.add(\"invalid\");
usernameField.focus();
}
function validateUsername(username) {
isValid = false;
needsValidation = false;
usernameOutput.innerText = \"\";
usernameField.parentElement.classList.remove(\"invalid\");
usernameOutput.classList.remove(\"error\");
if (!username) {
return reportError(\"This is required. Please provide a username\");
}
if (username.length > 255) {
return reportError(\"Too long, please choose something shorter\");
}
if (!allowedUsernameCharacters.test(username)) {
return reportError(\"Invalid username, please only use \" + allowedCharactersString);
}
usernameOutput.innerText = \"Checking if username is available …\";
throttledCheckUsernameAvailable(username);
}
const throttledCheckUsernameAvailable = throttle(function(username) {
const handleError = function(err) {
// don\"t prevent form submission on error
usernameOutput.innerText = \"\";
isValid = true;
};
try {
checkUsernameAvailable(username).then(function(result) {
if (!result.available) {
reportError(result.message);
} else {
isValid = true;
usernameOutput.innerText = \"\";
}
}, handleError);
} catch (err) {
handleError(err);
}
}, 500);
form.addEventListener(\"submit\", function(evt) {
if (needsValidation) {
validateUsername(usernameField.value);
evt.preventDefault();
return;
}
if (!isValid) {
evt.preventDefault();
usernameField.focus();
return;
}
});
usernameField.addEventListener(\"input\", function(evt) {
validateUsername(usernameField.value);
});
usernameField.addEventListener(\"change\", function(evt) {
if (needsValidation) {
validateUsername(usernameField.value);
}
});
"}}
</script>

View file

@ -0,0 +1,19 @@
<footer>
<svg role="img" aria-label="[Matrix logo]" viewBox="0 0 200 85" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="parent" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="child" transform="translate(-122.000000, -6.000000)" fill="#000000" fill-rule="nonzero">
<g id="matrix-logo" transform="translate(122.000000, 6.000000)">
<polygon id="left-bracket" points="2.24708861 1.93811009 2.24708861 82.7268844 8.10278481 82.7268844 8.10278481 84.6652459 0 84.6652459 0 0 8.10278481 0 8.10278481 1.93811009"></polygon>
<path d="M24.8073418,27.5493174 L24.8073418,31.6376991 L24.924557,31.6376991 C26.0227848,30.0814294 27.3455696,28.8730642 28.8951899,28.0163743 C30.4437975,27.1611927 32.2189873,26.7318422 34.218481,26.7318422 C36.1394937,26.7318422 37.8946835,27.102622 39.4825316,27.8416679 C41.0708861,28.5819706 42.276962,29.8856073 43.1005063,31.7548404 C44.0017722,30.431345 45.2270886,29.2629486 46.7767089,28.2506569 C48.3253165,27.2388679 50.158481,26.7318422 52.2764557,26.7318422 C53.8843038,26.7318422 55.3736709,26.9269101 56.7473418,27.3162917 C58.1189873,27.7056734 59.295443,28.3285835 60.2759494,29.185022 C61.255443,30.0422147 62.02,31.1615927 62.5701266,32.5426532 C63.1187342,33.9262275 63.3936709,35.5898349 63.3936709,37.5372459 L63.3936709,57.7443688 L55.0410127,57.7441174 L55.0410127,40.6319376 C55.0410127,39.6201486 55.0020253,38.6661761 54.9232911,37.7700202 C54.8440506,36.8751211 54.6293671,36.0968606 54.2764557,35.4339817 C53.9232911,34.772611 53.403038,34.2464807 52.7177215,33.8568477 C52.0313924,33.4689743 51.0997468,33.2731523 49.9235443,33.2731523 C48.7473418,33.2731523 47.7962025,33.4983853 47.0706329,33.944578 C46.344557,34.393033 45.7764557,34.9774826 45.3650633,35.6969211 C44.9534177,36.4181193 44.6787342,37.2353431 44.5417722,38.150855 C44.4037975,39.0653615 44.3356962,39.9904257 44.3356962,40.9247908 L44.3356962,57.7443688 L35.9835443,57.7443688 L35.9835443,40.8079009 C35.9835443,39.9124991 35.963038,39.0263982 35.9253165,38.150855 C35.8853165,37.2743064 35.7192405,36.4666349 35.424557,35.7263321 C35.1303797,34.9872862 34.64,34.393033 33.9539241,33.944578 C33.2675949,33.4983853 32.2579747,33.2731523 30.9248101,33.2731523 C30.5321519,33.2731523 30.0126582,33.3608826 29.3663291,33.5365945 C28.7192405,33.7118037 28.0913924,34.0433688 27.4840506,34.5292789 C26.875443,35.0164459 26.3564557,35.7172826 25.9250633,36.6315376 C25.4934177,37.5470495 25.2779747,38.7436 25.2779747,40.2229486 L25.2779747,57.7441174 L16.9260759,57.7443688 L16.9260759,27.5493174 L24.8073418,27.5493174 Z" id="m"></path>
<path d="M68.7455696,31.9886202 C69.6075949,30.7033339 70.7060759,29.672189 72.0397468,28.8926716 C73.3724051,28.1141596 74.8716456,27.5596239 76.5387342,27.2283101 C78.2050633,26.8977505 79.8817722,26.7315908 81.5678481,26.7315908 C83.0974684,26.7315908 84.6458228,26.8391798 86.2144304,27.0525982 C87.7827848,27.2675248 89.2144304,27.6865688 90.5086076,28.3087248 C91.8025316,28.9313835 92.8610127,29.7983798 93.6848101,30.9074514 C94.5083544,32.0170257 94.92,33.4870734 94.92,35.3173431 L94.92,51.026844 C94.92,52.3913138 94.998481,53.6941963 95.1556962,54.9400165 C95.3113924,56.1865908 95.5863291,57.120956 95.9787342,57.7436147 L87.5091139,57.7436147 C87.3518987,57.276055 87.2240506,56.7996972 87.1265823,56.3125303 C87.0278481,55.8266202 86.9592405,55.3301523 86.9207595,54.8236294 C85.5873418,56.1865908 84.0182278,57.1405633 82.2156962,57.6857982 C80.4113924,58.2295248 78.5683544,58.503022 76.6860759,58.503022 C75.2346835,58.503022 73.8817722,58.3275615 72.6270886,57.9776459 C71.3718987,57.6269761 70.2744304,57.082244 69.3334177,56.3411872 C68.3921519,55.602644 67.656962,54.6680275 67.1275949,53.5390972 C66.5982278,52.410167 66.3331646,51.065556 66.3331646,49.5087835 C66.3331646,47.7961578 66.6367089,46.384178 67.2455696,45.2756092 C67.8529114,44.1652807 68.6367089,43.2799339 69.5987342,42.6173064 C70.5589873,41.9556844 71.6567089,41.4592165 72.8924051,41.1284055 C74.1273418,40.7978459 75.3721519,40.5356606 76.6270886,40.3398385 C77.8820253,40.1457761 79.116962,39.9896716 80.3329114,39.873033 C81.5483544,39.7558917 82.6270886,39.5804312 83.5681013,39.3469028 C84.5093671,39.1133743 85.2536709,38.7732624 85.8032911,38.3250587 C86.3513924,37.8773578 86.6063291,37.2252881 86.5678481,36.3680954 C86.5678481,35.4731963 86.4210127,34.7620532 86.1268354,34.2366771 C85.8329114,33.7113009 85.4405063,33.3018092 84.9506329,33.0099615 C84.4602532,32.7181138 83.8916456,32.5232972 83.2450633,32.4255119 C82.5977215,32.3294862 81.9010127,32.2797138 81.156962,32.2797138 C79.5098734,32.2797138 78.2159494,32.6303835 77.2746835,33.3312202 C76.3339241,34.0320569 75.7837975,35.2007046 75.6275949,36.8354037 L67.275443,36.8354037 C67.3924051,34.8892495 67.8817722,33.2726495 68.7455696,31.9886202 Z M85.2440506,43.6984752 C84.7149367,43.873433 84.1460759,44.0189798 83.5387342,44.1361211 C82.9306329,44.253011 82.2936709,44.350545 81.6270886,44.4279688 C80.96,44.5066495 80.2934177,44.6034294 79.6273418,44.7203193 C78.9994937,44.8362037 78.3820253,44.9933138 77.7749367,45.1871248 C77.1663291,45.3829468 76.636962,45.6451321 76.1865823,45.9759431 C75.7349367,46.3070055 75.3724051,46.7263009 75.0979747,47.2313156 C74.8232911,47.7375872 74.6863291,48.380356 74.6863291,49.1588679 C74.6863291,49.8979138 74.8232911,50.5218294 75.0979747,51.026844 C75.3724051,51.5338697 75.7455696,51.9328037 76.2159494,52.2246514 C76.6863291,52.5164991 77.2349367,52.7213706 77.8632911,52.8375064 C78.4898734,52.9546477 79.136962,53.012967 79.8037975,53.012967 C81.4506329,53.012967 82.724557,52.740978 83.6273418,52.1952404 C84.5288608,51.6507596 85.1949367,50.9981872 85.6270886,50.2382771 C86.0579747,49.4793725 86.323038,48.7119211 86.4212658,47.9321523 C86.518481,47.1536404 86.5681013,46.5304789 86.5681013,46.063422 L86.5681013,42.9677248 C86.2146835,43.2799339 85.7736709,43.5230147 85.2440506,43.6984752 Z" id="a"></path>
<path d="M116.917975,27.5493174 L116.917975,33.0976917 L110.801266,33.0976917 L110.801266,48.0492936 C110.801266,49.4502128 111.036203,50.3850807 111.507089,50.8518862 C111.976962,51.3191945 112.918734,51.5527229 114.33038,51.5527229 C114.801013,51.5527229 115.251392,51.5336183 115.683038,51.4944037 C116.114177,51.4561945 116.526076,51.3968697 116.917975,51.3194459 L116.917975,57.7438661 C116.212152,57.860756 115.427595,57.9381798 114.565316,57.9778972 C113.702785,58.0153523 112.859747,58.0357138 112.036203,58.0357138 C110.742278,58.0357138 109.516456,57.9477321 108.36,57.7722716 C107.202785,57.5975651 106.183544,57.2577046 105.301519,56.7509303 C104.418987,56.2454128 103.722785,55.5242147 103.213418,54.5898495 C102.703038,53.6562385 102.448608,52.4292716 102.448608,50.9099541 L102.448608,33.0976917 L97.3903797,33.0976917 L97.3903797,27.5493174 L102.448608,27.5493174 L102.448608,18.4967596 L110.801013,18.4967596 L110.801013,27.5493174 L116.917975,27.5493174 Z" id="t"></path>
<path d="M128.857975,27.5493174 L128.857975,33.1565138 L128.975696,33.1565138 C129.367089,32.2213945 129.896203,31.3559064 130.563544,30.557033 C131.23038,29.7596679 131.99443,29.0776844 132.857215,28.5130936 C133.719241,27.9495083 134.641266,27.5113596 135.622532,27.1988991 C136.601772,26.8879468 137.622025,26.7315908 138.681013,26.7315908 C139.229873,26.7315908 139.836962,26.8296275 140.504304,27.0239413 L140.504304,34.7336477 C140.111646,34.6552183 139.641013,34.586844 139.092658,34.5290275 C138.543291,34.4704569 138.014177,34.4410459 137.504304,34.4410459 C135.974937,34.4410459 134.681013,34.6949358 133.622785,35.2004532 C132.564051,35.7067248 131.711392,36.397255 131.064051,37.2735523 C130.417215,38.1501009 129.955443,39.1714422 129.681266,40.3398385 C129.407089,41.5074807 129.269873,42.7736624 129.269873,44.1361211 L129.269873,57.7438661 L120.917722,57.7438661 L120.917722,27.5493174 L128.857975,27.5493174 Z" id="r"></path>
<path d="M144.033165,22.8767376 L144.033165,16.0435798 L152.386076,16.0435798 L152.386076,22.8767376 L144.033165,22.8767376 Z M152.386076,27.5493174 L152.386076,57.7438661 L144.033165,57.7438661 L144.033165,27.5493174 L152.386076,27.5493174 Z" id="i"></path>
<polygon id="x" points="156.738228 27.5493174 166.266582 27.5493174 171.619494 35.4337303 176.913418 27.5493174 186.147848 27.5493174 176.148861 41.6831927 187.383544 57.7441174 177.85443 57.7441174 171.501772 48.2245028 165.148861 57.7441174 155.797468 57.7441174 166.737468 41.8589046"></polygon>
<polygon id="right-bracket" points="197.580759 82.7268844 197.580759 1.93811009 191.725063 1.93811009 191.725063 0 199.828354 0 199.828354 84.6652459 191.725063 84.6652459 191.725063 82.7268844"></polygon>
</g>
</g>
</g>
</svg>
<p>An open network for secure, decentralized communication.<br>© 2023 The Matrix.org Foundation C.I.C.</p>
</footer>

View file

@ -0,0 +1,58 @@
{% extends "base.html" %}
{% block title %}Choose identity provider{% endblock %}
{% block style %}
.providers {
list-style: none;
padding: 0;
}
.providers li {
margin: 12px;
}
.providers a {
display: block;
border-radius: 4px;
border: 1px solid #17191C;
padding: 8px;
text-align: center;
text-decoration: none;
color: #17191C;
display: flex;
align-items: center;
font-weight: bold;
}
.providers a img {
width: 24px;
height: 24px;
}
.providers a span {
flex: 1;
}
{% endblock %}
{% block body %}
<header>
<h1>Log in to {{ server_name }} </h1>
<p>Choose an identity provider to log in</p>
</header>
<main>
<ul class="providers">
{% for idp in metadata %}
<li>
<a href="pick_idp?idp={{ idp.id }}&redirectUrl={{ redirect_url|urlencode_strict }}">
{% match crate::utils::mxc_to_http_or_none(idp.icon.as_deref(), "32", "32") %}
{% when Some with (mxc) %}
<img src="{{ mxc }}"/>
{% when None %}
{% endmatch %}
<span>{{ idp.name.as_deref().unwrap_or(idp.id) }}</span>
</a>
</li>
{% endfor %}
</ul>
</main>
{% include "sso_footer.html" %}
{% endblock %}

View file

@ -0,0 +1,27 @@
{% extends "_base.html" %}
{% block title %}Authentication{% endblock %}
{% block header %}
<style type="text/css">
#registrationForm input {
display: block;
margin: auto;
}
</style>
{% endblock %}
{% block body %}
<form id="registrationForm" method="post" action="{{ myurl }}">
<div>
{% if error is defined %}
<p class="error"><strong>Error: {{ error }}</strong></p>
{% endif %}
<p>
Please click the button below if you agree to the
<a href="{{ terms_url }}">privacy policy of this homeserver.</a>
</p>
<input type="hidden" name="session" value="{{ session }}" />
<input type="submit" value="Agree" />
</div>
</form>
{% endblock %}

View file

@ -21,6 +21,7 @@ mod report;
mod room;
mod search;
mod session;
mod sso;
mod space;
mod state;
mod sync;
@ -56,6 +57,7 @@ pub use report::*;
pub use room::*;
pub use search::*;
pub use session::*;
pub use sso::*;
pub use space::*;
pub use state::*;
pub use sync::*;

View file

@ -24,9 +24,19 @@ struct Claims {
pub async fn get_login_types_route(
_body: Ruma<get_login_types::v3::Request>,
) -> Result<get_login_types::v3::Response> {
let identity_providers = services()
.sso
.inner
.iter()
.map(|p| p.inner.clone())
.collect();
Ok(get_login_types::v3::Response::new(vec![
get_login_types::v3::LoginType::Password(Default::default()),
get_login_types::v3::LoginType::ApplicationService(Default::default()),
get_login_types::v3::LoginType::Sso(get_login_types::v3::SsoLoginType {
identity_providers,
}),
]))
}

View file

@ -0,0 +1,156 @@
use crate::{
service::sso::{macaroon::Macaroon, templates, COOKIE_STATE_EXPIRATION_SECS},
services, Error, Ruma,
};
use askama::Template;
use axum::{body::Full, response::IntoResponse};
use axum_extra::extract::cookie::{Cookie, SameSite};
use bytes::BytesMut;
use http::{header::SET_COOKIE, HeaderValue, StatusCode};
use openidconnect::{AuthorizationCode, CsrfToken, RedirectUrl};
use ruma::api::{
client::{error::ErrorKind, session},
OutgoingResponse,
};
use serde::Deserialize;
/// # `GET /_matrix/client/v3/login/sso/redirect`
///
/// Redirect user to SSO interface. The path argument is optional.
pub async fn get_sso_redirect(
body: Ruma<session::sso_login::v3::Request>,
) -> axum::response::Response {
if services().sso.get_all().is_empty() {
return Error::BadRequest(ErrorKind::NotFound, "SSO has not been configured")
.into_response();
}
return get_sso_fallback_template(body.redirect_url.as_deref().unwrap_or_default())
.into_response();
}
/// # `GET /_matrix/client/v3/login/sso/redirect/{idpId}`
///
/// Redirect user to SSO interface.
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(redirect_url.as_str())
.into_response();
};
let provider = match services()
.sso
.find_one(&body.idp_id)
{
Ok(provider) => provider,
Err(e) => return e.into_response(),
};
let (url, _, cookie) = provider.handle_redirect(redirect_url).await;
let mut res = session::sso_login_with_provider::v3::Response {
location: Some(url.to_string()),
cookie: Some(cookie.to_string()),
}
.try_into_http_response::<BytesMut>()
.unwrap();
*res.status_mut() = StatusCode::FOUND;
res.map(BytesMut::freeze).map(Full::new).into_response()
}
fn get_sso_fallback_template(redirect_url: &str) -> axum::response::Response {
let server_name = services().globals.server_name().to_string();
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,
metadata,
redirect_url,
};
t.render()
.map(|body| {
((
[(
http::header::CONTENT_TYPE,
http::HeaderValue::from_static(templates::IdpPicker::MIME_TYPE),
)],
body,
))
.into_response()
})
.expect("woops")
}
#[derive(Deserialize)]
pub struct Callback {
pub code: AuthorizationCode,
pub state: CsrfToken,
pub verifier: String,
}
pub struct Session {}
/// # `GET /_conduit/client/oidc/callback`
///
/// Verify the response received from the identity provider.
/// If everything is fine redirect
pub async fn get_sso_callback(
cookie: axum::extract::TypedHeader<axum::headers::Cookie>,
axum::extract::Query(callback): axum::extract::Query<Callback>,
) -> axum::response::Response {
let clear_cookie = Cookie::build("openid-state", "")
.path("/_conduit/client/sso")
.finish()
.to_string();
let Callback {
code,
state,
verifier,
} = callback;
let Some(cookie) = cookie.get("openid-state") else {
return Error::BadRequest(
ErrorKind::MissingToken,
"Could not retrieve SSO macaroon from cookie",
)
.into_response();
};
let macaroon = match Macaroon::verify(cookie, state.secret()) {
Ok(macaroon) => macaroon,
Err(error) => return error.into_response(),
};
let provider = match services().sso.find_one(&macaroon.idp_id) {
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;
(
axum::response::AppendHeaders([(SET_COOKIE, cookie)]),
"Hello, World!",
)
.into_response()
}

View file

@ -9,9 +9,12 @@ use serde::{de::IgnoredAny, Deserialize};
use tracing::warn;
mod proxy;
mod sso;
use self::proxy::ProxyConfig;
pub use sso::*;
#[derive(Clone, Debug, Deserialize)]
pub struct Config {
#[serde(default = "default_address")]
@ -79,6 +82,10 @@ pub struct Config {
pub turn_secret: String,
#[serde(default = "default_turn_ttl")]
pub turn_ttl: u64,
#[serde(default)]
pub macaroon_key: Option<String>,
#[serde(default)]
pub sso: Vec<ProviderConfig>,
pub emergency_password: Option<String>,

106
src/config/sso.rs Normal file
View file

@ -0,0 +1,106 @@
use ruma::OwnedMxcUri;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize)]
pub struct ProviderConfig {
pub id: String,
pub name: Option<String>,
pub icon: Option<OwnedMxcUri>,
// Information retrieved while creating the OpenID Application
pub client: ClientConfig,
// #[serde(deserialize_with = "crate::utils::deserialize_from_str")]
pub issuer: url::Url,
// Always contains "openid" by default
// "profile", "email" and "name" are useful to suggest an MXID
pub scopes: Vec<String>,
// PKCE provides dynamic client secrets
// Should be enabled when `ClientAuthMethod` is `None`
pub pkce: Option<bool>,
// Should be enabled when the authorization response does not contain a unique subject claim
pub subject_claim: Option<String>,
// Allow existent accounts to login with OIDC
#[serde(default)]
pub allow_existing_users: bool,
// Invalidate user sessions when the OP session expires
#[serde(default)]
pub backchannel_logout: bool,
// Should be enabled when the authorization response does not contain userinfo
#[serde(default)]
pub force_userinfo: bool,
#[serde(default)]
pub discovery: DiscoveryConfig,
}
#[derive(Clone, Debug, Deserialize)]
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)]
pub struct Endpoints {
pub auth: url::Url,
pub token: Option<url::Url>,
pub userinfo: Option<url::Url>,
pub jwk: Option<url::Url>,
pub ok: AuthMethod,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DiscoveryConfig {
// Should be used for OPs supporting the OIDC Discovery endpoint
#[default]
Automatic,
Manual(Endpoints),
}
#[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 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)]
// pub struct PrivateJwt {
// pub payload:
// pub header: jsonwebtoken::Header,
// pub key: jsonwebtoken::EncodingKey,
// }

View file

@ -401,7 +401,7 @@ impl KeyValueDatabase {
let db = Box::leak(db_raw);
let services_raw = Box::new(Services::build(db, config)?);
let services_raw = Box::new(Services::build(db, config).await?);
// This is the first and only time we initialize the SERVICE static
*SERVICES.write().unwrap() = Some(Box::leak(services_raw));

View file

@ -24,7 +24,7 @@ use ruma::api::{
IncomingRequest,
};
use tokio::signal;
use tower::ServiceBuilder;
use tower::{ServiceBuilder};
use tower_http::{
cors::{self, CorsLayer},
trace::TraceLayer,
@ -148,6 +148,7 @@ async fn run_server() -> io::Result<()> {
let x_requested_with = HeaderName::from_static("x-requested-with");
let middlewares = ServiceBuilder::new()
// TODO token
.sensitive_headers([header::AUTHORIZATION])
.layer(axum::middleware::from_fn(spawn_task))
.layer(
@ -178,6 +179,7 @@ async fn run_server() -> io::Result<()> {
header::CONTENT_TYPE,
header::ACCEPT,
header::AUTHORIZATION,
header::LOCATION,
])
.max_age(Duration::from_secs(86400)),
)
@ -399,6 +401,23 @@ fn routes() -> Router {
"/_matrix/key/v2/server/:key_id",
get(server_server::get_server_keys_deprecated_route),
)
.route(
"/_matrix/client/v3/login/sso/redirect",
get(client_server::get_sso_redirect),
)
.route(
"/_matrix/client/v3/login/sso/redirect/",
get(client_server::get_sso_redirect),
)
.route(
"/_matrix/client/v3/login/sso/redirect/:idp_id",
get(client_server::get_sso_redirect_with_provider),
)
.route(
"/_conduit/v3/login/sso/callback",
get(client_server::get_sso_callback),
)
// .route("/sso_return", get(client_server::get_sso_return))
.ruma_route(server_server::get_public_rooms_route)
.ruma_route(server_server::get_public_rooms_filtered_route)
.ruma_route(server_server::send_transaction_message_route)

View file

@ -8,7 +8,7 @@ use ruma::{
use crate::api::server_server::FedDest;
use crate::{services, Config, Error, Result};
use futures_util::FutureExt;
use futures_util::{future, FutureExt, TryFutureExt};
use hyper::{
client::connect::dns::{GaiResolver, Name},
service::Service as HyperService,
@ -25,7 +25,7 @@ use std::{
collections::{BTreeMap, HashMap},
error::Error as StdError,
fs,
future::{self, Future},
future::Future,
iter,
net::{IpAddr, SocketAddr},
path::PathBuf,
@ -75,6 +75,7 @@ pub struct Service {
pub rotate: RotationHandler,
pub shutdown: AtomicBool,
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.
@ -147,7 +148,7 @@ impl Resolve for Resolver {
}
impl Service {
pub fn load(db: &'static dyn Data, config: Config) -> Result<Self> {
pub async fn load(db: &'static dyn Data, config: Config) -> Result<Self> {
let keypair = db.load_keypair();
let keypair = match keypair {
@ -159,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
@ -212,6 +215,7 @@ impl Service {
sync_receivers: RwLock::new(HashMap::new()),
rotate: RotationHandler::new(),
shutdown: AtomicBool::new(false),
macaroon_key,
};
fs::create_dir_all(s.get_media_folder())?;

View file

@ -13,6 +13,7 @@ pub mod appservice;
pub mod globals;
pub mod key_backups;
pub mod media;
pub mod sso;
pub mod pdu;
pub mod pusher;
pub mod rooms;
@ -34,10 +35,11 @@ pub struct Services {
pub key_backups: key_backups::Service,
pub media: media::Service,
pub sending: Arc<sending::Service>,
pub sso: Arc<sso::Service>,
}
impl Services {
pub fn build<
pub async fn build<
D: appservice::Data
+ pusher::Data
+ rooms::Data
@ -114,8 +116,8 @@ impl Services {
key_backups: key_backups::Service { db },
media: media::Service { db },
sending: sending::Service::build(db, &config),
globals: globals::Service::load(db, config)?,
sso: sso::Service::build(&config).await,
globals: globals::Service::load(db, config).await?,
})
}
fn memory_usage(&self) -> String {

View file

@ -0,0 +1,54 @@
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation};
use openidconnect::{CsrfToken, Nonce, RedirectUrl};
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use crate::Error;
#[derive(Serialize, Deserialize)]
pub struct Macaroon {
pub idp_id: String,
pub nonce: Nonce,
pub csrf: CsrfToken,
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(),
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 encoding",
))
}
}

197
src/service/sso/mod.rs Normal file
View file

@ -0,0 +1,197 @@
use std::{collections::HashMap, sync::Arc};
use axum_extra::extract::cookie::{Cookie, SameSite};
use futures_util::future::{self};
use openidconnect::{
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 crate::{
config::{ClientConfig, DiscoveryConfig as Discovery, ProviderConfig},
services, Config, Error,
};
use self::macaroon::Macaroon;
pub const COOKIE_STATE_EXPIRATION_SECS: i64 = 60 * 60;
pub mod macaroon;
pub mod templates;
pub struct Service {
pub inner: Vec<Provider>,
}
impl Service {
pub async fn build(config: &Config) -> Arc<Self> {
// 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> {
match self.inner.iter().find(|p| p.inner.id == idp_id.as_ref()) {
Some(provider) => Ok(provider.to_owned()),
None => Err(Error::BadRequest(
ErrorKind::NotFound,
"unknown identity provider",
)),
}
}
pub fn get_all(&self) -> &[Provider] {
self.inner.as_slice()
}
}
#[derive(Clone)]
pub struct Provider {
pub inner: IdentityProvider,
pub client: Arc<CoreClient>,
pub scopes: Vec<String>,
pub subject_claim: Option<String>,
}
impl Provider {
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,
},
scopes: config.scopes,
subject_claim: config.subject_claim,
}
}
fn create_client(
discovery: Discovery,
issuer: url::Url,
config: ClientConfig,
) -> Result<Arc<CoreClient>, Error> {
let base_url = services()
.globals
.well_known_client()
.as_deref()
.unwrap_or(services().globals.server_name().as_str());
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 discovery = CoreProviderMetadata::discover(
&IssuerUrl::from_url(issuer),
http_client,
)
.unwrap();
CoreClient::from_provider_metadata(
discovery,
ClientId::new(config.id),
config.secret.map(ClientSecret::new),
)
}
Discovery::Manual(endpoints) => CoreClient::new(
ClientId::new(config.id),
config.secret.map(ClientSecret::new),
IssuerUrl::from_url(issuer),
AuthUrl::from_url(endpoints.auth),
endpoints.token.map(TokenUrl::from_url),
endpoints.userinfo.map(UserInfoUrl::from_url),
Default::default(),
)
.set_redirect_uri(redirect_url),
};
Ok(Arc::new(config))
}
pub async fn handle_redirect(&self, redirect_url: RedirectUrl) -> (url::Url, Nonce, Cookie) {
let req = self
.client
.authorize_url(
CoreAuthenticationFlow::AuthorizationCode,
|| CsrfToken::new_random_len(48),
|| Nonce::new_random_len(48),
)
.add_scopes(self.scopes.iter().map(ToOwned::to_owned).map(Scope::new));
let (url, csrf, nonce) = req.url();
let mac = Macaroon::new(
self.inner.id.clone(),
nonce.clone(),
csrf,
redirect_url.clone(),
);
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(
&self,
code: AuthorizationCode,
nonce: Nonce,
) -> Result<(), Error> {
let resp = self
.client
.exchange_code(code)
.request_async(async_http_client)
.await
.unwrap();
let id_token = resp.id_token().unwrap();
let claims = id_token
.claims(&self.client.id_token_verifier(), &nonce)
.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);
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(),
// self.subject_claim.clone().map(SubjectIdentifier::new),
// ).map(|req| req.request_async(async_http_client)) {
// Err(e) => Ok(claims),
// Ok(req) => req.await,
// }
}
}

View file

@ -0,0 +1,59 @@
use askama::Template;
use ruma::{
api::client::{search::search_events::v3::UserProfile, session::get_login_types::v3::IdentityProvider}, OwnedMxcUri, OwnedServerName, OwnedUserId,
};
use super::Provider;
#[derive(Template)]
#[template(path = "auth_confirmation.html", escape = "none")]
pub struct AuthConfirmation {
description: String,
redirect_url: url::Url,
idp_name: String,
}
#[derive(Template)]
#[template(path = "auth_failure.html", escape = "none")]
pub struct AuthFailure {
server_name: OwnedServerName,
}
#[derive(Template)]
#[template(path = "auth_success.html", escape = "none")]
pub struct AuthSuccess {}
#[derive(Template)]
#[template(path = "deactivated.html", escape = "none")]
pub struct Deactivated {}
#[derive(Template)]
#[template(path = "idp_picker.html", escape = "none")]
pub struct IdpPicker {
pub server_name: String,
pub metadata: Vec<IdentityProvider>,
pub redirect_url: String,
}
#[derive(Template)]
#[template(path = "registration.html", escape = "none")]
pub struct Registration {
pub server_name: OwnedServerName,
pub idp: IdentityProvider,
pub user: Attributes,
}
pub struct Attributes {
pub localpart: String,
pub displayname: Option<String>,
pub avatar_url: Option<String>,
pub emails: Vec<String>,
}
#[derive(Template)]
#[template(path = "redirect_confirm.html", escape = "none")]
pub struct RedirectConfirm {
pub user_id: OwnedUserId,
pub user_profile: UserProfile,
pub display_url: url::Url,
pub redirect_url: url::Url,
}

View file

@ -4,13 +4,17 @@ use argon2::{Config, Variant};
use cmp::Ordering;
use rand::prelude::*;
use ring::digest;
use ruma::{canonical_json::try_from_json_map, CanonicalJsonError, CanonicalJsonObject};
use ruma::{
canonical_json::try_from_json_map, CanonicalJsonError, CanonicalJsonObject, MxcUri, MxcUriError, OwnedMxcUri,
};
use std::{
cmp, fmt,
str::FromStr,
time::{SystemTime, UNIX_EPOCH},
};
use crate::services;
pub fn millis_since_unix_epoch() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
@ -180,3 +184,26 @@ impl<'a> fmt::Display for HtmlEscape<'a> {
Ok(())
}
}
pub fn mxc_to_http_or_none(mxc_uri: Option<&MxcUri>, width: &str, height: &str) -> Option<String> {
let Some(mxc_uri) = mxc_uri else {
return None;
};
let base_url = services()
.globals
.well_known_client()
.as_deref()
.unwrap_or(services().globals.server_name().as_str());
let (server_name, media_id) = mxc_uri.parts().ok()?;
let mut host =
format!("https://{base_url}/_matrix/media/v3/thumbnail/{server_name}/{media_id}")
.parse::<url::Url>()
.expect("server_name should be a valid domain");
host.query_pairs_mut()
.append_pair("width", width)
.append_pair("height", height)
.append_pair("method", "scale");
Some(host.to_string())
}