From 922e1452adae44bed2aa9655be16e19796acb39b Mon Sep 17 00:00:00 2001 From: Hunter Perrin Date: Fri, 31 Mar 2023 14:32:38 -0700 Subject: [PATCH] feat: add ability to tell tilmeld to log in a specific user during authentication --- packages/server/README.md | 74 ++++++++++++++++++++++++++- packages/tilmeld-setup/README.md | 3 +- packages/tilmeld/src/Tilmeld.ts | 88 ++++++++++++++++++++++++-------- 3 files changed, 141 insertions(+), 24 deletions(-) diff --git a/packages/server/README.md b/packages/server/README.md index 3a73089b..9ad1c288 100644 --- a/packages/server/README.md +++ b/packages/server/README.md @@ -46,7 +46,7 @@ app.listen(80); You will need to import any entities you use on the server, so they are available to Nymph. -Now you can configure your client using your server's address (and the optional path, if set). +Now you can configure your **client**, using your server's address (and the optional path, if set). ```ts import { Nymph } from '@nymphjs/client'; @@ -61,6 +61,78 @@ const nymph = new Nymph({ const MyEntity = nymph.addEntityClass(MyEntityClass); ``` +The REST server will authenticate for you using the Tilmeld auth cookie and XSRF token, but if you need to, you can authenticate in some other way (like OAuth2), and place the user in `response.locals.user`. + +```ts +import express from 'express'; +import { Nymph } from '@nymphjs/nymph'; +import SQLite3Driver from '@nymphjs/driver-sqlite3'; +import { Tilmeld } from '@nymphjs/tilmeld'; +import createServer from '@nymphjs/server'; +import setup from '@nymphjs/tilmeld-setup'; + +// Import all the entities you will be using on the server. +import MyEntityClass from './entities/MyEntity'; + +// Consfigure Tilmeld. +const tilmeld = new Tilmeld({ + appName: 'My Awesome App', + appUrl: 'https://mydomain.tld', + setupPath: '/user', +}); + +// Configure Nymph. +const nymph = new Nymph( + {}, + new SQLite3Driver({ + filename: ':memory:', + }), + tilmeld +); +const MyEntity = nymph.addEntityClass(MyEntityClass); + +// Create your Express app. +const app = express(); + +// Authenticate the user manually. +app.use('/rest', async (request, response, next) => { + const { User } = tilmeld; + + try { + // Somehow authenticate the user... + const user = await User.factoryUsername(username); + + if (user.guid != null && user.enabled) { + response.locals.user = user; + } + + next(); + } catch (e: any) { + response.status(500); + response.send('Internal server error.'); + } +}); + +// Create and use the REST server (with an optional path). +app.use('/rest', createServer(nymph)); + +// Create Tilmeld setup app. +app.user( + '/user', + setup( + { + restUrl: 'https://mydomain.tld/rest', + }, + nymph + ) +); + +// Do anything else you need to do... + +// Start your server. +app.listen(80); +``` + # License Copyright 2021 SciActive Inc diff --git a/packages/tilmeld-setup/README.md b/packages/tilmeld-setup/README.md index 77b1e0f8..d4e9c696 100644 --- a/packages/tilmeld-setup/README.md +++ b/packages/tilmeld-setup/README.md @@ -53,7 +53,8 @@ const app = express(); // Use the REST server. app.use('/rest', createServer(nymph)); -// Use the Tilmeld Setup App, passing in the Nymph Client Config. + +// Create Tilmeld setup app. app.use( '/user', setup( diff --git a/packages/tilmeld/src/Tilmeld.ts b/packages/tilmeld/src/Tilmeld.ts index 170fa663..7179de29 100644 --- a/packages/tilmeld/src/Tilmeld.ts +++ b/packages/tilmeld/src/Tilmeld.ts @@ -960,6 +960,15 @@ export default class Tilmeld implements TilmeldInterface { /** * Check for a TILMELDAUTH token, and, if set, authenticate from it. * + * You can also call this function after setting `response.locals.user` to the + * user you want to authenticate. You *should* check for `user.enabled` before + * setting this variable, unless you explicitly want to log in as a disabled + * user. (The user must be an instance of the User class for this Tilmeld + * instance.) + * + * This function will set `response.locals.user` to the logged in user on + * successful authentication. + * * @param skipXsrfToken Skip the XSRF token check. * @returns True if a user was authenticated, false on any failure. */ @@ -968,6 +977,17 @@ export default class Tilmeld implements TilmeldInterface { return false; } + if ( + this.response && + this.response.locals.user && + this.response.locals.user instanceof this.User + ) { + // The user has already been authenticated through some other means. + const user = this.response.locals.user; + this.fillSession(user); + return true; + } + const cookies = this.request.cookies ?? {}; // If a client does't support cookies, they can use the X-TILMELDAUTH header @@ -1022,6 +1042,11 @@ export default class Tilmeld implements TilmeldInterface { } else { this.fillSession(user); } + + if (this.response) { + this.response.locals.user = user; + } + return true; } @@ -1029,26 +1054,40 @@ export default class Tilmeld implements TilmeldInterface { * Logs the given user into the system. * * @param user The user. - * @param sendAuthHeader When true, a custom header with the auth token will be sent. + * @param sendAuthHeader Send the auth token as a custom header. + * @param sendCookie Send the auth token as a cookie. * @returns True on success, false on failure. */ - public login(user: User & UserData, sendAuthHeader: boolean) { + public login( + user: User & UserData, + sendAuthHeader: boolean, + sendCookie = true + ) { if (user.guid != null && user.enabled) { - if (this.response && !this.response.headersSent) { - const token = this.config.jwtBuilder(this.config, user); - const appUrl = new URL(this.config.appUrl); - this.response.cookie('TILMELDAUTH', token, { - domain: this.config.cookieDomain, - path: this.config.cookiePath, - maxAge: this.config.jwtExpire * 1000, - secure: appUrl.protocol === 'https', - httpOnly: false, // Allow JS access (for CSRF protection). - sameSite: appUrl.protocol === 'https' ? 'lax' : 'strict', - }); - if (sendAuthHeader) { - this.response.set('X-TILMELDAUTH', token); + if (this.response) { + if (!this.response.headersSent) { + const token = this.config.jwtBuilder(this.config, user); + + if (sendCookie) { + const appUrl = new URL(this.config.appUrl); + this.response.cookie('TILMELDAUTH', token, { + domain: this.config.cookieDomain, + path: this.config.cookiePath, + maxAge: this.config.jwtExpire * 1000, + secure: appUrl.protocol === 'https', + httpOnly: false, // Allow JS access (for CSRF protection). + sameSite: appUrl.protocol === 'https' ? 'lax' : 'strict', + }); + } + + if (sendAuthHeader) { + this.response.set('X-TILMELDAUTH', token); + } } + + this.response.locals.user = user; } + this.fillSession(user); return true; } @@ -1057,15 +1096,20 @@ export default class Tilmeld implements TilmeldInterface { /** * Logs the current user out of the system. + * + * @param clearCookie Clear the auth cookie. (Also send a header.) */ - public logout() { + public logout(clearCookie = true) { this.clearSession(); - if (this.response && !this.response.headersSent) { - this.response.clearCookie('TILMELDAUTH', { - domain: this.config.cookieDomain, - path: this.config.cookiePath, - }); - this.response.set('X-TILMELDAUTH', ''); + if (this.response) { + if (clearCookie && !this.response.headersSent) { + this.response.clearCookie('TILMELDAUTH', { + domain: this.config.cookieDomain, + path: this.config.cookiePath, + }); + this.response.set('X-TILMELDAUTH', ''); + } + this.response.locals.user = null; } }