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 9, 2023
1 parent 04d9416 commit 633fdb0
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 10 deletions.
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;

Check failure on line 7 in src/authentication/refresh.handler.ts

View workflow job for this annotation

GitHub Actions / Test and Publish

Property 'refreshTokenPath' does not exist on type 'AdminJSOptionsWithDefault'.
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";

Check failure on line 1 in src/types.ts

View workflow job for this annotation

GitHub Actions / Test and Publish

Module '"adminjs"' has no exported member 'BaseAuthProvider'. Did you mean to use 'import BaseAuthProvider from "adminjs"' instead?
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;
};

0 comments on commit 633fdb0

Please sign in to comment.