2022-04-04 21:50:40 +00:00
|
|
|
import { ApiError } from 'app/common/ApiError';
|
2022-06-15 14:29:29 +00:00
|
|
|
import { appSettings } from 'app/server/lib/AppSettings';
|
2022-04-04 21:50:40 +00:00
|
|
|
import { getRequestProfile } from 'app/server/lib/Authorizer';
|
|
|
|
import { expressWrap } from 'app/server/lib/expressWrap';
|
2023-11-07 20:04:23 +00:00
|
|
|
import { GristLoginMiddleware, GristLoginSystem, GristServer, setUserInSession } from 'app/server/lib/GristServer';
|
|
|
|
import log from 'app/server/lib/log';
|
2022-04-04 21:50:40 +00:00
|
|
|
import { optStringParam } from 'app/server/lib/requestUtils';
|
|
|
|
import * as express from 'express';
|
|
|
|
import trimEnd = require('lodash/trimEnd');
|
|
|
|
import trimStart = require('lodash/trimStart');
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return a login system that can work in concert with middleware that
|
2023-11-07 20:04:23 +00:00
|
|
|
* does authentication and then passes identity in a header.
|
|
|
|
* There are two modes of operation, distinguished by whether GRIST_IGNORE_SESSION is set.
|
2022-04-04 21:50:40 +00:00
|
|
|
*
|
2023-11-07 20:04:23 +00:00
|
|
|
* 1. With sessions, and forward-auth on login endpoints.
|
2022-04-04 21:50:40 +00:00
|
|
|
*
|
2023-11-07 20:04:23 +00:00
|
|
|
* For example, using traefik reverse proxy with traefik-forward-auth middleware:
|
|
|
|
*
|
|
|
|
* https://github.com/thomseddon/traefik-forward-auth
|
|
|
|
*
|
|
|
|
* Grist environment:
|
|
|
|
* - GRIST_FORWARD_AUTH_HEADER: set to a header that will contain
|
|
|
|
* authorized user emails, say "x-forwarded-user"
|
|
|
|
* - GRIST_FORWARD_AUTH_LOGOUT_PATH: set to a path that will trigger
|
|
|
|
* a logout (for traefik-forward-auth by default that is /_oauth/logout).
|
|
|
|
* - GRIST_FORWARD_AUTH_LOGIN_PATH: optionally set to override the default (/auth/login).
|
|
|
|
* - GRIST_IGNORE_SESSION: do NOT set, or set to a falsy value.
|
|
|
|
*
|
|
|
|
* Reverse proxy:
|
|
|
|
* - Make sure your reverse proxy applies the forward auth middleware to
|
|
|
|
* GRIST_FORWARD_AUTH_LOGIN_PATH and GRIST_FORWARD_AUTH_LOGOUT_PATH.
|
|
|
|
* - If you want to allow anonymous access in some cases, make sure all
|
|
|
|
* other paths are free of the forward auth middleware - Grist will
|
|
|
|
* trigger it as needed by redirecting to /auth/login.
|
|
|
|
* - Grist only uses the configured header at login/logout. Once the user is logged in, Grist
|
|
|
|
* will use the session info to identify the user, until logout.
|
|
|
|
* - Optionally, tell the middleware where to forward back to after logout.
|
|
|
|
* (For traefik-forward-auth, you'd run it with LOGOUT_REDIRECT set to .../signed-out)
|
|
|
|
*
|
|
|
|
* 2. With no sessions, and forward-auth on all endpoints.
|
|
|
|
*
|
|
|
|
* For example, using HTTP Basic Auth and server configuration that sets a header to the
|
|
|
|
* logged-in user (e.g. to REMOTE_USER with Apache).
|
|
|
|
*
|
|
|
|
* Grist environment:
|
|
|
|
* - GRIST_IGNORE_SESSION: set to true. Grist sessions will not be used.
|
|
|
|
* - GRIST_FORWARD_AUTH_HEADER: set to to a header that will contain authorized user emails, say
|
|
|
|
* "x-remote-user".
|
|
|
|
*
|
|
|
|
* Reverse proxy:
|
|
|
|
* - Make sure your reverse proxy sets the header you specified for all requests that may need
|
|
|
|
* login information. It is imperative that this header cannot be spoofed by the user, since
|
|
|
|
* Grist will trust whatever is in it.
|
|
|
|
*
|
|
|
|
* GRIST_PROXY_AUTH_HEADER is deprecated in favor of GRIST_FORWARD_AUTH_HEADER. It is currently
|
|
|
|
* interpreted as a synonym, with a warning, but support for it may be dropped.
|
2022-04-04 21:50:40 +00:00
|
|
|
*
|
|
|
|
* Redirection logic currently assumes a single-site installation.
|
|
|
|
*/
|
|
|
|
export async function getForwardAuthLoginSystem(): Promise<GristLoginSystem|undefined> {
|
2022-06-15 14:29:29 +00:00
|
|
|
const section = appSettings.section('login').section('system').section('forwardAuth');
|
2023-11-07 20:04:23 +00:00
|
|
|
const headerSetting = section.flag('header');
|
|
|
|
const header = headerSetting.readString({
|
|
|
|
envVar: ['GRIST_FORWARD_AUTH_HEADER', 'GRIST_PROXY_AUTH_HEADER']
|
2022-06-15 14:29:29 +00:00
|
|
|
});
|
2023-11-07 20:04:23 +00:00
|
|
|
if (!header) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (headerSetting.describe().foundInEnvVar === 'GRIST_PROXY_AUTH_HEADER') {
|
|
|
|
log.warn("GRIST_PROXY_AUTH_HEADER is deprecated; interpreted as a synonym of GRIST_FORWARD_AUTH_HEADER");
|
|
|
|
}
|
|
|
|
|
2022-06-15 14:29:29 +00:00
|
|
|
section.flag('active').set(true);
|
|
|
|
const logoutPath = section.flag('logoutPath').readString({
|
|
|
|
envVar: 'GRIST_FORWARD_AUTH_LOGOUT_PATH'
|
|
|
|
}) || '';
|
|
|
|
const loginPath = section.flag('loginPath').requireString({
|
|
|
|
envVar: 'GRIST_FORWARD_AUTH_LOGIN_PATH',
|
|
|
|
defaultValue: '/auth/login',
|
2023-11-07 20:04:23 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
const skipSession = appSettings.section('login').flag('skipSession').readBool({
|
|
|
|
envVar: 'GRIST_IGNORE_SESSION',
|
|
|
|
});
|
|
|
|
|
2022-04-04 21:50:40 +00:00
|
|
|
return {
|
|
|
|
async getMiddleware(gristServer: GristServer) {
|
|
|
|
async function getLoginRedirectUrl(req: express.Request, url: URL) {
|
2022-06-15 14:29:29 +00:00
|
|
|
const target = new URL(trimEnd(gristServer.getHomeUrl(req), '/') +
|
|
|
|
'/' + trimStart(loginPath, '/'));
|
2023-11-07 20:04:23 +00:00
|
|
|
// In lieu of sanitizing the next url, we include only the path
|
2022-04-04 21:50:40 +00:00
|
|
|
// component. This will only work for single-domain installations.
|
|
|
|
target.searchParams.append('next', url.pathname);
|
|
|
|
return target.href;
|
|
|
|
}
|
2023-11-07 20:04:23 +00:00
|
|
|
const middleware: GristLoginMiddleware = {
|
2022-04-04 21:50:40 +00:00
|
|
|
getLoginRedirectUrl,
|
|
|
|
getSignUpRedirectUrl: getLoginRedirectUrl,
|
|
|
|
async getLogoutRedirectUrl(req: express.Request) {
|
|
|
|
return trimEnd(gristServer.getHomeUrl(req), '/') + '/' +
|
|
|
|
trimStart(logoutPath, '/');
|
|
|
|
},
|
|
|
|
async addEndpoints(app: express.Express) {
|
2023-11-07 20:04:23 +00:00
|
|
|
app.get(loginPath, expressWrap(async (req, res) => {
|
2022-04-04 21:50:40 +00:00
|
|
|
const profile = getRequestProfile(req, header);
|
|
|
|
if (!profile) {
|
|
|
|
throw new ApiError('cannot find user', 401);
|
|
|
|
}
|
|
|
|
await setUserInSession(req, gristServer, profile);
|
|
|
|
const target = new URL(gristServer.getHomeUrl(req));
|
2023-09-05 18:27:35 +00:00
|
|
|
const next = optStringParam(req.query.next, 'next');
|
2022-04-04 21:50:40 +00:00
|
|
|
if (next) {
|
|
|
|
target.pathname = next;
|
|
|
|
}
|
|
|
|
res.redirect(target.href);
|
|
|
|
}));
|
2023-11-07 20:04:23 +00:00
|
|
|
return skipSession ? "forward-auth-skip-session" : "forward-auth";
|
2022-06-15 14:29:29 +00:00
|
|
|
},
|
2022-04-04 21:50:40 +00:00
|
|
|
};
|
2023-11-07 20:04:23 +00:00
|
|
|
if (skipSession) {
|
|
|
|
// With GRIST_IGNORE_SESSION, respect the header for all requests.
|
|
|
|
middleware.overrideProfile = async (req) => getRequestProfile(req, header);
|
|
|
|
}
|
|
|
|
return middleware;
|
2022-04-04 21:50:40 +00:00
|
|
|
},
|
|
|
|
async deleteUser() {
|
|
|
|
// If we could delete the user account in the external
|
|
|
|
// authentication system, this is our chance - but we can't.
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|