Skip to content
This repository was archived by the owner on Nov 25, 2024. It is now read-only.

Commit bebf701

Browse files
authored
Add login fallback (#3302)
Part of #3216 The files are basically copied from Synapse, with minor changes to the called endpoints. We never seem to have had the `/_matrix/static/client/login/` endpoint, this adds it.
1 parent dae1ef2 commit bebf701

File tree

6 files changed

+430
-0
lines changed

6 files changed

+430
-0
lines changed

setup/base/base.go

+11
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ import (
5050
//go:embed static/*.gotmpl
5151
var staticContent embed.FS
5252

53+
//go:embed static/client/login
54+
var loginFallback embed.FS
55+
5356
const HTTPServerTimeout = time.Minute * 5
5457

5558
// CreateClient creates a new client (normally used for media fetch requests).
@@ -158,6 +161,14 @@ func SetupAndServeHTTP(
158161
_, _ = w.Write(landingPage.Bytes())
159162
})
160163

164+
// We only need the files beneath the static/client/login folder.
165+
sub, err := fs.Sub(loginFallback, "static/client/login")
166+
if err != nil {
167+
logrus.Panicf("unable to read embedded files, this should never happen: %s", err)
168+
}
169+
// Serve a static page for login fallback
170+
routers.Static.PathPrefix("/client/login/").Handler(http.StripPrefix("/_matrix/static/client/login/", http.FileServer(http.FS(sub))))
171+
161172
var clientHandler http.Handler
162173
clientHandler = routers.Client
163174
if cfg.Global.Sentry.Enabled {
+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
5+
<title> Login </title>
6+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
7+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
8+
<link rel="stylesheet" href="style.css">
9+
<script src="js/jquery-3.4.1.min.js"></script>
10+
<script src="js/login.js"></script>
11+
</head>
12+
<body onload="matrixLogin.onLoad()">
13+
<div id="container">
14+
<h1 id="title"></h1>
15+
16+
<span id="feedback"></span>
17+
18+
<div id="loading">
19+
<img src="spinner.gif" />
20+
</div>
21+
22+
<div id="sso_flow" class="login_flow" style="display: none;">
23+
Single-sign on:
24+
<form id="sso_form" action="/_matrix/client/v3/login/sso/redirect" method="get">
25+
<input id="sso_redirect_url" type="hidden" name="redirectUrl" value=""/>
26+
<input type="submit" value="Log in"/>
27+
</form>
28+
</div>
29+
30+
<div id="password_flow" class="login_flow" style="display: none;">
31+
Password Authentication:
32+
<form onsubmit="matrixLogin.passwordLogin(); return false;">
33+
<input id="user_id" size="32" type="text" placeholder="Matrix ID (e.g. bob)" autocapitalize="off" autocorrect="off" />
34+
<br/>
35+
<input id="password" size="32" type="password" placeholder="Password"/>
36+
<br/>
37+
38+
<input type="submit" value="Log in"/>
39+
</form>
40+
</div>
41+
42+
<div id="no_login_types" type="button" class="login_flow" style="display: none;">
43+
Log in currently unavailable.
44+
</div>
45+
</div>
46+
</body>
47+
</html>

setup/base/static/client/login/js/jquery-3.4.1.min.js

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
+291
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
window.matrixLogin = {
2+
endpoint: location.origin + "/_matrix/client/v3/login",
3+
serverAcceptsPassword: false,
4+
serverAcceptsSso: false,
5+
};
6+
7+
// Titles get updated through the process to give users feedback.
8+
const TITLE_PRE_AUTH = "Log in with one of the following methods";
9+
const TITLE_POST_AUTH = "Logging in...";
10+
11+
// The cookie used to store the original query parameters when using SSO.
12+
const COOKIE_KEY = "dendrite_login_fallback_qs";
13+
14+
/*
15+
* Submit a login request.
16+
*
17+
* type: The login type as a string (e.g. "m.login.foo").
18+
* data: An object of data specific to the login type.
19+
* extra: (Optional) An object to search for extra information to send with the
20+
* login request, e.g. device_id.
21+
* callback: (Optional) Function to call on successful login.
22+
*/
23+
function submitLogin(type, data, extra, callback) {
24+
console.log("Logging in with " + type);
25+
setTitle(TITLE_POST_AUTH);
26+
27+
// Add the login type.
28+
data.type = type;
29+
30+
// Add the device information, if it was provided.
31+
if (extra.device_id) {
32+
data.device_id = extra.device_id;
33+
}
34+
if (extra.initial_device_display_name) {
35+
data.initial_device_display_name = extra.initial_device_display_name;
36+
}
37+
38+
$.post(matrixLogin.endpoint, JSON.stringify(data), function(response) {
39+
if (callback) {
40+
callback();
41+
}
42+
matrixLogin.onLogin(response);
43+
}).fail(errorFunc);
44+
}
45+
46+
/*
47+
* Display an error to the user and show the login form again.
48+
*/
49+
function errorFunc(err) {
50+
// We want to show the error to the user rather than redirecting immediately to the
51+
// SSO portal (if SSO is the only login option), so we inhibit the redirect.
52+
showLogin(true);
53+
54+
if (err.responseJSON && err.responseJSON.error) {
55+
setFeedbackString(err.responseJSON.error + " (" + err.responseJSON.errcode + ")");
56+
}
57+
else {
58+
setFeedbackString("Request failed: " + err.status);
59+
}
60+
}
61+
62+
/*
63+
* Display an error to the user.
64+
*/
65+
function setFeedbackString(text) {
66+
$("#feedback").text(text);
67+
}
68+
69+
/*
70+
* (Maybe) Show the login forms.
71+
*
72+
* This actually does a few unrelated functions:
73+
*
74+
* * Configures the SSO redirect URL to come back to this page.
75+
* * Configures and shows the SSO form, if the server supports SSO.
76+
* * Otherwise, shows the password form.
77+
*/
78+
function showLogin(inhibitRedirect) {
79+
setTitle(TITLE_PRE_AUTH);
80+
81+
// If inhibitRedirect is false, and SSO is the only supported login method,
82+
// we can redirect straight to the SSO page.
83+
if (matrixLogin.serverAcceptsSso) {
84+
// Set the redirect to come back to this page, a login token will get
85+
// added as a query parameter and handled after the redirect.
86+
$("#sso_redirect_url").val(window.location.origin + window.location.pathname);
87+
88+
// Before submitting SSO, set the current query parameters into a cookie
89+
// for retrieval later.
90+
var qs = parseQsFromUrl();
91+
setCookie(COOKIE_KEY, JSON.stringify(qs));
92+
93+
// If password is not supported and redirects are allowed, then submit
94+
// the form (redirecting to the SSO provider).
95+
if (!inhibitRedirect && !matrixLogin.serverAcceptsPassword) {
96+
$("#sso_form").submit();
97+
return;
98+
}
99+
100+
// Otherwise, show the SSO form
101+
$("#sso_flow").show();
102+
}
103+
104+
if (matrixLogin.serverAcceptsPassword) {
105+
$("#password_flow").show();
106+
}
107+
108+
// If neither password or SSO are supported, show an error to the user.
109+
if (!matrixLogin.serverAcceptsPassword && !matrixLogin.serverAcceptsSso) {
110+
$("#no_login_types").show();
111+
}
112+
113+
$("#loading").hide();
114+
}
115+
116+
/*
117+
* Hides the forms and shows a loading throbber.
118+
*/
119+
function showSpinner() {
120+
$("#password_flow").hide();
121+
$("#sso_flow").hide();
122+
$("#no_login_types").hide();
123+
$("#loading").show();
124+
}
125+
126+
/*
127+
* Helper to show the page's main title.
128+
*/
129+
function setTitle(title) {
130+
$("#title").text(title);
131+
}
132+
133+
/*
134+
* Query the login endpoint for the homeserver's supported flows.
135+
*
136+
* This populates matrixLogin.serverAccepts* variables.
137+
*/
138+
function fetchLoginFlows(cb) {
139+
$.get(matrixLogin.endpoint, function(response) {
140+
for (var i = 0; i < response.flows.length; i++) {
141+
var flow = response.flows[i];
142+
if ("m.login.sso" === flow.type) {
143+
matrixLogin.serverAcceptsSso = true;
144+
console.log("Server accepts SSO");
145+
}
146+
if ("m.login.password" === flow.type) {
147+
matrixLogin.serverAcceptsPassword = true;
148+
console.log("Server accepts password");
149+
}
150+
}
151+
152+
cb();
153+
}).fail(errorFunc);
154+
}
155+
156+
/*
157+
* Called on load to fetch login flows and attempt SSO login (if a token is available).
158+
*/
159+
matrixLogin.onLoad = function() {
160+
fetchLoginFlows(function() {
161+
// (Maybe) attempt logging in via SSO if a token is available.
162+
if (!tryTokenLogin()) {
163+
showLogin(false);
164+
}
165+
});
166+
};
167+
168+
/*
169+
* Submit simple user & password login.
170+
*/
171+
matrixLogin.passwordLogin = function() {
172+
var user = $("#user_id").val();
173+
var pwd = $("#password").val();
174+
175+
setFeedbackString("");
176+
177+
showSpinner();
178+
submitLogin(
179+
"m.login.password",
180+
{user: user, password: pwd},
181+
parseQsFromUrl());
182+
};
183+
184+
/*
185+
* The onLogin function gets called after a successful login.
186+
*
187+
* It is expected that implementations override this to be notified when the
188+
* login is complete. The response to the login call is provided as the single
189+
* parameter.
190+
*/
191+
matrixLogin.onLogin = function(response) {
192+
// clobber this function
193+
console.warn("onLogin - This function should be replaced to proceed.");
194+
};
195+
196+
/*
197+
* Process the query parameters from the current URL into an object.
198+
*/
199+
function parseQsFromUrl() {
200+
var pos = window.location.href.indexOf("?");
201+
if (pos == -1) {
202+
return {};
203+
}
204+
var query = window.location.href.substr(pos + 1);
205+
206+
var result = {};
207+
query.split("&").forEach(function(part) {
208+
var item = part.split("=");
209+
var key = item[0];
210+
var val = item[1];
211+
212+
if (val) {
213+
val = decodeURIComponent(val);
214+
}
215+
result[key] = val;
216+
});
217+
return result;
218+
}
219+
220+
/*
221+
* Process the cookies and return an object.
222+
*/
223+
function parseCookies() {
224+
var allCookies = document.cookie;
225+
var result = {};
226+
allCookies.split(";").forEach(function(part) {
227+
var item = part.split("=");
228+
// Cookies might have arbitrary whitespace between them.
229+
var key = item[0].trim();
230+
// You can end up with a broken cookie that doesn't have an equals sign
231+
// in it. Set to an empty value.
232+
var val = (item[1] || "").trim();
233+
// Values might be URI encoded.
234+
if (val) {
235+
val = decodeURIComponent(val);
236+
}
237+
result[key] = val;
238+
});
239+
return result;
240+
}
241+
242+
/*
243+
* Set a cookie that is valid for 1 hour.
244+
*/
245+
function setCookie(key, value) {
246+
// The maximum age is set in seconds.
247+
var maxAge = 60 * 60;
248+
// Set the cookie, this defaults to the current domain and path.
249+
document.cookie = key + "=" + encodeURIComponent(value) + ";max-age=" + maxAge + ";sameSite=lax";
250+
}
251+
252+
/*
253+
* Removes a cookie by key.
254+
*/
255+
function deleteCookie(key) {
256+
// Delete a cookie by setting the expiration to 0. (Note that the value
257+
// doesn't matter.)
258+
document.cookie = key + "=deleted;expires=0";
259+
}
260+
261+
/*
262+
* Submits the login token if one is found in the query parameters. Returns a
263+
* boolean of whether the login token was found or not.
264+
*/
265+
function tryTokenLogin() {
266+
// Check if the login token is in the query parameters.
267+
var qs = parseQsFromUrl();
268+
269+
var loginToken = qs.loginToken;
270+
if (!loginToken) {
271+
return false;
272+
}
273+
274+
// Retrieve the original query parameters (from before the SSO redirect).
275+
// They are stored as JSON in a cookie.
276+
var cookies = parseCookies();
277+
var originalQueryParams = JSON.parse(cookies[COOKIE_KEY] || "{}")
278+
279+
// If the login is successful, delete the cookie.
280+
function callback() {
281+
deleteCookie(COOKIE_KEY);
282+
}
283+
284+
submitLogin(
285+
"m.login.token",
286+
{token: loginToken},
287+
originalQueryParams,
288+
callback);
289+
290+
return true;
291+
}
1.81 KB
Loading

0 commit comments

Comments
 (0)