Add Playwright tests for OIDC-aware & OIDC-native (#12252)

* Resolve race condition between opening settings & well-known check in OIDC mode

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add OIDC-aware and OIDC-native tests using MAS

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2024-02-21 10:43:47 +00:00 committed by GitHub
parent 6e73d6579e
commit 36a8d503df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 798 additions and 9 deletions

View file

@ -85,7 +85,7 @@ to be left with some stray containers if, for example, you terminate a test such
that the `after()` does not run and also exit Playwright uncleanly. All the containers
it starts are prefixed, so they are easy to recognise. They can be removed safely.
After each test run, logs from the Synapse instances are saved in `playwright/synapselogs`
After each test run, logs from the Synapse instances are saved in `playwright/logs/synapse`
with each instance in a separate directory named after its ID. These logs are removed
at the start of each test run.

View file

@ -1,6 +1,6 @@
/test-results/
/html-report/
/synapselogs/
/logs/
# Only commit snapshots from Linux
/snapshots/**/*.png
!/snapshots/**/*-linux.png

View file

@ -0,0 +1,104 @@
/*
Copyright 2023 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.
*/
import { API, Messages } from "mailhog";
import { Page } from "@playwright/test";
import { test as base, expect } from "../../element-web-test";
import { MatrixAuthenticationService } from "../../plugins/matrix-authentication-service";
import { StartHomeserverOpts } from "../../plugins/homeserver";
export const test = base.extend<{
masPrepare: MatrixAuthenticationService;
mas: MatrixAuthenticationService;
}>({
// There's a bit of a chicken and egg problem between MAS & Synapse where they each need to know how to reach each other
// so spinning up a MAS is split into the prepare & start stage: prepare mas -> homeserver -> start mas to disentangle this.
masPrepare: async ({ context }, use) => {
const mas = new MatrixAuthenticationService(context);
await mas.prepare();
await use(mas);
},
mas: [
async ({ masPrepare: mas, homeserver, mailhog }, use, testInfo) => {
await mas.start(homeserver, mailhog.instance);
await use(mas);
await mas.stop(testInfo);
},
{ auto: true },
],
startHomeserverOpts: async ({ masPrepare }, use) => {
await use({
template: "mas-oidc",
variables: {
MAS_PORT: masPrepare.port,
},
});
},
config: async ({ homeserver, startHomeserverOpts, context }, use) => {
const issuer = `http://localhost:${(startHomeserverOpts as StartHomeserverOpts).variables["MAS_PORT"]}/`;
const wellKnown = {
"m.homeserver": {
base_url: homeserver.config.baseUrl,
},
"org.matrix.msc2965.authentication": {
issuer,
account: `${issuer}account`,
},
};
// Ensure org.matrix.msc2965.authentication is in well-known
await context.route("https://localhost/.well-known/matrix/client", async (route) => {
await route.fulfill({ json: wellKnown });
});
await use({
default_server_config: wellKnown,
});
},
});
export { expect };
export async function registerAccountMas(
page: Page,
mailhog: API,
username: string,
email: string,
password: string,
): Promise<void> {
await expect(page.getByText("Please sign in to continue:")).toBeVisible();
await page.getByRole("link", { name: "Create Account" }).click();
await page.getByRole("textbox", { name: "Username" }).fill(username);
await page.getByRole("textbox", { name: "Email address" }).fill(email);
await page.getByRole("textbox", { name: "Password", exact: true }).fill(password);
await page.getByRole("textbox", { name: "Confirm Password" }).fill(password);
await page.getByRole("button", { name: "Continue" }).click();
let messages: Messages;
await expect(async () => {
messages = await mailhog.messages();
expect(messages.items).toHaveLength(1);
}).toPass();
expect(messages.items[0].to).toEqual(`${username} <${email}>`);
const [code] = messages.items[0].text.match(/(\d{6})/);
await page.getByRole("textbox", { name: "6-digit code" }).fill(code);
await page.getByRole("button", { name: "Continue" }).click();
await expect(page.getByText("Allow access to your account?")).toBeVisible();
await page.getByRole("button", { name: "Continue" }).click();
}

View file

@ -0,0 +1,42 @@
/*
Copyright 2023 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.
*/
import { test, expect, registerAccountMas } from ".";
import { isDendrite } from "../../plugins/homeserver/dendrite";
test.describe("OIDC Aware", () => {
test.skip(isDendrite, "does not yet support MAS");
test.slow(); // trace recording takes a while here
test("can register an account and manage it", async ({ context, page, homeserver, mailhog, app }) => {
await page.goto("/#/login");
await page.getByRole("button", { name: "Continue" }).click();
await registerAccountMas(page, mailhog.api, "alice", "alice@email.com", "Pa$sW0rD!");
// Eventually, we should end up at the home screen.
await expect(page).toHaveURL(/\/#\/home$/, { timeout: 10000 });
await expect(page.getByRole("heading", { name: "Welcome alice", exact: true })).toBeVisible();
// Open settings and navigate to account management
await app.settings.openUserSettings("General");
const newPagePromise = context.waitForEvent("page");
await page.getByRole("button", { name: "Manage account" }).click();
// Assert new tab opened
const newPage = await newPagePromise;
await expect(newPage.getByText("Primary email")).toBeVisible();
});
});

View file

@ -0,0 +1,74 @@
/*
Copyright 2023 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.
*/
import { test, expect, registerAccountMas } from ".";
import { isDendrite } from "../../plugins/homeserver/dendrite";
test.describe("OIDC Native", () => {
test.skip(isDendrite, "does not yet support MAS");
test.slow(); // trace recording takes a while here
test.use({
labsFlags: ["feature_oidc_native_flow"],
});
test("can register the oauth2 client and an account", async ({ context, page, homeserver, mailhog, app, mas }) => {
const tokenUri = `http://localhost:${mas.port}/oauth2/token`;
const tokenApiPromise = page.waitForRequest(
(request) => request.url() === tokenUri && request.postDataJSON()["grant_type"] === "authorization_code",
);
await page.goto("/#/login");
await page.getByRole("button", { name: "Continue" }).click();
await registerAccountMas(page, mailhog.api, "alice", "alice@email.com", "Pa$sW0rD!");
// Eventually, we should end up at the home screen.
await expect(page).toHaveURL(/\/#\/home$/, { timeout: 10000 });
await expect(page.getByRole("heading", { name: "Welcome alice", exact: true })).toBeVisible();
const tokenApiRequest = await tokenApiPromise;
expect(tokenApiRequest.postDataJSON()["grant_type"]).toBe("authorization_code");
const deviceId = await page.evaluate<string>(() => window.localStorage.mx_device_id);
await app.settings.openUserSettings("General");
const newPagePromise = context.waitForEvent("page");
await page.getByRole("button", { name: "Manage account" }).click();
await app.settings.closeDialog();
// Assert MAS sees the session as OIDC Native
const newPage = await newPagePromise;
await newPage.getByText("Sessions").click();
await newPage.getByText(deviceId).click();
await expect(newPage.getByText("Element")).toBeVisible();
await expect(newPage.getByText("oauth2_session:")).toBeVisible();
await expect(newPage.getByText("http://localhost:8080/")).toBeVisible();
await newPage.close();
// Assert logging out revokes both tokens
const revokeUri = `http://localhost:${mas.port}/oauth2/revoke`;
const revokeAccessTokenPromise = page.waitForRequest(
(request) => request.url() === revokeUri && request.postDataJSON()["token_type_hint"] === "access_token",
);
const revokeRefreshTokenPromise = page.waitForRequest(
(request) => request.url() === revokeUri && request.postDataJSON()["token_type_hint"] === "refresh_token",
);
const locator = await app.settings.openUserMenu();
await locator.getByRole("menuitem", { name: "Sign out", exact: true }).click();
await revokeAccessTokenPromise;
await revokeRefreshTokenPromise;
});
});

View file

@ -134,7 +134,7 @@ export class Synapse implements Homeserver, HomeserverInstance {
public async stop(): Promise<string[]> {
if (!this.config) throw new Error("Missing existing synapse instance, did you call stop() before start()?");
const id = this.config.serverId;
const synapseLogsPath = path.join("playwright", "synapselogs", id);
const synapseLogsPath = path.join("playwright", "logs", "synapse", id);
await fse.ensureDir(synapseLogsPath);
await this.docker.persistLogsToFile({
stdoutFile: path.join(synapseLogsPath, "stdout.log"),

View file

@ -0,0 +1 @@
A synapse configured with auth delegated to via matrix authentication service

View file

@ -0,0 +1,194 @@
server_name: "localhost"
pid_file: /data/homeserver.pid
public_baseurl: "{{PUBLIC_BASEURL}}"
listeners:
- port: 8008
tls: false
bind_addresses: ["::"]
type: http
x_forwarded: true
resources:
- names: [client]
compress: false
database:
name: "sqlite3"
args:
database: ":memory:"
log_config: "/data/log.config"
rc_messages_per_second: 10000
rc_message_burst_count: 10000
rc_registration:
per_second: 10000
burst_count: 10000
rc_joins:
local:
per_second: 9999
burst_count: 9999
remote:
per_second: 9999
burst_count: 9999
rc_joins_per_room:
per_second: 9999
burst_count: 9999
rc_3pid_validation:
per_second: 1000
burst_count: 1000
rc_invites:
per_room:
per_second: 1000
burst_count: 1000
per_user:
per_second: 1000
burst_count: 1000
rc_login:
address:
per_second: 10000
burst_count: 10000
account:
per_second: 10000
burst_count: 10000
failed_attempts:
per_second: 10000
burst_count: 10000
media_store_path: "/data/media_store"
uploads_path: "/data/uploads"
registration_shared_secret: "{{REGISTRATION_SECRET}}"
report_stats: false
macaroon_secret_key: "{{MACAROON_SECRET_KEY}}"
form_secret: "{{FORM_SECRET}}"
signing_key_path: "/data/localhost.signing.key"
trusted_key_servers:
- server_name: "matrix.org"
suppress_key_server_warning: true
ui_auth:
session_timeout: "300s"
# Inhibit background updates as this Synapse isn't long-lived
background_updates:
min_batch_size: 100000
sleep_duration_ms: 100000
serve_server_wellknown: true
experimental_features:
msc3861:
enabled: true
issuer: http://localhost:%MAS_PORT%/
# We have to bake in the metadata here as we need to override `introspection_endpoint`
issuer_metadata: {
"issuer": "http://localhost:%MAS_PORT%/",
"authorization_endpoint": "http://localhost:%MAS_PORT%/authorize",
"token_endpoint": "http://localhost:%MAS_PORT%/oauth2/token",
"jwks_uri": "http://localhost:%MAS_PORT%/oauth2/keys.json",
"registration_endpoint": "http://localhost:%MAS_PORT%/oauth2/registration",
"scopes_supported": ["openid", "email"],
"response_types_supported": ["code", "id_token", "code id_token"],
"response_modes_supported": ["form_post", "query", "fragment"],
"grant_types_supported":
[
"authorization_code",
"refresh_token",
"client_credentials",
"urn:ietf:params:oauth:grant-type:device_code",
],
"token_endpoint_auth_methods_supported":
["client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt", "none"],
"token_endpoint_auth_signing_alg_values_supported":
[
"HS256",
"HS384",
"HS512",
"RS256",
"RS384",
"RS512",
"PS256",
"PS384",
"PS512",
"ES256",
"ES384",
"ES256K",
],
"revocation_endpoint": "http://localhost:%MAS_PORT%/oauth2/revoke",
"revocation_endpoint_auth_methods_supported":
["client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt", "none"],
"revocation_endpoint_auth_signing_alg_values_supported":
[
"HS256",
"HS384",
"HS512",
"RS256",
"RS384",
"RS512",
"PS256",
"PS384",
"PS512",
"ES256",
"ES384",
"ES256K",
],
# This is the only changed value
"introspection_endpoint": "http://host.containers.internal:%MAS_PORT%/oauth2/introspect",
"introspection_endpoint_auth_methods_supported":
["client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt", "none"],
"introspection_endpoint_auth_signing_alg_values_supported":
[
"HS256",
"HS384",
"HS512",
"RS256",
"RS384",
"RS512",
"PS256",
"PS384",
"PS512",
"ES256",
"ES384",
"ES256K",
],
"code_challenge_methods_supported": ["plain", "S256"],
"userinfo_endpoint": "http://localhost:%MAS_PORT%/oauth2/userinfo",
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported":
["RS256", "RS384", "RS512", "ES256", "ES384", "PS256", "PS384", "PS512", "ES256K"],
"userinfo_signing_alg_values_supported":
["RS256", "RS384", "RS512", "ES256", "ES384", "PS256", "PS384", "PS512", "ES256K"],
"display_values_supported": ["page"],
"claim_types_supported": ["normal"],
"claims_supported": ["iss", "sub", "aud", "iat", "exp", "nonce", "auth_time", "at_hash", "c_hash"],
"claims_parameter_supported": false,
"request_parameter_supported": false,
"request_uri_parameter_supported": false,
"prompt_values_supported": ["none", "login", "create"],
"device_authorization_endpoint": "http://localhost:%MAS_PORT%/oauth2/device",
"org.matrix.matrix-authentication-service.graphql_endpoint": "http://localhost:%MAS_PORT%/graphql",
"account_management_uri": "http://localhost:%MAS_PORT%/account/",
"account_management_actions_supported":
[
"org.matrix.profile",
"org.matrix.sessions_list",
"org.matrix.session_view",
"org.matrix.session_end",
],
}
# Matches the `client_id` in the auth service config
client_id: 0000000000000000000SYNAPSE
# Matches the `client_auth_method` in the auth service config
client_auth_method: client_secret_basic
# Matches the `client_secret` in the auth service config
client_secret: "SomeRandomSecret"
# Matches the `matrix.secret` in the auth service config
admin_token: "AnotherRandomSecret"
# URL to advertise to clients where users can self-manage their account
account_management_url: "http://localhost:%MAS_PORT%/account"

View file

@ -0,0 +1,50 @@
# Log configuration for Synapse.
#
# This is a YAML file containing a standard Python logging configuration
# dictionary. See [1] for details on the valid settings.
#
# Synapse also supports structured logging for machine readable logs which can
# be ingested by ELK stacks. See [2] for details.
#
# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema
# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html
version: 1
formatters:
precise:
format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s'
handlers:
# A handler that writes logs to stderr. Unused by default, but can be used
# instead of "buffer" and "file" in the logger handlers.
console:
class: logging.StreamHandler
formatter: precise
loggers:
synapse.storage.SQL:
# beware: increasing this to DEBUG will make synapse log sensitive
# information such as access tokens.
level: DEBUG
twisted:
# We send the twisted logging directly to the file handler,
# to work around https://github.com/matrix-org/synapse/issues/3471
# when using "buffer" logger. Use "console" to log to stderr instead.
handlers: [console]
propagate: false
root:
level: DEBUG
# Write logs to the `buffer` handler, which will buffer them together in memory,
# then write them to a file.
#
# Replace "buffer" with "console" to log to stderr instead. (Note that you'll
# also need to update the configuration for the `twisted` logger above, in
# this case.)
#
handlers: [console]
disable_existing_loggers: false

View file

@ -0,0 +1,153 @@
clients:
- client_id: 0000000000000000000SYNAPSE
client_auth_method: client_secret_basic
client_secret: "SomeRandomSecret"
http:
listeners:
- name: web
resources:
- name: discovery
- name: human
- name: oauth
- name: compat
- name: graphql
playground: true
- name: assets
path: /usr/local/share/mas-cli/assets/
binds:
- address: "[::]:8080"
proxy_protocol: false
- name: internal
resources:
- name: health
binds:
- host: localhost
port: 8081
proxy_protocol: false
trusted_proxies:
- 192.128.0.0/16
- 172.16.0.0/12
- 10.0.0.0/10
- 127.0.0.1/8
- fd00::/8
- ::1/128
public_base: "http://localhost:{{MAS_PORT}}/"
issuer: http://localhost:{{MAS_PORT}}/
database:
host: "{{POSTGRES_HOST}}"
port: 5432
database: postgres
username: postgres
password: "{{POSTGRES_PASSWORD}}"
max_connections: 10
min_connections: 0
connect_timeout: 30
idle_timeout: 600
max_lifetime: 1800
telemetry:
tracing:
exporter: none
propagators: []
metrics:
exporter: none
sentry:
dsn: null
templates:
path: /usr/local/share/mas-cli/templates/
assets_manifest: /usr/local/share/mas-cli/manifest.json
translations_path: /usr/local/share/mas-cli/translations/
email:
from: '"Authentication Service" <root@localhost>'
reply_to: '"Authentication Service" <root@localhost>'
transport: smtp
mode: plain
hostname: "host.containers.internal"
port: %{{SMTP_PORT}}
username: username
password: password
secrets:
encryption: 984b18e207c55ad5fbb2a49b217481a722917ee87b2308d4cf314c83fed8e3b5
keys:
- kid: YEAhzrKipJ
key: |
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAuIV+AW5vx52I4CuumgSxp6yvKfIAnRdALeZZCoFkIGxUli1B
S79NJ3ls46oLh1pSD9RrhaMp6HTNoi4K3hnP9Q9v77pD7KwdFKG3UdG1zksIB0s/
+/Ey/DmX4LPluwBBS7r/LkQ1jk745lENA++oiDqZf2D/uP8jCHlvaSNyVKTqi1ki
OXPd4T4xBUjzuas9ze5jQVSYtfOidgnv1EzUipbIxgvH1jNt4raRlmP8mOq7xEnW
R+cF5x6n/g17PdSEfrwO4kz6aKGZuMP5lVlDEEnMHKabFSQDBl7+Mpok6jXutbtA
uiBnsKEahF9eoj4na4fpbRNPdIVyoaN5eGvm5wIDAQABAoIBAApyFCYEmHNWaa83
CdVSOrRhRDE9r+c0r79pcNT1ajOjrk4qFa4yEC4R46YntCtfY5Hd1pBkIjU0l4d8
z8Su9WTMEOwjQUEepS7L0NLi6kXZXYT8L40VpGs+32grBvBFHW0qEtQNrHJ36gMv
x2rXoFTF7HaXiSJx3wvVxAbRqOE9tBXLsmNHaWaAdWQG5o77V9+zvMri3cAeEg2w
VkKokb0dza7es7xG3tqS26k69SrwGeeuKo7qCHPH2cfyWmY5Yhv8iOoA59JzzbiK
UdxyzCHskrPSpRKVkVVwmY3RBt282TmSRG7td7e5ESSj50P2e5BI5uu1Hp/dvU4F
vYjV7kECgYEA6WqYoUpVsgQiqhvJwJIc/8gRm0mUy8TenI36z4Iim01Nt7fibWH7
XnsFqLGjXtYNVWvBcCrUl9doEnRbJeG2eRGbGKYAWVrOeFvwM4fYvw9GoOiJdDj4
cgWDe7eHbHE+UTqR7Nnr/UBfipoNWDh6X68HRBuXowh0Q6tOfxsrRFECgYEAyl/V
4b8bFp3pKZZCb+KPSYsQf793cRmrBexPcLWcDPYbMZQADEZ/VLjbrNrpTOWxUWJT
hr8MrWswnHO+l5AFu5CNO+QgV2dHLk+2w8qpdpFRPJCfXfo2D3wZ0c4cv3VCwv1V
5y7f6XWVjDWZYV4wj6c3shxZJjZ+9Hbhf3/twbcCgYA6fuRRR3fCbRbi2qPtBrEN
yO3gpMgNaQEA6vP4HPzfPrhDWmn8T5nXS61XYW03zxz4U1De81zj0K/cMBzHmZFJ
NghQXQmpWwBzWVcREvJWr1Vb7erEnaJlsMwKrSvbGWYspSj82oAxr3hCG+lMOpsw
b4S6pM+TpAK/EqdRY1WsgQKBgQCGoMaaTRXqL9bC0bEU2XVVCWxKb8c3uEmrwQ7/
/fD4NmjUzI5TnDps1CVfkqoNe+hAKddDFqmKXHqUOfOaxDbsFje+lf5l5tDVoDYH
fjTKKdYPIm7CiAeauYY7qpA5Vfq52Opixy4yEwUPp0CII67OggFtPaqY3zwJyWQt
+57hdQKBgGCXM/KKt7ceUDcNJxSGjvu0zD9D5Sv2ihYlEBT/JLaTCCJdvzREevaJ
1d+mpUAt0Lq6A8NWOMq8HPaxAik3rMQ0WtM5iG+XgsUqvTSb7NcshArDLuWGnW3m
MC4rM0UBYAS4QweduUSH1imrwH/1Gu5+PxbiecceRMMggWpzu0Bq
-----END RSA PRIVATE KEY-----
- kid: 8J1AxrlNZT
key: |
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIF1cjfIOEdy3BXJ72x6fKpEB8WP1ddZAUJAaqqr/6CpToAoGCCqGSM49
AwEHoUQDQgAEfHdNuI1Yeh3/uOq2PlnW2vymloOVpwBYebbw4VVsna9xhnutIdQW
dE8hkX8Yb0pIDasrDiwllVLzSvsWJAI0Kw==
-----END EC PRIVATE KEY-----
- kid: 3BW6un1EBi
key: |
-----BEGIN EC PRIVATE KEY-----
MIGkAgEBBDA+3ZV17r8TsiMdw1cpbTSNbyEd5SMy3VS1Mk/kz6O2Ev/3QZut8GE2
q3eGtLBoVQigBwYFK4EEACKhZANiAASs8Wxjk/uRimRKXnPr2/wDaXkN9wMDjYQK
mZULb+0ZP1/cXmuXuri8hUGhQvIU8KWY9PkpV+LMPEdpE54mHPKSLjq5CDXoSZ/P
9f7cdRaOZ000KQPZfIFR9ujJTtDN7Vs=
-----END EC PRIVATE KEY-----
- kid: pkZ0pTKK0X
key: |
-----BEGIN EC PRIVATE KEY-----
MHQCAQEEIHenfsXYPc5yzjZKUfvmydDR1YRwdsfZYvwHf/2wsYxooAcGBSuBBAAK
oUQDQgAEON1x7Vlu+nA0KvC5vYSOHhDUkfLYNZwYSLPFVT02h9E13yFFMIJegIBl
Aer+6PMZpPc8ycyeH9N+U9NAyliBhQ==
-----END EC PRIVATE KEY-----
passwords:
enabled: true
schemes:
- version: 1
algorithm: argon2id
matrix:
homeserver: localhost
secret: AnotherRandomSecret
endpoint: "{{SYNAPSE_URL}}"
policy:
wasm_module: /usr/local/share/mas-cli/policy.wasm
client_registration_entrypoint: client_registration/violation
register_entrypoint: register/violation
authorization_grant_entrypoint: authorization_grant/violation
password_entrypoint: password/violation
email_entrypoint: email/violation
data:
client_registration:
allow_insecure_uris: true # allow non-SSL and localhost URIs
allow_missing_contacts: true # EW doesn't have contacts at this time
upstream_oauth2:
providers: []
branding:
service_name: null
policy_uri: null
tos_uri: null
imprint: null
logo_uri: null
experimental:
access_token_ttl: 300
compat_token_ttl: 300

View file

@ -0,0 +1,159 @@
/*
Copyright 2023 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.
*/
import path, { basename } from "node:path";
import os from "node:os";
import * as fse from "fs-extra";
import { BrowserContext, TestInfo } from "@playwright/test";
import { getFreePort } from "../utils/port";
import { Docker } from "../docker";
import { PG_PASSWORD, PostgresDocker } from "../postgres";
import { HomeserverInstance } from "../homeserver";
import { Instance as MailhogInstance } from "../mailhog";
// Docker tag to use for `ghcr.io/matrix-org/matrix-authentication-service` image.
// We use a debug tag so that we have a shell and can run all 3 necessary commands in one run.
const TAG = "0.8.0-debug";
export interface ProxyInstance {
containerId: string;
postgresId: string;
configDir: string;
port: number;
}
async function cfgDirFromTemplate(opts: {
postgresHost: string;
synapseUrl: string;
masPort: string;
smtpPort: string;
}): Promise<{
configDir: string;
}> {
const configPath = path.join(__dirname, "config.yaml");
const tempDir = await fse.mkdtemp(path.join(os.tmpdir(), "react-sdk-mas-"));
const outputHomeserver = path.join(tempDir, "config.yaml");
console.log(`Gen ${configPath} -> ${outputHomeserver}`);
let config = await fse.readFile(configPath, "utf8");
config = config.replace(/{{MAS_PORT}}/g, opts.masPort);
config = config.replace(/{{POSTGRES_HOST}}/g, opts.postgresHost);
config = config.replace(/{{POSTGRES_PASSWORD}}/g, PG_PASSWORD);
config = config.replace(/%{{SMTP_PORT}}/g, opts.smtpPort);
config = config.replace(/{{SYNAPSE_URL}}/g, opts.synapseUrl);
await fse.writeFile(outputHomeserver, config);
// Allow anyone to read, write and execute in the temp directory
// so that the DIND setup that we use to update the playwright screenshots work without any issues.
await fse.chmod(tempDir, 0o757);
return {
configDir: tempDir,
};
}
export class MatrixAuthenticationService {
private readonly masDocker = new Docker();
private readonly postgresDocker = new PostgresDocker("mas");
private instance: ProxyInstance;
public port: number;
constructor(private context: BrowserContext) {}
async prepare(): Promise<{ port: number }> {
this.port = await getFreePort();
return { port: this.port };
}
async start(homeserver: HomeserverInstance, mailhog: MailhogInstance): Promise<ProxyInstance> {
console.log(new Date(), "Starting mas...");
if (!this.port) await this.prepare();
const port = this.port;
const { containerId: postgresId, ipAddress: postgresIp } = await this.postgresDocker.start();
const { configDir } = await cfgDirFromTemplate({
masPort: port.toString(),
postgresHost: postgresIp,
synapseUrl: homeserver.config.dockerUrl,
smtpPort: mailhog.smtpPort.toString(),
});
console.log(new Date(), "starting mas container...", TAG);
const containerId = await this.masDocker.run({
image: "ghcr.io/matrix-org/matrix-authentication-service:" + TAG,
containerName: "react-sdk-playwright-mas",
params: ["-p", `${port}:8080/tcp`, "-v", `${configDir}:/config`, "--entrypoint", "sh"],
cmd: [
"-c",
"mas-cli database migrate --config /config/config.yaml && " +
"mas-cli config sync --config /config/config.yaml && " +
"mas-cli server --config /config/config.yaml",
],
});
console.log(new Date(), "started!");
// Set up redirects
const baseUrl = `http://localhost:${port}`;
for (const path of [
"**/_matrix/client/*/login",
"**/_matrix/client/*/login/**",
"**/_matrix/client/*/logout",
"**/_matrix/client/*/refresh",
]) {
await this.context.route(path, async (route) => {
await route.continue({
url: new URL(route.request().url().split("/").slice(3).join("/"), baseUrl).href,
});
});
}
this.instance = { containerId, postgresId, port, configDir };
return this.instance;
}
async stop(testInfo: TestInfo): Promise<void> {
if (!this.instance) return; // nothing to stop
const id = this.instance.containerId;
const logPath = path.join("playwright", "logs", "matrix-authentication-service", id);
await fse.ensureDir(logPath);
await this.masDocker.persistLogsToFile({
stdoutFile: path.join(logPath, "stdout.log"),
stderrFile: path.join(logPath, "stderr.log"),
});
await this.masDocker.stop();
await this.postgresDocker.stop();
if (testInfo.status !== "passed") {
const logs = [path.join(logPath, "stdout.log"), path.join(logPath, "stderr.log")];
for (const path of logs) {
await testInfo.attach(`mas-${basename(path)}`, {
path,
contentType: "text/plain",
});
}
await testInfo.attach("mas-config.yaml", {
path: path.join(this.instance.configDir, "config.yaml"),
contentType: "text/plain",
});
}
await fse.remove(this.instance.configDir);
console.log(new Date(), "Stopped mas.");
}
}

View file

@ -175,6 +175,7 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
// the enabled flag value.
const canChangePassword = !changePasswordCap || changePasswordCap["enabled"] !== false;
await this.context.oidcClientStore.readyPromise; // wait for the store to be ready
const externalAccountManagementUrl = this.context.oidcClientStore.accountManagementEndpoint;
// https://spec.matrix.org/v1.7/client-server-api/#m3pid_changes-capability
// We support as far back as v1.1 which doesn't have m.3pid_changes

View file

@ -173,7 +173,10 @@ const SessionManagerTab: React.FC = () => {
* delegated auth provider.
* See https://github.com/matrix-org/matrix-spec-proposals/pull/3824
*/
const delegatedAuthAccountUrl = sdkContext.oidcClientStore.accountManagementEndpoint;
const delegatedAuthAccountUrl = useAsyncMemo(async () => {
await sdkContext.oidcClientStore.readyPromise; // wait for the store to be ready
return sdkContext.oidcClientStore.accountManagementEndpoint;
}, [sdkContext.oidcClientStore]);
const disableMultipleSignout = !!delegatedAuthAccountUrl;
const userId = matrixClient?.getUserId();

View file

@ -30,17 +30,25 @@ import PlatformPeg from "../../PlatformPeg";
export class OidcClientStore {
private oidcClient?: OidcClient;
private initialisingOidcClientPromise: Promise<void> | undefined;
private authenticatedIssuer?: string;
private authenticatedIssuer?: string; // set only in OIDC-native mode
private _accountManagementEndpoint?: string;
/**
* Promise which resolves once this store is read to use, which may mean there is no OIDC client if we're in legacy mode,
* or we just have the account management endpoint if running in OIDC-aware mode.
*/
public readonly readyPromise: Promise<void>;
public constructor(private readonly matrixClient: MatrixClient) {
this.readyPromise = this.init();
}
private async init(): Promise<void> {
this.authenticatedIssuer = getStoredOidcTokenIssuer();
if (this.authenticatedIssuer) {
this.getOidcClient();
await this.getOidcClient();
} else {
matrixClient.waitForClientWellKnown().then((wellKnown) => {
const wellKnown = await this.matrixClient.waitForClientWellKnown();
this._accountManagementEndpoint = getDelegatedAuthAccountUrl(wellKnown);
});
}
}