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 8a375ab commit e9fd09b
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 79 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
},
"homepage": "https://github.com/SoftwareBrothers/adminjs-expressjs#readme",
"peerDependencies": {
"adminjs": "^7.0.0",
"adminjs": "^7.4.0",
"express": ">=4.18.2",
"express-formidable": "^1.2.0",
"express-session": ">=1.17.3",
Expand All @@ -60,7 +60,7 @@
"@types/node": "^18.15.3",
"@typescript-eslint/eslint-plugin": "^5.53.0",
"@typescript-eslint/parser": "^5.53.0",
"adminjs": "^7.0.0",
"adminjs": "^7.4.0",
"commitlint": "^17.4.4",
"eslint": "^8.35.0",
"eslint-config-airbnb-base": "^15.0.0",
Expand Down
54 changes: 41 additions & 13 deletions src/authentication/login.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,47 +70,75 @@ export const withLogin = (
const { rootPath } = admin.options;
const loginPath = getLoginPath(admin);

const { provider } = auth;
const providerProps = provider?.getUiProps?.() ?? {};

router.get(loginPath, async (req, res) => {
const login = await admin.renderLogin({
const baseProps = {
action: admin.options.loginPath,
errorMessage: null,
};
const login = await admin.renderLogin({
...baseProps,
...providerProps,
});
res.send(login);

return res.send(login);
});

router.post(loginPath, async (req, res, next) => {
if (!new Retry(req.ip).canLogin(auth.maxRetries)) {
const login = await admin.renderLogin({
action: admin.options.loginPath,
errorMessage: "tooManyRequests",
...providerProps,
});
res.send(login);
return;

return res.send(login);
}
const { email, password } = req.fields as {
email: string;
password: string;
};

const context: AuthenticationContext = { req, res };
const adminUser = await auth.authenticate(email, password, context);

let adminUser;
if (provider) {
adminUser = await provider.handleLogin(
{
headers: req.headers,
query: req.query,
params: req.params,
data: req.fields ?? {},
},
context
);
} else {
const { email, password } = req.fields 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 119 in src/authentication/login.handler.ts

View workflow job for this annotation

GitHub Actions / Test

Forbidden non-null assertion
}

if (adminUser) {
req.session.adminUser = adminUser;
req.session.save((err) => {
if (err) {
next(err);
return next(err);
}
if (req.session.redirectTo) {
res.redirect(302, req.session.redirectTo);
return res.redirect(302, req.session.redirectTo);
} else {
res.redirect(302, rootPath);
return res.redirect(302, rootPath);
}
});
} else {
const login = await admin.renderLogin({
action: admin.options.loginPath,
errorMessage: "invalidCredentials",
...providerProps,
});
res.send(login);

return res.send(login);
}
});
};
13 changes: 12 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 { Router } from "express";
import { AuthenticationOptions } from "../types.js";

const getLogoutPath = (admin: AdminJS) => {
const { logoutPath, rootPath } = admin.options;
Expand All @@ -10,10 +11,20 @@ const getLogoutPath = (admin: AdminJS) => {
: `/${normalizedLogoutPath}`;
};

export const withLogout = (router: Router, admin: AdminJS): void => {
export const withLogout = (
router: Router,
admin: AdminJS,
auth: AuthenticationOptions
): void => {
const logoutPath = getLogoutPath(admin);

const { provider } = auth;

router.get(logoutPath, async (request, response) => {
if (provider) {
await provider.handleLogout({ req: request, res: response });
}

request.session.destroy(() => {
response.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 { Router } from "express";
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 = (
router: Router,
admin: AdminJS,
auth: AuthenticationOptions
): void => {
const refreshTokenPath = getRefreshTokenPath(admin);

const { provider } = auth;

router.post(refreshTokenPath, async (request, response) => {
if (!provider) {
throw new WrongArgumentError(MISSING_PROVIDER_ERROR);
}

const updatedAuthInfo = await provider.handleRefreshToken(
{
data: request.fields ?? {},
query: request.query,
params: request.params,
headers: request.headers,
},
{ req: request, res: response }
);

let admin = request.session.adminUser as Partial<CurrentAdmin> | null;
if (!admin) {
admin = {};
}

if (!admin._auth) {
admin._auth = {};
}

admin._auth = {
...admin._auth,
...updatedAuthInfo,
};

request.session.adminUser = admin;
request.session.save(() => {
response.send(admin);
});
});
};
26 changes: 24 additions & 2 deletions src/buildAuthenticatedRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@ import { withLogin } from "./authentication/login.handler.js";
import { withLogout } from "./authentication/logout.handler.js";
import { withProtectedRoutesHandler } from "./authentication/protected-routes.handler.js";
import { buildAssets, buildRoutes, initializeAdmin } from "./buildRouter.js";
import { OldBodyParserUsedError } from "./errors.js";
import { OldBodyParserUsedError, WrongArgumentError } from "./errors.js";
import { AuthenticationOptions, FormidableOptions } from "./types.js";
import { withRefresh } from "./authentication/refresh.handler.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 @@ -58,6 +64,21 @@ export const buildAuthenticatedRouter = (
const { routes, assets } = AdminRouter;
const router = predefinedRouter || express.Router();

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(),
};
}

router.use((req, _, next) => {
if ((req as any)._body) {

Check warning on line 83 in src/buildAuthenticatedRouter.ts

View workflow job for this annotation

GitHub Actions / Test

Unexpected any. Specify a different type
next(new OldBodyParserUsedError());
Expand All @@ -76,10 +97,11 @@ export const buildAuthenticatedRouter = (
router.use(formidableMiddleware(formidableOptions) as any);

Check warning on line 97 in src/buildAuthenticatedRouter.ts

View workflow job for this annotation

GitHub Actions / Test

Unexpected any. Specify a different type

withLogin(router, admin, auth);
withLogout(router, admin);
withLogout(router, admin, auth);
buildAssets({ admin, assets, routes, router });

withProtectedRoutesHandler(router, admin);
withRefresh(router, admin, auth);
buildRoutes({ admin, routes, router });

return router;
Expand Down
4 changes: 3 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { BaseAuthProvider } from "adminjs";
import { Request, Response } from "express";

export type FormidableOptions = {
Expand Down Expand Up @@ -37,7 +38,7 @@ export type AuthenticationMaxRetriesOptions = {
export type AuthenticationOptions = {
cookiePassword: string;
cookieName?: string;
authenticate: (
authenticate?: (
email: string,
password: string,
context?: AuthenticationContext
Expand All @@ -46,4 +47,5 @@ export type AuthenticationOptions = {
* @description Maximum number of authorization attempts (if number - per minute)
*/
maxRetries?: number | AuthenticationMaxRetriesOptions;
provider?: BaseAuthProvider;
};
72 changes: 12 additions & 60 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3583,44 +3583,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 @@ -3654,13 +3616,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/body-parser@*":
version "1.19.0"
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f"
Expand Down Expand Up @@ -3880,15 +3835,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 @@ -4084,10 +4030,10 @@ acorn@^8.4.1, 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 @@ -4106,8 +4052,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 @@ -4118,6 +4062,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 @@ -10684,6 +10629,13 @@ [email protected]:
dependencies:
side-channel "^1.0.4"

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"

qs@~6.5.2:
version "6.5.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
Expand Down

0 comments on commit e9fd09b

Please sign in to comment.