From cd857514261eccaac86094584a0428d8b61ee7e6 Mon Sep 17 00:00:00 2001 From: Rafal Dziegielewski Date: Thu, 9 Nov 2023 12:24:31 +0100 Subject: [PATCH] feat: add support for auth providers --- package.json | 4 +- src/authentication/login.handler.ts | 39 ++++++++++++--- src/authentication/logout.handler.ts | 9 +++- src/authentication/refresh.handler.ts | 61 +++++++++++++++++++++++ src/buildAuthenticatedRouter.ts | 25 +++++++++- src/types.ts | 17 ++++++- yarn.lock | 72 +++++---------------------- 7 files changed, 155 insertions(+), 72 deletions(-) create mode 100644 src/authentication/refresh.handler.ts diff --git a/package.json b/package.json index 5949171..a17d480 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "check:all": "yarn lint && yarn build" }, "peerDependencies": { - "adminjs": "^7.0.0" + "adminjs": "^7.4.0" }, "devDependencies": { "@semantic-release/git": "^10.0.1", @@ -29,7 +29,7 @@ "@types/node": "^18.15.3", "@typescript-eslint/eslint-plugin": "^5.56.0", "@typescript-eslint/parser": "^5.56.0", - "adminjs": "^7.0.0", + "adminjs": "^7.4.0", "eslint": "^8.36.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-import": "^2.27.5", diff --git a/src/authentication/login.handler.ts b/src/authentication/login.handler.ts index afc0ab6..04cb81d 100644 --- a/src/authentication/login.handler.ts +++ b/src/authentication/login.handler.ts @@ -1,7 +1,7 @@ import AdminJS from 'adminjs'; import { FastifyInstance } from 'fastify'; -import { AuthenticationOptions } from '../types.js'; +import { AuthenticationContext, AuthenticationOptions } from '../types.js'; const getLoginPath = (admin: AdminJS): string => { const { loginPath } = admin.options; @@ -17,21 +17,45 @@ export const withLogin = ( const { rootPath } = admin.options; const loginPath = getLoginPath(admin); + const { provider } = auth; + const providerProps = provider?.getUiProps?.() ?? {}; + fastifyInstance.get(loginPath, async (req, reply) => { - const login = await admin.renderLogin({ + const baseProps = { action: admin.options.loginPath, errorMessage: null, + }; + const login = await admin.renderLogin({ + ...baseProps, + ...providerProps, }); reply.type('text/html'); reply.send(login); }); fastifyInstance.post(loginPath, async (req, reply) => { - const { email, password } = req.body as { - email: string; - password: string; - }; - const adminUser = await auth.authenticate(email, password); + const context: AuthenticationContext = { request: req, reply }; + + let adminUser; + if (provider) { + adminUser = await provider.handleLogin( + { + headers: req.headers, + query: req.query ?? {}, + params: req.params ?? {}, + data: req.body ?? {}, + }, + context + ); + } else { + const { email, password } = req.body as { + email: string; + password: string; + }; + // "auth.authenticate" must always be defined if "auth.provider" isn't + adminUser = await auth.authenticate!(email, password, context); + } + if (adminUser) { req.session.set('adminUser', adminUser); @@ -44,6 +68,7 @@ export const withLogin = ( const login = await admin.renderLogin({ action: admin.options.loginPath, errorMessage: 'invalidCredentials', + ...providerProps, }); reply.type('text/html'); reply.send(login); diff --git a/src/authentication/logout.handler.ts b/src/authentication/logout.handler.ts index de46ddd..02461a7 100644 --- a/src/authentication/logout.handler.ts +++ b/src/authentication/logout.handler.ts @@ -1,5 +1,6 @@ import AdminJS from 'adminjs'; import { FastifyInstance } from 'fastify'; +import { AuthenticationOptions } from '../types.js'; const getLogoutPath = (admin: AdminJS) => { const { logoutPath } = admin.options; @@ -9,11 +10,17 @@ const getLogoutPath = (admin: AdminJS) => { export const withLogout = ( fastifyApp: FastifyInstance, - admin: AdminJS + admin: AdminJS, + auth: AuthenticationOptions, ): void => { const logoutPath = getLogoutPath(admin); + const { provider } = auth; fastifyApp.get(logoutPath, async (request, reply) => { + if (provider) { + await provider.handleLogout({ request, reply }); + } + if (request.session) { request.session.destroy(() => { reply.redirect(admin.options.loginPath); diff --git a/src/authentication/refresh.handler.ts b/src/authentication/refresh.handler.ts new file mode 100644 index 0000000..b360593 --- /dev/null +++ b/src/authentication/refresh.handler.ts @@ -0,0 +1,61 @@ +import AdminJS, { CurrentAdmin } from "adminjs"; +import { FastifyInstance } from "fastify"; +import { AuthenticationOptions } from "../types.js"; +import { WrongArgumentError } from "../errors.js"; + +const getRefreshTokenPath = (admin: AdminJS) => { + const { refreshTokenPath, rootPath } = admin.options; + const normalizedRefreshTokenPath = refreshTokenPath.replace(rootPath, ""); + + return normalizedRefreshTokenPath.startsWith("/") + ? normalizedRefreshTokenPath + : `/${normalizedRefreshTokenPath}`; +}; + +const MISSING_PROVIDER_ERROR = + '"provider" has to be configured to use refresh token mechanism'; + +export const withRefresh = ( + fastifyApp: FastifyInstance, + admin: AdminJS, + auth: AuthenticationOptions +): void => { + const refreshTokenPath = getRefreshTokenPath(admin); + + const { provider } = auth; + + fastifyApp.post(refreshTokenPath, async (request, reply) => { + if (!provider) { + throw new WrongArgumentError(MISSING_PROVIDER_ERROR); + } + + const updatedAuthInfo = await provider.handleRefreshToken( + { + data: request.body ?? {}, + query: request.query ?? {}, + params: request.params ?? {}, + headers: request.headers, + }, + { request, reply } + ); + + let admin = request.session.adminUser as Partial | null; + if (!admin) { + admin = {}; + } + + if (!admin._auth) { + admin._auth = {}; + } + + admin._auth = { + ...admin._auth, + ...updatedAuthInfo, + }; + + request.session.set('adminUser', admin); + request.session.save(() => { + reply.send(admin); + }); + }); +}; diff --git a/src/buildAuthenticatedRouter.ts b/src/buildAuthenticatedRouter.ts index 1321f52..3e56f72 100644 --- a/src/buildAuthenticatedRouter.ts +++ b/src/buildAuthenticatedRouter.ts @@ -6,9 +6,16 @@ import { FastifyInstance } from 'fastify'; import { withLogin } from './authentication/login.handler.js'; import { withLogout } from './authentication/logout.handler.js'; +import { withRefresh } from './authentication/refresh.handler.js'; import { withProtectedRoutesHandler } from './authentication/protected-routes.handler.js'; import { buildRouter } from './buildRouter.js'; import { AuthenticationOptions } from './types.js'; +import { WrongArgumentError } from './errors.js'; + +const MISSING_AUTH_CONFIG_ERROR = + 'You must configure either "authenticate" method or assign an auth "provider"'; +const INVALID_AUTH_CONFIG_ERROR = + 'You cannot configure both "authenticate" and "provider". "authenticate" will be removed in next major release.'; /** * @typedef {Function} Authenticate @@ -51,6 +58,21 @@ export const buildAuthenticatedRouter = async ( fastifyApp: FastifyInstance, sessionOptions?: FastifySessionPlugin.FastifySessionOptions ): Promise => { + if (!auth.authenticate && !auth.provider) { + throw new WrongArgumentError(MISSING_AUTH_CONFIG_ERROR); + } + + if (auth.authenticate && auth.provider) { + throw new WrongArgumentError(INVALID_AUTH_CONFIG_ERROR); + } + + if (auth.provider) { + admin.options.env = { + ...admin.options.env, + ...auth.provider.getUiProps(), + }; + } + await fastifyApp.register(fastifyCookie, { secret: auth.cookiePassword, }); @@ -65,5 +87,6 @@ export const buildAuthenticatedRouter = async ( await buildRouter(admin, fastifyApp); withProtectedRoutesHandler(fastifyApp, admin); withLogin(fastifyApp, admin, auth); - withLogout(fastifyApp, admin); + withLogout(fastifyApp, admin, auth); + withRefresh(fastifyApp, admin, auth); }; diff --git a/src/types.ts b/src/types.ts index cb69770..8d0d418 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,20 @@ +import { BaseAuthProvider } from "adminjs"; +import { FastifyReply, FastifyRequest } from "fastify"; + export type AuthenticationOptions = { cookiePassword: string; cookieName?: string; - authenticate: (email: string, password: string) => unknown | null; + authenticate?: (email: string, password: string, context?: AuthenticationContext) => unknown | null; + provider?: BaseAuthProvider; +}; + +export type AuthenticationContext = { + /** + * @description Authentication request object + */ + request: FastifyRequest; + /** + * @description Authentication response object + */ + reply: FastifyReply; }; diff --git a/yarn.lock b/yarn.lock index 4b81cb6..4c56c1a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2817,44 +2817,6 @@ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== -"@types/babel-core@^6.25.7": - version "6.25.7" - resolved "https://registry.yarnpkg.com/@types/babel-core/-/babel-core-6.25.7.tgz#f9c22d5c085686da2f6ffbdae778edb3e6017671" - integrity sha512-WPnyzNFVRo6bxpr7bcL27qXtNKNQ3iToziNBpibaXHyKGWQA0+tTLt73QQxC/5zzbM544ih6Ni5L5xrck6rGwg== - dependencies: - "@types/babel-generator" "*" - "@types/babel-template" "*" - "@types/babel-traverse" "*" - "@types/babel-types" "*" - "@types/babylon" "*" - -"@types/babel-generator@*": - version "6.25.5" - resolved "https://registry.yarnpkg.com/@types/babel-generator/-/babel-generator-6.25.5.tgz#b02723fd589349b05524376e5530228d3675d878" - integrity sha512-lhbwMlAy5rfWG+R6l8aPtJdEFX/kcv6LMFIuvUb0i89ehqgD24je9YcB+0fRspQhgJGlEsUImxpw4pQeKS/+8Q== - dependencies: - "@types/babel-types" "*" - -"@types/babel-template@*": - version "6.25.2" - resolved "https://registry.yarnpkg.com/@types/babel-template/-/babel-template-6.25.2.tgz#3c4cde02dbcbbf461a58d095a9f69f35eabd5f06" - integrity sha512-QKtDQRJmAz3Y1HSxfMl0syIHebMc/NnOeH/8qeD0zjgU2juD0uyC922biMxCy5xjTNvHinigML2l8kxE8eEBmw== - dependencies: - "@types/babel-types" "*" - "@types/babylon" "*" - -"@types/babel-traverse@*": - version "6.25.7" - resolved "https://registry.yarnpkg.com/@types/babel-traverse/-/babel-traverse-6.25.7.tgz#bc75fce23d8394534562a36a32dec94a54d11835" - integrity sha512-BeQiEGLnVzypzBdsexEpZAHUx+WucOMXW6srEWDkl4SegBlaCy+iBvRO+4vz6EZ+BNQg22G4MCdDdvZxf+jW5A== - dependencies: - "@types/babel-types" "*" - -"@types/babel-types@*": - version "7.0.11" - resolved "https://registry.yarnpkg.com/@types/babel-types/-/babel-types-7.0.11.tgz#263b113fa396fac4373188d73225297fb86f19a9" - integrity sha512-pkPtJUUY+Vwv6B1inAz55rQvivClHJxc9aVEPPmaq2cbyeMLCiDpbKpcKyX4LAwpNGi+SHBv0tHv6+0gXv0P2A== - "@types/babel__core@^7.1.14": version "7.20.0" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.0.tgz#61bc5a4cae505ce98e1e36c5445e4bee060d8891" @@ -2888,13 +2850,6 @@ dependencies: "@babel/types" "^7.3.0" -"@types/babylon@*": - version "6.16.6" - resolved "https://registry.yarnpkg.com/@types/babylon/-/babylon-6.16.6.tgz#a1e7e01567b26a5ebad321a74d10299189d8d932" - integrity sha512-G4yqdVlhr6YhzLXFKy5F7HtRBU8Y23+iWy7UKthMq/OSQnL1hbsoeXESQ2LY8zEDlknipDG3nRGhUC9tkwvy/w== - dependencies: - "@types/babel-types" "*" - "@types/busboy@^1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@types/busboy/-/busboy-1.5.0.tgz#62681556cbbd2afc8d2efa6bafaa15602f0838b9" @@ -3032,15 +2987,6 @@ "@types/scheduler" "*" csstype "^3.0.2" -"@types/react@^18.0.28": - version "18.0.28" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.28.tgz#accaeb8b86f4908057ad629a26635fe641480065" - integrity sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew== - dependencies: - "@types/prop-types" "*" - "@types/scheduler" "*" - csstype "^3.0.2" - "@types/resolve@1.20.2": version "1.20.2" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975" @@ -3229,10 +3175,10 @@ acorn@^8.5.0, acorn@^8.8.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== -adminjs@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/adminjs/-/adminjs-7.0.0.tgz#5dad16fcdd91dfe9fd84402b3e109f9fdbb74534" - integrity sha512-6cvr04yhPpoqpK9lfy5ohxHMUI+J9lDZbRScyqzmpPTZ4P8E68unZekixx7nAGXFBmhixP5+CumLNpCNzcUeGA== +adminjs@^7.4.0: + version "7.4.0" + resolved "https://registry.yarnpkg.com/adminjs/-/adminjs-7.4.0.tgz#9551c79ac1b6047f1cc86ac1525e01660fea954a" + integrity sha512-GKot4WNEe5aQN2MLkSR216N0oE9KrpJ+COwPrYhRlF42wUMiQucwQbq36VfMb/ZsiEpF3SfBdSa9Qi6EApR0WQ== dependencies: "@adminjs/design-system" "^4.0.0" "@babel/core" "^7.21.0" @@ -3251,8 +3197,6 @@ adminjs@^7.0.0: "@rollup/plugin-node-resolve" "^15.0.1" "@rollup/plugin-replace" "^5.0.2" "@rollup/plugin-terser" "^0.4.0" - "@types/babel-core" "^6.25.7" - "@types/react" "^18.0.28" axios "^1.3.4" commander "^10.0.0" flat "^5.0.2" @@ -3263,6 +3207,7 @@ adminjs@^7.0.0: ora "^6.2.0" prop-types "^15.8.1" punycode "^2.3.0" + qs "^6.11.1" react "^18.2.0" react-dom "^18.2.0" react-feather "^2.0.10" @@ -8462,6 +8407,13 @@ qrcode-terminal@^0.12.0: resolved "https://registry.yarnpkg.com/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz#bb5b699ef7f9f0505092a3748be4464fe71b5819" integrity sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ== +qs@^6.11.1: + version "6.11.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" + integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA== + dependencies: + side-channel "^1.0.4" + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"