Skip to content

Commit

Permalink
feat: add support for auth providers
Browse files Browse the repository at this point in the history
  • Loading branch information
dziraf committed Nov 21, 2023
1 parent 04d9416 commit cd85751
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 72 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
39 changes: 32 additions & 7 deletions src/authentication/login.handler.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);

Check warning on line 56 in src/authentication/login.handler.ts

View workflow job for this annotation

GitHub Actions / Test and Publish

Forbidden non-null assertion
}

if (adminUser) {
req.session.set('adminUser', adminUser);

Expand All @@ -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);
Expand Down
9 changes: 8 additions & 1 deletion src/authentication/logout.handler.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);
Expand Down
61 changes: 61 additions & 0 deletions src/authentication/refresh.handler.ts
Original file line number Diff line number Diff line change
@@ -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<CurrentAdmin> | 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);
});
});
};
25 changes: 24 additions & 1 deletion src/buildAuthenticatedRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -51,6 +58,21 @@ export const buildAuthenticatedRouter = async (
fastifyApp: FastifyInstance,
sessionOptions?: FastifySessionPlugin.FastifySessionOptions
): Promise<void> => {
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,
});
Expand All @@ -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);
};
17 changes: 16 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -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;
};
72 changes: 12 additions & 60 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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/[email protected]":
version "1.20.2"
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975"
Expand Down Expand Up @@ -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"
Expand All @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit cd85751

Please sign in to comment.