feat: in-app Chat (Matrix JWT SSO) + Drive (embedded oCIS)
- api: matrix-sso module (tuwunel jwt -> login/get_token), POST /matrix/session, GET /matrix/identity, drive controller - web: /chat full-height Element view, /files Library+Drive tabs, embedded-app frame component - edge: /hub-sso.html handshake route (origin-pinned CSP), X-Frame-Options strip on files - oCIS: csp.yaml frame-ancestors app.occupymedia.cc - server config still required: tuwunel [global.jwt], MATRIX_SSO_JWT_SECRET, oCIS restart
This commit is contained in:
parent
a2aa70c961
commit
fe5f2403d6
22 changed files with 1057 additions and 8 deletions
|
|
@ -85,6 +85,14 @@ services:
|
|||
ARCHIVE_PROVISIONER_HOMESERVER_URL: ${ARCHIVE_PROVISIONER_HOMESERVER_URL:-}
|
||||
ARCHIVE_PROVISIONER_USER: ${ARCHIVE_PROVISIONER_USER:-}
|
||||
ARCHIVE_PROVISIONER_PASSWORD: ${ARCHIVE_PROVISIONER_PASSWORD:-}
|
||||
# In-app Chat sign-in: same secret as [global.jwt] key in tuwunel.toml.
|
||||
# Unset = the Chat view falls back to Element's own sign-in screen.
|
||||
MATRIX_SSO_JWT_SECRET: ${MATRIX_SSO_JWT_SECRET:-}
|
||||
MATRIX_SSO_HOMESERVER_URL: ${MATRIX_SSO_HOMESERVER_URL:-https://chat.occupymedia.cc}
|
||||
MATRIX_SSO_LOCALPART_PREFIX: ${MATRIX_SSO_LOCALPART_PREFIX:-}
|
||||
# In-app Drive: oCIS web origin + auth mode (interactive | oidc).
|
||||
FILES_DRIVE_URL: ${FILES_DRIVE_URL:-https://files.occupymedia.cc}
|
||||
FILES_DRIVE_AUTH: ${FILES_DRIVE_AUTH:-interactive}
|
||||
ports: ["3000:3000"]
|
||||
depends_on:
|
||||
postgres: { condition: service_healthy }
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ services:
|
|||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- /opt/occ-brand:/opt/occ-brand:ro
|
||||
- ./hub-sso:/opt/hub-sso:ro
|
||||
- ./certs:/etc/letsencrypt:ro
|
||||
|
||||
networks:
|
||||
|
|
|
|||
63
infra/occupymedia/edge/hub-sso/chat-sso.html
Normal file
63
infra/occupymedia/edge/hub-sso/chat-sso.html
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<!doctype html>
|
||||
<!--
|
||||
Hub → Element sign-in bootstrap, served by the edge at
|
||||
https://chat.occupymedia.cc/hub-sso.html (same origin as Element Web).
|
||||
|
||||
Element only completes `/?loginToken=…` when localStorage["mx_sso_hs_url"]
|
||||
was set on its own origin beforehand (Lifecycle.attemptTokenLogin — normally
|
||||
Element writes it when IT starts an SSO redirect). The Hub cannot write
|
||||
another origin's storage, so this page does it, then hands over.
|
||||
|
||||
Protocol (window.postMessage, both directions origin-pinned):
|
||||
page → Hub { type: "tmbh:chat-sso:need-token" }
|
||||
Hub → page { type: "tmbh:chat-sso:token", loginToken }
|
||||
The Hub frames this page as /hub-sso.html#expect=<mxid>; when Element
|
||||
already holds a live session for that MXID no token is requested at all, so
|
||||
re-opening Chat never replaces the device (and its E2EE keys).
|
||||
-->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="referrer" content="no-referrer">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Signing in to chat…</title>
|
||||
<style>html,body{margin:0;height:100%;background:#000;color:#9a9a9a;font:14px system-ui,sans-serif;display:grid;place-items:center}</style>
|
||||
</head>
|
||||
<body>
|
||||
<p>Signing in to chat…</p>
|
||||
<script>
|
||||
(function () {
|
||||
var HUB_ORIGINS = ["https://app.occupymedia.cc"];
|
||||
// Element and the homeserver share this host (/_matrix is proxied here).
|
||||
var HOMESERVER = location.origin;
|
||||
|
||||
function openElement(query) {
|
||||
location.replace("/" + query);
|
||||
}
|
||||
|
||||
// Opened directly rather than framed by the Hub: plain Element.
|
||||
if (window.parent === window) return openElement("");
|
||||
|
||||
var expected = new URLSearchParams(location.hash.slice(1)).get("expect");
|
||||
var signedInAs = localStorage.getItem("mx_user_id");
|
||||
var live = localStorage.getItem("mx_has_access_token") === "true" && localStorage.getItem("mx_soft_logout") !== "true";
|
||||
if (expected !== null && expected === signedInAs && live) return openElement("");
|
||||
|
||||
var done = false;
|
||||
window.addEventListener("message", function (event) {
|
||||
if (done || event.source !== window.parent || HUB_ORIGINS.indexOf(event.origin) === -1) return;
|
||||
var data = event.data;
|
||||
if (data === null || typeof data !== "object" || data.type !== "tmbh:chat-sso:token" || typeof data.loginToken !== "string") return;
|
||||
done = true;
|
||||
localStorage.setItem("mx_sso_hs_url", HOMESERVER);
|
||||
localStorage.removeItem("mx_sso_is_url");
|
||||
openElement("?loginToken=" + encodeURIComponent(data.loginToken));
|
||||
});
|
||||
// A non-matching targetOrigin is dropped by the browser, never delivered.
|
||||
HUB_ORIGINS.forEach(function (origin) {
|
||||
window.parent.postMessage({ type: "tmbh:chat-sso:need-token" }, origin);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -118,6 +118,14 @@ http {
|
|||
default_type application/json;
|
||||
return 200 '{"m.homeserver":{"base_url":"https://chat.occupymedia.cc"}}';
|
||||
}
|
||||
# Hub in-app Chat: sign-in bootstrap on Element's own origin (see hub-sso/chat-sso.html).
|
||||
location = /hub-sso.html {
|
||||
alias /opt/hub-sso/chat-sso.html;
|
||||
default_type text/html;
|
||||
add_header X-Served-By occ-edge always;
|
||||
add_header Cache-Control "no-store" always;
|
||||
add_header Content-Security-Policy "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; frame-ancestors https://app.occupymedia.cc" always;
|
||||
}
|
||||
location / {
|
||||
proxy_pass http://occ-element:80;
|
||||
proxy_http_version 1.1;
|
||||
|
|
@ -154,6 +162,14 @@ http {
|
|||
default_type application/json;
|
||||
return 200 '{"m.server":"chat.occupymedia.cc:443"}';
|
||||
}
|
||||
# Hub in-app Chat: sign-in bootstrap on Element's own origin (see hub-sso/chat-sso.html).
|
||||
location = /hub-sso.html {
|
||||
alias /opt/hub-sso/chat-sso.html;
|
||||
default_type text/html;
|
||||
add_header X-Served-By occ-edge always;
|
||||
add_header Cache-Control "no-store" always;
|
||||
add_header Content-Security-Policy "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; frame-ancestors https://app.occupymedia.cc" always;
|
||||
}
|
||||
location / {
|
||||
proxy_pass http://occ-element:80;
|
||||
proxy_http_version 1.1;
|
||||
|
|
@ -189,6 +205,9 @@ http {
|
|||
}
|
||||
location / {
|
||||
proxy_pass http://192.168.13.210:9200;
|
||||
# Hub in-app Drive frames oCIS web from app.occupymedia.cc: framing policy
|
||||
# lives in oCIS csp.yaml (frame-ancestors); drop the legacy SAMEORIGIN.
|
||||
proxy_hide_header X-Frame-Options;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
|
@ -220,6 +239,9 @@ http {
|
|||
}
|
||||
location / {
|
||||
proxy_pass http://192.168.13.210:9200;
|
||||
# Hub in-app Drive frames oCIS web from app.occupymedia.cc: framing policy
|
||||
# lives in oCIS csp.yaml (frame-ancestors); drop the legacy SAMEORIGIN.
|
||||
proxy_hide_header X-Frame-Options;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ directives:
|
|||
child-src: ["'self'"]
|
||||
connect-src: ["'self'", "blob:"]
|
||||
font-src: ["'self'", "data:"]
|
||||
frame-ancestors: ["'self'"]
|
||||
# The Hub's in-app Drive view (app.occupymedia.cc/files/drive) frames oCIS web.
|
||||
frame-ancestors: ["'self'", "https://app.occupymedia.cc"]
|
||||
frame-src: ["'self'", "blob:", "https://${OFFICE_HOST}"]
|
||||
img-src: ["'self'", "data:", "blob:", "https://${OFFICE_HOST}"]
|
||||
manifest-src: ["'self'"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue