UNPKG

keystone-6-oauth

Version:

Keystone6 Plugin that enables social logins such as Google, Twitter, Github, Facebook and others.

464 lines (437 loc) 16.1 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _objectSpread = require('@babel/runtime/helpers/objectSpread2'); var _objectWithoutProperties = require('@babel/runtime/helpers/objectWithoutProperties'); var _includesInstanceProperty = require('@babel/runtime-corejs3/core-js-stable/instance/includes'); var _mapInstanceProperty = require('@babel/runtime-corejs3/core-js-stable/instance/map'); var _JSON$stringify = require('@babel/runtime-corejs3/core-js-stable/json/stringify'); var _startsWithInstanceProperty = require('@babel/runtime-corejs3/core-js-stable/instance/starts-with'); var cookie = require('cookie'); var react = require('next-auth/react'); var jwt = require('next-auth/jwt'); var ejs = require('ejs'); var _filterInstanceProperty = require('@babel/runtime-corejs3/core-js-stable/instance/filter'); var core = require('@keystone-6/core'); var url = require('url'); function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; } function _interopNamespace(e) { if (e && e.__esModule) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n["default"] = e; return Object.freeze(n); } var _includesInstanceProperty__default = /*#__PURE__*/_interopDefault(_includesInstanceProperty); var _mapInstanceProperty__default = /*#__PURE__*/_interopDefault(_mapInstanceProperty); var _JSON$stringify__default = /*#__PURE__*/_interopDefault(_JSON$stringify); var _startsWithInstanceProperty__default = /*#__PURE__*/_interopDefault(_startsWithInstanceProperty); var cookie__namespace = /*#__PURE__*/_interopNamespace(cookie); var ejs__default = /*#__PURE__*/_interopDefault(ejs); var _filterInstanceProperty__default = /*#__PURE__*/_interopDefault(_filterInstanceProperty); var url__default = /*#__PURE__*/_interopDefault(url); const template$1 = ` const keystoneConfig = require('@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/next-config').config; module.exports = { ...keystoneConfig, basePath: '<%= keystonePath || '' %>' }; `; const nextConfigTemplate = _ref => { let { keystonePath } = _ref; const nextConfigOut = ejs__default["default"].render(template$1, { keystonePath }); return nextConfigOut; }; const template = ` import { getContext } from '@keystone-6/core/context'; import getNextAuthPage from 'keystone-6-oauth/pages/NextAuthPage'; import keystoneConfig from '../../../../../keystone'; import * as PrismaModule from '.prisma/client'; const keystoneContext = global.keystoneContext || getContext(keystoneConfig, PrismaModule); if (process.env.NODE_ENV !== 'production') globalThis.keystoneContext = keystoneContext export default getNextAuthPage({ autoCreate: <%= autoCreate %>, context: keystoneContext, identityField: '<%= identityField %>', listKey: '<%= listKey %>', onSignIn: keystoneConfig.onSignIn, onSignUp: keystoneConfig.onSignUp, pages: keystoneConfig.pages, providers: keystoneConfig.providers, sessionData: '<%= sessionData %>', sessionSecret: '<%= sessionSecret %>', }); `; const authTemplate = _ref => { let { autoCreate, identityField, listKey, sessionData, sessionSecret } = _ref; const authOut = ejs__default["default"].render(template, { autoCreate, identityField, listKey, sessionData, sessionSecret }); return authOut; }; function getBaseAuthSchema(_ref) { let { listKey, base } = _ref; const extension = { query: { authenticatedItem: core.graphql.field({ type: core.graphql.union({ name: 'AuthenticatedItem', types: [base.object(listKey)], resolveType: (root, context) => { var _context$session; return (_context$session = context.session) === null || _context$session === void 0 ? void 0 : _context$session.listKey; } }), resolve(root, args, _ref2) { let { session, db } = _ref2; if (typeof (session === null || session === void 0 ? void 0 : session.itemId) === 'string' && typeof session.listKey === 'string') { return db[session.listKey].findOne({ where: { id: session.itemId } }); } return null; } }) } }; return { extension }; } const getSchemaExtension = _ref => { let { listKey } = _ref; return core.graphql.extend(base => { var _context; const baseSchema = getBaseAuthSchema({ base, listKey }); return _filterInstanceProperty__default["default"](_context = [baseSchema.extension]).call(_context, x => x !== undefined); }); }; const _excluded = ["get", "end"]; /** * createAuth function * * Generates config for Keystone to implement standard auth features. */ function createAuth(_ref) { let { autoCreate, context, cookies, identityField, listKey, keystonePath, onSignIn, onSignUp, pages, providers, sessionData, sessionSecret } = _ref; // The protectIdentities flag is currently under review to see whether it should be // part of the createAuth API (in which case its use cases need to be documented and tested) // or whether always being true is what we want, in which case we can refactor our code // to match this. -TL const customPath = !keystonePath || keystonePath === '/' ? '' : keystonePath; /** * pageMiddleware * * Should be added to the ui.pageMiddleware stack. * * Redirects: * - from the signin or init pages to the index when a valid session is present * - to the init page when initFirstItem is configured, and there are no user in the database * - to the signin page when no valid session is present */ // TODO: [TYPES] Check pageMiddleware const authMiddleware = async _ref2 => { let { context, isValidSession } = _ref2; const { req, session } = context; const pathname = url__default["default"].parse(req === null || req === void 0 ? void 0 : req.url).pathname; if (isValidSession) { if (customPath !== '' && pathname === '/') { return { kind: 'redirect', to: `${customPath}` }; } return null; } if (!session && !_includesInstanceProperty__default["default"](pathname).call(pathname, `${customPath}/api/auth/`)) { return { kind: 'redirect', to: (pages === null || pages === void 0 ? void 0 : pages.signIn) || `${customPath}/api/auth/signin` }; } }; /** * authGetAdditionalFiles * * This function adds files to be generated into the Admin UI build. Must be added to the * ui.getAdditionalFiles config. * * The sign-in page is always included, and the init page is included when initFirstItem is set */ const authGetAdditionalFiles = () => { const filesToWrite = [{ mode: 'write', outputPath: 'pages/api/auth/[...nextauth].js', src: authTemplate({ autoCreate, identityField, listKey, sessionData, sessionSecret }) }, { mode: 'write', outputPath: 'next.config.js', src: nextConfigTemplate({ keystonePath: customPath }) }]; return filesToWrite; }; /** * publicAuthPages * * Must be added to the ui.publicPages config */ const authPublicPages = [`${customPath}/api/__keystone_api_build`, `${customPath}/api/auth/csrf`, `${customPath}/api/auth/signin`, `${customPath}/api/auth/callback`, `${customPath}/api/auth/session`, `${customPath}/api/auth/providers`, `${customPath}/api/auth/signout`, `${customPath}/api/auth/error`]; // TODO: [TYPES] Add Provider // @ts-ignore function addPages(provider) { const name = provider.id; authPublicPages.push(`${customPath}/api/auth/signin/${name}`); authPublicPages.push(`${customPath}/api/auth/callback/${name}`); } _mapInstanceProperty__default["default"](providers).call(providers, addPages); /** * extendGraphqlSchema * * Must be added to the extendGraphqlSchema config. Can be composed. */ const extendGraphqlSchema = getSchemaExtension({ identityField, listKey }); /** * validateConfig * * Validates the provided auth config; optional step when integrating auth */ const validateConfig = keystoneConfig => { const listConfig = keystoneConfig.lists[listKey]; if (listConfig === undefined) { const msg = `A createAuth() invocation specifies the list "${listKey}" but no list with that key has been defined.`; throw new Error(msg); } // TODO: Check for String-like typing for identityField? How? // TODO: Validate that the identifyField is unique. // TODO: If this field isn't required, what happens if I try to log in as `null`? const identityFieldConfig = listConfig.fields[identityField]; if (identityFieldConfig === undefined) { const identityFieldName = _JSON$stringify__default["default"](identityField); const msg = `A createAuth() invocation for the "${listKey}" list specifies ${identityFieldName} as its identityField but no field with that key exists on the list.`; throw new Error(msg); } }; /** * withItemData * * Automatically injects a session.data value with the authenticated item */ /* TODO: - [ ] We could support additional where input to validate item sessions (e.g an isEnabled boolean) */ const withItemData = _sessionStrategy => { const { get, end } = _sessionStrategy, sessionStrategy = _objectWithoutProperties(_sessionStrategy, _excluded); return _objectSpread(_objectSpread({}, sessionStrategy), {}, { end: async _ref3 => { let { context } = _ref3; await end({ context }); const TOKEN_NAME = process.env.NODE_ENV === 'development' ? 'next-auth.session-token' : '__Secure-next-auth.session-token'; const { req, res } = context; if (!req || !res) return; res.setHeader('Set-Cookie', cookie__namespace.serialize(TOKEN_NAME, '', { // TODO: Update parse to URL domain: url__default["default"].parse(req.url).hostname, expires: new Date(), httpOnly: true, maxAge: 0, path: '/', sameSite: 'lax', secure: process.env.NODE_ENV === 'production' })); }, // TODO: [TYPES] Add get typing // @ts-ignore get: async _ref4 => { var _req$headers, _req$headers$authoriz; let { context } = _ref4; const { req } = context; const pathname = url__default["default"].parse(req === null || req === void 0 ? void 0 : req.url).pathname; // TODO let nextSession = null; if (!req) return; if (_includesInstanceProperty__default["default"](pathname).call(pathname, '/api/auth')) { return null; } const sudoContext = context.sudo(); if (((_req$headers = req.headers) === null || _req$headers === void 0 ? void 0 : (_req$headers$authoriz = _req$headers.authorization) === null || _req$headers$authoriz === void 0 ? void 0 : _req$headers$authoriz.split(' ')[0]) === 'Bearer') { nextSession = await jwt.getToken({ req: req, secret: sessionSecret }); } else { nextSession = await react.getSession({ req }); // TODO: [TYPES] Review nextSession } if (!nextSession || !nextSession.listKey || nextSession.listKey !== listKey || !nextSession.itemId || !sudoContext.query[listKey] || !nextSession.itemId) { return null; } const reqWithUser = req; reqWithUser.user = { data: nextSession.data, itemId: nextSession.itemId, listKey: nextSession.listKey }; const userSession = await get({ context }); return _objectSpread(_objectSpread(_objectSpread({}, userSession), nextSession), {}, { data: nextSession.data, itemId: nextSession.itemId, listKey: nextSession.listKey }); } }); }; function defaultIsAccessAllowed(_ref5) { let { session } = _ref5; return session !== undefined; } /** * withAuth * * Automatically extends config with the correct auth functionality. This is the easiest way to * configure auth for keystone; you should probably use it unless you want to extend or replace * the way auth is set up with custom functionality. * * It validates the auth config against the provided keystone config, and preserves existing * config by composing existing extendGraphqlSchema functions and ui config. */ const withAuth = keystoneConfig => { var _ui; validateConfig(keystoneConfig); let { ui } = keystoneConfig; if (!((_ui = ui) !== null && _ui !== void 0 && _ui.isDisabled)) { const { getAdditionalFiles = [], isAccessAllowed = defaultIsAccessAllowed, pageMiddleware, publicPages = [] } = ui || {}; ui = _objectSpread(_objectSpread({}, ui), {}, { publicPages: [...publicPages, ...authPublicPages], isAccessAllowed: async context => { var _context$req; const pathname = url__default["default"].parse((_context$req = context.req) === null || _context$req === void 0 ? void 0 : _context$req.url).pathname; if (_startsWithInstanceProperty__default["default"](pathname).call(pathname, `${customPath}/_next`) || _startsWithInstanceProperty__default["default"](pathname).call(pathname, `${customPath}/__next`) || _startsWithInstanceProperty__default["default"](pathname).call(pathname, `${customPath}/api/auth/`) || pages !== null && pages !== void 0 && pages.signIn && _includesInstanceProperty__default["default"](pathname).call(pathname, pages === null || pages === void 0 ? void 0 : pages.signIn) || pages !== null && pages !== void 0 && pages.error && _includesInstanceProperty__default["default"](pathname).call(pathname, pages === null || pages === void 0 ? void 0 : pages.error) || pages !== null && pages !== void 0 && pages.signOut && _includesInstanceProperty__default["default"](pathname).call(pathname, pages === null || pages === void 0 ? void 0 : pages.signOut)) { return true; } return await isAccessAllowed(context); }, getAdditionalFiles: [...getAdditionalFiles, authGetAdditionalFiles], pageMiddleware: async args => { if (!authMiddleware) throw new Error('Missing authMiddleware'); const shouldRedirect = await authMiddleware(args); if (shouldRedirect) return shouldRedirect; return pageMiddleware === null || pageMiddleware === void 0 ? void 0 : pageMiddleware(args); } }); } if (!keystoneConfig.session) throw new TypeError('Missing .session configuration'); const session = withItemData(keystoneConfig.session); const existingExtendGraphQLSchema = keystoneConfig.extendGraphqlSchema; return _objectSpread(_objectSpread({}, keystoneConfig), {}, { context, cookies, extendGraphqlSchema: existingExtendGraphQLSchema // TODO: [TYPES] Add schema ? schema => existingExtendGraphQLSchema(extendGraphqlSchema(schema)) : extendGraphqlSchema, lists: _objectSpread({}, keystoneConfig.lists), onSignIn, onSignUp, pages, providers, session, ui }); }; return { withAuth }; } exports.createAuth = createAuth;