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

Add login fallback #3302

Merged
merged 3 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions setup/base/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ import (
//go:embed static/*.gotmpl
var staticContent embed.FS

//go:embed static/client/login
var loginFallback embed.FS

const HTTPServerTimeout = time.Minute * 5

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

// We only need the files beneath the static/client/login folder.
sub, err := fs.Sub(loginFallback, "static/client/login")
if err != nil {
logrus.Panicf("unable to read embedded files, this should never happen: %s", err)
}
// Serve a static page for login fallback
routers.Static.PathPrefix("/client/login/").Handler(http.StripPrefix("/_matrix/static/client/login/", http.FileServer(http.FS(sub))))

var clientHandler http.Handler
clientHandler = routers.Client
if cfg.Global.Sentry.Enabled {
Expand Down
47 changes: 47 additions & 0 deletions setup/base/static/client/login/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title> Login </title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="style.css">
<script src="js/jquery-3.4.1.min.js"></script>
<script src="js/login.js"></script>
</head>
<body onload="matrixLogin.onLoad()">
<div id="container">
<h1 id="title"></h1>

<span id="feedback"></span>

<div id="loading">
<img src="spinner.gif" />
</div>

<div id="sso_flow" class="login_flow" style="display: none;">
Single-sign on:
<form id="sso_form" action="/_matrix/client/v3/login/sso/redirect" method="get">
<input id="sso_redirect_url" type="hidden" name="redirectUrl" value=""/>
<input type="submit" value="Log in"/>
</form>
</div>

<div id="password_flow" class="login_flow" style="display: none;">
Password Authentication:
<form onsubmit="matrixLogin.passwordLogin(); return false;">
<input id="user_id" size="32" type="text" placeholder="Matrix ID (e.g. bob)" autocapitalize="off" autocorrect="off" />
<br/>
<input id="password" size="32" type="password" placeholder="Password"/>
<br/>

<input type="submit" value="Log in"/>
</form>
</div>

<div id="no_login_types" type="button" class="login_flow" style="display: none;">
Log in currently unavailable.
</div>
</div>
</body>
</html>
2 changes: 2 additions & 0 deletions setup/base/static/client/login/js/jquery-3.4.1.min.js

Large diffs are not rendered by default.

291 changes: 291 additions & 0 deletions setup/base/static/client/login/js/login.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
window.matrixLogin = {
endpoint: location.origin + "/_matrix/client/v3/login",
serverAcceptsPassword: false,
serverAcceptsSso: false,
};

// Titles get updated through the process to give users feedback.
const TITLE_PRE_AUTH = "Log in with one of the following methods";
const TITLE_POST_AUTH = "Logging in...";

// The cookie used to store the original query parameters when using SSO.
const COOKIE_KEY = "dendrite_login_fallback_qs";

/*
* Submit a login request.
*
* type: The login type as a string (e.g. "m.login.foo").
* data: An object of data specific to the login type.
* extra: (Optional) An object to search for extra information to send with the
* login request, e.g. device_id.
* callback: (Optional) Function to call on successful login.
*/
function submitLogin(type, data, extra, callback) {
console.log("Logging in with " + type);
setTitle(TITLE_POST_AUTH);

// Add the login type.
data.type = type;

// Add the device information, if it was provided.
if (extra.device_id) {
data.device_id = extra.device_id;
}
if (extra.initial_device_display_name) {
data.initial_device_display_name = extra.initial_device_display_name;
}

$.post(matrixLogin.endpoint, JSON.stringify(data), function(response) {
if (callback) {
callback();
}
matrixLogin.onLogin(response);
}).fail(errorFunc);
}

/*
* Display an error to the user and show the login form again.
*/
function errorFunc(err) {
// We want to show the error to the user rather than redirecting immediately to the
// SSO portal (if SSO is the only login option), so we inhibit the redirect.
showLogin(true);

if (err.responseJSON && err.responseJSON.error) {
setFeedbackString(err.responseJSON.error + " (" + err.responseJSON.errcode + ")");
}
else {
setFeedbackString("Request failed: " + err.status);
}
}

/*
* Display an error to the user.
*/
function setFeedbackString(text) {
$("#feedback").text(text);
}

/*
* (Maybe) Show the login forms.
*
* This actually does a few unrelated functions:
*
* * Configures the SSO redirect URL to come back to this page.
* * Configures and shows the SSO form, if the server supports SSO.
* * Otherwise, shows the password form.
*/
function showLogin(inhibitRedirect) {
setTitle(TITLE_PRE_AUTH);

// If inhibitRedirect is false, and SSO is the only supported login method,
// we can redirect straight to the SSO page.
if (matrixLogin.serverAcceptsSso) {
// Set the redirect to come back to this page, a login token will get
// added as a query parameter and handled after the redirect.
$("#sso_redirect_url").val(window.location.origin + window.location.pathname);

// Before submitting SSO, set the current query parameters into a cookie
// for retrieval later.
var qs = parseQsFromUrl();
setCookie(COOKIE_KEY, JSON.stringify(qs));

// If password is not supported and redirects are allowed, then submit
// the form (redirecting to the SSO provider).
if (!inhibitRedirect && !matrixLogin.serverAcceptsPassword) {
$("#sso_form").submit();
return;
}

// Otherwise, show the SSO form
$("#sso_flow").show();
}

if (matrixLogin.serverAcceptsPassword) {
$("#password_flow").show();
}

// If neither password or SSO are supported, show an error to the user.
if (!matrixLogin.serverAcceptsPassword && !matrixLogin.serverAcceptsSso) {
$("#no_login_types").show();
}

$("#loading").hide();
}

/*
* Hides the forms and shows a loading throbber.
*/
function showSpinner() {
$("#password_flow").hide();
$("#sso_flow").hide();
$("#no_login_types").hide();
$("#loading").show();
}

/*
* Helper to show the page's main title.
*/
function setTitle(title) {
$("#title").text(title);
}

/*
* Query the login endpoint for the homeserver's supported flows.
*
* This populates matrixLogin.serverAccepts* variables.
*/
function fetchLoginFlows(cb) {
$.get(matrixLogin.endpoint, function(response) {
for (var i = 0; i < response.flows.length; i++) {
var flow = response.flows[i];
if ("m.login.sso" === flow.type) {
matrixLogin.serverAcceptsSso = true;
console.log("Server accepts SSO");
}
if ("m.login.password" === flow.type) {
matrixLogin.serverAcceptsPassword = true;
console.log("Server accepts password");
}
}

cb();
}).fail(errorFunc);
}

/*
* Called on load to fetch login flows and attempt SSO login (if a token is available).
*/
matrixLogin.onLoad = function() {
fetchLoginFlows(function() {
// (Maybe) attempt logging in via SSO if a token is available.
if (!tryTokenLogin()) {
showLogin(false);
}
});
};

/*
* Submit simple user & password login.
*/
matrixLogin.passwordLogin = function() {
var user = $("#user_id").val();
var pwd = $("#password").val();

setFeedbackString("");

showSpinner();
submitLogin(
"m.login.password",
{user: user, password: pwd},
parseQsFromUrl());
};

/*
* The onLogin function gets called after a successful login.
*
* It is expected that implementations override this to be notified when the
* login is complete. The response to the login call is provided as the single
* parameter.
*/
matrixLogin.onLogin = function(response) {
// clobber this function
console.warn("onLogin - This function should be replaced to proceed.");
};

/*
* Process the query parameters from the current URL into an object.
*/
function parseQsFromUrl() {
var pos = window.location.href.indexOf("?");
if (pos == -1) {
return {};
}
var query = window.location.href.substr(pos + 1);

var result = {};
query.split("&").forEach(function(part) {
var item = part.split("=");
var key = item[0];
var val = item[1];

if (val) {
val = decodeURIComponent(val);
}
result[key] = val;
});
return result;
}

/*
* Process the cookies and return an object.
*/
function parseCookies() {
var allCookies = document.cookie;
var result = {};
allCookies.split(";").forEach(function(part) {
var item = part.split("=");
// Cookies might have arbitrary whitespace between them.
var key = item[0].trim();
// You can end up with a broken cookie that doesn't have an equals sign
// in it. Set to an empty value.
var val = (item[1] || "").trim();
// Values might be URI encoded.
if (val) {
val = decodeURIComponent(val);
}
result[key] = val;
});
return result;
}

/*
* Set a cookie that is valid for 1 hour.
*/
function setCookie(key, value) {
// The maximum age is set in seconds.
var maxAge = 60 * 60;
// Set the cookie, this defaults to the current domain and path.
document.cookie = key + "=" + encodeURIComponent(value) + ";max-age=" + maxAge + ";sameSite=lax";
}

/*
* Removes a cookie by key.
*/
function deleteCookie(key) {
// Delete a cookie by setting the expiration to 0. (Note that the value
// doesn't matter.)
document.cookie = key + "=deleted;expires=0";
}

/*
* Submits the login token if one is found in the query parameters. Returns a
* boolean of whether the login token was found or not.
*/
function tryTokenLogin() {
// Check if the login token is in the query parameters.
var qs = parseQsFromUrl();

var loginToken = qs.loginToken;
if (!loginToken) {
return false;
}

// Retrieve the original query parameters (from before the SSO redirect).
// They are stored as JSON in a cookie.
var cookies = parseCookies();
var originalQueryParams = JSON.parse(cookies[COOKIE_KEY] || "{}")

// If the login is successful, delete the cookie.
function callback() {
deleteCookie(COOKIE_KEY);
}

submitLogin(
"m.login.token",
{token: loginToken},
originalQueryParams,
callback);

return true;
}
Binary file added setup/base/static/client/login/spinner.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading