Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for auth providers #18

Merged
merged 1 commit into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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 @@
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
Loading