mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) add a login method based on headers
Summary: This fleshes out header-based authentication a little more to work with traefik-forward-auth. Test Plan: manually tested Reviewers: georgegevoian Reviewed By: georgegevoian Subscribers: alexmojaki Differential Revision: https://phab.getgrist.com/D3348
This commit is contained in:
parent
c6d66e15bf
commit
fea8f906d7
@ -94,10 +94,11 @@ export function isSingleUserMode(): boolean {
|
|||||||
/**
|
/**
|
||||||
* Returns a profile if it can be deduced from the request. This requires a
|
* Returns a profile if it can be deduced from the request. This requires a
|
||||||
* header to specify the users' email address. The header to set comes from the
|
* header to specify the users' email address. The header to set comes from the
|
||||||
* environment variable GRIST_PROXY_AUTH_HEADER.
|
* environment variable GRIST_PROXY_AUTH_HEADER, or may be passed in.
|
||||||
*/
|
*/
|
||||||
export function getRequestProfile(req: Request|IncomingMessage): UserProfile|undefined {
|
export function getRequestProfile(req: Request|IncomingMessage,
|
||||||
const header = process.env.GRIST_PROXY_AUTH_HEADER;
|
header?: string): UserProfile|undefined {
|
||||||
|
header = header || process.env.GRIST_PROXY_AUTH_HEADER;
|
||||||
let profile: UserProfile|undefined;
|
let profile: UserProfile|undefined;
|
||||||
|
|
||||||
if (header) {
|
if (header) {
|
||||||
|
@ -729,6 +729,11 @@ export class FlexServer implements GristServer {
|
|||||||
recordSignUpEvent: true,
|
recordSignUpEvent: true,
|
||||||
}});
|
}});
|
||||||
|
|
||||||
|
if (process.env.GRIST_SINGLE_ORG) {
|
||||||
|
// Merged org is not meaningful in this case.
|
||||||
|
return res.redirect(this.getHomeUrl(req));
|
||||||
|
}
|
||||||
|
|
||||||
// Redirect to teams page if users has access to more than one org. Otherwise, redirect to
|
// Redirect to teams page if users has access to more than one org. Otherwise, redirect to
|
||||||
// personal org.
|
// personal org.
|
||||||
const domain = mreq.org;
|
const domain = mreq.org;
|
||||||
|
75
app/server/lib/ForwardAuthLogin.ts
Normal file
75
app/server/lib/ForwardAuthLogin.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { ApiError } from 'app/common/ApiError';
|
||||||
|
import { getRequestProfile } from 'app/server/lib/Authorizer';
|
||||||
|
import { expressWrap } from 'app/server/lib/expressWrap';
|
||||||
|
import { GristLoginSystem, GristServer, setUserInSession } from 'app/server/lib/GristServer';
|
||||||
|
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
|
||||||
|
* does authentication and then passes identity in a header. An example
|
||||||
|
* of such middleware is traefik-forward-auth:
|
||||||
|
*
|
||||||
|
* https://github.com/thomseddon/traefik-forward-auth
|
||||||
|
*
|
||||||
|
* To make it function:
|
||||||
|
* - Set GRIST_FORWARD_AUTH_HEADER to a header that will contain
|
||||||
|
* authorized user emails, say "x-forwarded-user"
|
||||||
|
* - Make sure /auth/login is processed by forward auth middleware
|
||||||
|
* - Set GRIST_FORWARD_AUTH_LOGOUT_PATH to a path that will trigger
|
||||||
|
* a logout (for traefik-forward-auth by default that is /_oauth/logout).
|
||||||
|
* - Make sure that logout path is processed by forward auth middleware
|
||||||
|
* - 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.
|
||||||
|
* - Optionally, tell the middleware where to forward back to after logout.
|
||||||
|
* (For traefik-forward-auth, you'd set LOGOUT_REDIRECT to .../signed-out)
|
||||||
|
*
|
||||||
|
* Redirection logic currently assumes a single-site installation.
|
||||||
|
*/
|
||||||
|
export async function getForwardAuthLoginSystem(): Promise<GristLoginSystem|undefined> {
|
||||||
|
const header = process.env.GRIST_FORWARD_AUTH_HEADER;
|
||||||
|
const logoutPath = process.env.GRIST_FORWARD_AUTH_LOGOUT_PATH || '';
|
||||||
|
if (!header) { return; }
|
||||||
|
return {
|
||||||
|
async getMiddleware(gristServer: GristServer) {
|
||||||
|
async function getLoginRedirectUrl(req: express.Request, url: URL) {
|
||||||
|
const target = new URL(trimEnd(gristServer.getHomeUrl(req), '/') + "/auth/login");
|
||||||
|
// In lieu of sanatizing the next url, we include only the path
|
||||||
|
// component. This will only work for single-domain installations.
|
||||||
|
target.searchParams.append('next', url.pathname);
|
||||||
|
return target.href;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
getLoginRedirectUrl,
|
||||||
|
getSignUpRedirectUrl: getLoginRedirectUrl,
|
||||||
|
async getLogoutRedirectUrl(req: express.Request) {
|
||||||
|
return trimEnd(gristServer.getHomeUrl(req), '/') + '/' +
|
||||||
|
trimStart(logoutPath, '/');
|
||||||
|
},
|
||||||
|
async addEndpoints(app: express.Express) {
|
||||||
|
app.get('/auth/login', expressWrap(async (req, res) => {
|
||||||
|
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));
|
||||||
|
const next = optStringParam(req.query.next);
|
||||||
|
if (next) {
|
||||||
|
target.pathname = next;
|
||||||
|
}
|
||||||
|
res.redirect(target.href);
|
||||||
|
}));
|
||||||
|
return "forward-auth";
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async deleteUser() {
|
||||||
|
// If we could delete the user account in the external
|
||||||
|
// authentication system, this is our chance - but we can't.
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import { GristLoadConfig } from 'app/common/gristUrls';
|
import { GristLoadConfig } from 'app/common/gristUrls';
|
||||||
import { FullUser } from 'app/common/UserAPI';
|
import { FullUser, UserProfile } from 'app/common/UserAPI';
|
||||||
import { Document } from 'app/gen-server/entity/Document';
|
import { Document } from 'app/gen-server/entity/Document';
|
||||||
import { Organization } from 'app/gen-server/entity/Organization';
|
import { Organization } from 'app/gen-server/entity/Organization';
|
||||||
import { Workspace } from 'app/gen-server/entity/Workspace';
|
import { Workspace } from 'app/gen-server/entity/Workspace';
|
||||||
@ -12,6 +12,7 @@ import { IDocStorageManager } from 'app/server/lib/IDocStorageManager';
|
|||||||
import { INotifier } from 'app/server/lib/INotifier';
|
import { INotifier } from 'app/server/lib/INotifier';
|
||||||
import { IPermitStore } from 'app/server/lib/Permit';
|
import { IPermitStore } from 'app/server/lib/Permit';
|
||||||
import { ISendAppPageOptions } from 'app/server/lib/sendAppPage';
|
import { ISendAppPageOptions } from 'app/server/lib/sendAppPage';
|
||||||
|
import { fromCallback } from 'app/server/lib/serverUtils';
|
||||||
import { Sessions } from 'app/server/lib/Sessions';
|
import { Sessions } from 'app/server/lib/Sessions';
|
||||||
import * as express from 'express';
|
import * as express from 'express';
|
||||||
|
|
||||||
@ -59,6 +60,20 @@ export interface GristLoginMiddleware {
|
|||||||
addEndpoints(app: express.Express): Promise<string>;
|
addEndpoints(app: express.Express): Promise<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the user in the current session.
|
||||||
|
*/
|
||||||
|
export async function setUserInSession(req: express.Request, gristServer: GristServer, profile: UserProfile) {
|
||||||
|
const scopedSession = gristServer.getSessions().getOrCreateSessionFromRequest(req);
|
||||||
|
// Make sure session is up to date before operating on it.
|
||||||
|
// Behavior on a completely fresh session is a little awkward currently.
|
||||||
|
const reqSession = (req as any).session;
|
||||||
|
if (reqSession?.save) {
|
||||||
|
await fromCallback(cb => reqSession.save(cb));
|
||||||
|
}
|
||||||
|
await scopedSession.updateUserProfile(req, profile);
|
||||||
|
}
|
||||||
|
|
||||||
export interface RequestWithGrist extends express.Request {
|
export interface RequestWithGrist extends express.Request {
|
||||||
gristServer?: GristServer;
|
gristServer?: GristServer;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { UserProfile } from 'app/common/UserAPI';
|
import { UserProfile } from 'app/common/UserAPI';
|
||||||
import {GristLoginSystem, GristServer} from 'app/server/lib/GristServer';
|
import { GristLoginSystem, GristServer, setUserInSession } from 'app/server/lib/GristServer';
|
||||||
import {fromCallback} from 'app/server/lib/serverUtils';
|
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -11,18 +10,16 @@ export async function getMinimalLoginSystem(): Promise<GristLoginSystem> {
|
|||||||
// no nuance here.
|
// no nuance here.
|
||||||
return {
|
return {
|
||||||
async getMiddleware(gristServer: GristServer) {
|
async getMiddleware(gristServer: GristServer) {
|
||||||
|
async function getLoginRedirectUrl(req: Request, url: URL) {
|
||||||
|
await setUserInSession(req, gristServer, getDefaultProfile());
|
||||||
|
return url.href;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
async getLoginRedirectUrl(req: Request, url: URL) {
|
getLoginRedirectUrl,
|
||||||
await setSingleUser(req, gristServer);
|
getSignUpRedirectUrl: getLoginRedirectUrl,
|
||||||
return url.href;
|
|
||||||
},
|
|
||||||
async getLogoutRedirectUrl(req: Request, url: URL) {
|
async getLogoutRedirectUrl(req: Request, url: URL) {
|
||||||
return url.href;
|
return url.href;
|
||||||
},
|
},
|
||||||
async getSignUpRedirectUrl(req: Request, url: URL) {
|
|
||||||
await setSingleUser(req, gristServer);
|
|
||||||
return url.href;
|
|
||||||
},
|
|
||||||
async addEndpoints() {
|
async addEndpoints() {
|
||||||
// If working without a login system, make sure default user exists.
|
// If working without a login system, make sure default user exists.
|
||||||
const dbManager = gristServer.getHomeDBManager();
|
const dbManager = gristServer.getHomeDBManager();
|
||||||
@ -43,22 +40,6 @@ export async function getMinimalLoginSystem(): Promise<GristLoginSystem> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the user in the current session to the single hard-coded user.
|
|
||||||
*/
|
|
||||||
async function setSingleUser(req: Request, gristServer: GristServer) {
|
|
||||||
const scopedSession = gristServer.getSessions().getOrCreateSessionFromRequest(req);
|
|
||||||
// Make sure session is up to date before operating on it.
|
|
||||||
// Behavior on a completely fresh session is a little awkward currently.
|
|
||||||
const reqSession = (req as any).session;
|
|
||||||
if (reqSession?.save) {
|
|
||||||
await fromCallback(cb => reqSession.save(cb));
|
|
||||||
}
|
|
||||||
await scopedSession.operateOnScopedSession(req, async (user) => Object.assign(user, {
|
|
||||||
profile: getDefaultProfile()
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDefaultProfile(): UserProfile {
|
function getDefaultProfile(): UserProfile {
|
||||||
return {
|
return {
|
||||||
email: process.env.GRIST_DEFAULT_EMAIL || 'you@example.com',
|
email: process.env.GRIST_DEFAULT_EMAIL || 'you@example.com',
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
|
import { getForwardAuthLoginSystem } from 'app/server/lib/ForwardAuthLogin';
|
||||||
import { GristLoginSystem } from 'app/server/lib/GristServer';
|
import { GristLoginSystem } from 'app/server/lib/GristServer';
|
||||||
import { getMinimalLoginSystem } from 'app/server/lib/MinimalLogin';
|
import { getMinimalLoginSystem } from 'app/server/lib/MinimalLogin';
|
||||||
import { getSamlLoginSystem } from 'app/server/lib/SamlConfig';
|
import { getSamlLoginSystem } from 'app/server/lib/SamlConfig';
|
||||||
|
|
||||||
export async function getLoginSystem(): Promise<GristLoginSystem> {
|
export async function getLoginSystem(): Promise<GristLoginSystem> {
|
||||||
const saml = await getSamlLoginSystem();
|
return await getSamlLoginSystem() ||
|
||||||
if (saml) { return saml; }
|
await getForwardAuthLoginSystem() ||
|
||||||
return getMinimalLoginSystem();
|
await getMinimalLoginSystem();
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user