Skip to content

Commit e9fd09b

Browse files
committed
feat: add support for auth providers
1 parent 8a375ab commit e9fd09b

File tree

7 files changed

+155
-79
lines changed

7 files changed

+155
-79
lines changed

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
},
3838
"homepage": "https://github.com/SoftwareBrothers/adminjs-expressjs#readme",
3939
"peerDependencies": {
40-
"adminjs": "^7.0.0",
40+
"adminjs": "^7.4.0",
4141
"express": ">=4.18.2",
4242
"express-formidable": "^1.2.0",
4343
"express-session": ">=1.17.3",
@@ -60,7 +60,7 @@
6060
"@types/node": "^18.15.3",
6161
"@typescript-eslint/eslint-plugin": "^5.53.0",
6262
"@typescript-eslint/parser": "^5.53.0",
63-
"adminjs": "^7.0.0",
63+
"adminjs": "^7.4.0",
6464
"commitlint": "^17.4.4",
6565
"eslint": "^8.35.0",
6666
"eslint-config-airbnb-base": "^15.0.0",

src/authentication/login.handler.ts

+41-13
Original file line numberDiff line numberDiff line change
@@ -70,47 +70,75 @@ export const withLogin = (
7070
const { rootPath } = admin.options;
7171
const loginPath = getLoginPath(admin);
7272

73+
const { provider } = auth;
74+
const providerProps = provider?.getUiProps?.() ?? {};
75+
7376
router.get(loginPath, async (req, res) => {
74-
const login = await admin.renderLogin({
77+
const baseProps = {
7578
action: admin.options.loginPath,
7679
errorMessage: null,
80+
};
81+
const login = await admin.renderLogin({
82+
...baseProps,
83+
...providerProps,
7784
});
78-
res.send(login);
85+
86+
return res.send(login);
7987
});
8088

8189
router.post(loginPath, async (req, res, next) => {
8290
if (!new Retry(req.ip).canLogin(auth.maxRetries)) {
8391
const login = await admin.renderLogin({
8492
action: admin.options.loginPath,
8593
errorMessage: "tooManyRequests",
94+
...providerProps,
8695
});
87-
res.send(login);
88-
return;
96+
97+
return res.send(login);
8998
}
90-
const { email, password } = req.fields as {
91-
email: string;
92-
password: string;
93-
};
99+
94100
const context: AuthenticationContext = { req, res };
95-
const adminUser = await auth.authenticate(email, password, context);
101+
102+
let adminUser;
103+
if (provider) {
104+
adminUser = await provider.handleLogin(
105+
{
106+
headers: req.headers,
107+
query: req.query,
108+
params: req.params,
109+
data: req.fields ?? {},
110+
},
111+
context
112+
);
113+
} else {
114+
const { email, password } = req.fields as {
115+
email: string;
116+
password: string;
117+
};
118+
// "auth.authenticate" must always be defined if "auth.provider" isn't
119+
adminUser = await auth.authenticate!(email, password, context);
120+
}
121+
96122
if (adminUser) {
97123
req.session.adminUser = adminUser;
98124
req.session.save((err) => {
99125
if (err) {
100-
next(err);
126+
return next(err);
101127
}
102128
if (req.session.redirectTo) {
103-
res.redirect(302, req.session.redirectTo);
129+
return res.redirect(302, req.session.redirectTo);
104130
} else {
105-
res.redirect(302, rootPath);
131+
return res.redirect(302, rootPath);
106132
}
107133
});
108134
} else {
109135
const login = await admin.renderLogin({
110136
action: admin.options.loginPath,
111137
errorMessage: "invalidCredentials",
138+
...providerProps,
112139
});
113-
res.send(login);
140+
141+
return res.send(login);
114142
}
115143
});
116144
};

src/authentication/logout.handler.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import AdminJS from "adminjs";
22
import { Router } from "express";
3+
import { AuthenticationOptions } from "../types.js";
34

45
const getLogoutPath = (admin: AdminJS) => {
56
const { logoutPath, rootPath } = admin.options;
@@ -10,10 +11,20 @@ const getLogoutPath = (admin: AdminJS) => {
1011
: `/${normalizedLogoutPath}`;
1112
};
1213

13-
export const withLogout = (router: Router, admin: AdminJS): void => {
14+
export const withLogout = (
15+
router: Router,
16+
admin: AdminJS,
17+
auth: AuthenticationOptions
18+
): void => {
1419
const logoutPath = getLogoutPath(admin);
1520

21+
const { provider } = auth;
22+
1623
router.get(logoutPath, async (request, response) => {
24+
if (provider) {
25+
await provider.handleLogout({ req: request, res: response });
26+
}
27+
1728
request.session.destroy(() => {
1829
response.redirect(admin.options.loginPath);
1930
});

src/authentication/refresh.handler.ts

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import AdminJS, { CurrentAdmin } from "adminjs";
2+
import { Router } from "express";
3+
import { AuthenticationOptions } from "../types.js";
4+
import { WrongArgumentError } from "../errors.js";
5+
6+
const getRefreshTokenPath = (admin: AdminJS) => {
7+
const { refreshTokenPath, rootPath } = admin.options;
8+
const normalizedRefreshTokenPath = refreshTokenPath.replace(rootPath, "");
9+
10+
return normalizedRefreshTokenPath.startsWith("/")
11+
? normalizedRefreshTokenPath
12+
: `/${normalizedRefreshTokenPath}`;
13+
};
14+
15+
const MISSING_PROVIDER_ERROR =
16+
'"provider" has to be configured to use refresh token mechanism';
17+
18+
export const withRefresh = (
19+
router: Router,
20+
admin: AdminJS,
21+
auth: AuthenticationOptions
22+
): void => {
23+
const refreshTokenPath = getRefreshTokenPath(admin);
24+
25+
const { provider } = auth;
26+
27+
router.post(refreshTokenPath, async (request, response) => {
28+
if (!provider) {
29+
throw new WrongArgumentError(MISSING_PROVIDER_ERROR);
30+
}
31+
32+
const updatedAuthInfo = await provider.handleRefreshToken(
33+
{
34+
data: request.fields ?? {},
35+
query: request.query,
36+
params: request.params,
37+
headers: request.headers,
38+
},
39+
{ req: request, res: response }
40+
);
41+
42+
let admin = request.session.adminUser as Partial<CurrentAdmin> | null;
43+
if (!admin) {
44+
admin = {};
45+
}
46+
47+
if (!admin._auth) {
48+
admin._auth = {};
49+
}
50+
51+
admin._auth = {
52+
...admin._auth,
53+
...updatedAuthInfo,
54+
};
55+
56+
request.session.adminUser = admin;
57+
request.session.save(() => {
58+
response.send(admin);
59+
});
60+
});
61+
};

src/buildAuthenticatedRouter.ts

+24-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,14 @@ import { withLogin } from "./authentication/login.handler.js";
77
import { withLogout } from "./authentication/logout.handler.js";
88
import { withProtectedRoutesHandler } from "./authentication/protected-routes.handler.js";
99
import { buildAssets, buildRoutes, initializeAdmin } from "./buildRouter.js";
10-
import { OldBodyParserUsedError } from "./errors.js";
10+
import { OldBodyParserUsedError, WrongArgumentError } from "./errors.js";
1111
import { AuthenticationOptions, FormidableOptions } from "./types.js";
12+
import { withRefresh } from "./authentication/refresh.handler.js";
13+
14+
const MISSING_AUTH_CONFIG_ERROR =
15+
'You must configure either "authenticate" method or assign an auth "provider"';
16+
const INVALID_AUTH_CONFIG_ERROR =
17+
'You cannot configure both "authenticate" and "provider". "authenticate" will be removed in next major release.';
1218

1319
/**
1420
* @typedef {Function} Authenticate
@@ -58,6 +64,21 @@ export const buildAuthenticatedRouter = (
5864
const { routes, assets } = AdminRouter;
5965
const router = predefinedRouter || express.Router();
6066

67+
if (!auth.authenticate && !auth.provider) {
68+
throw new WrongArgumentError(MISSING_AUTH_CONFIG_ERROR);
69+
}
70+
71+
if (auth.authenticate && auth.provider) {
72+
throw new WrongArgumentError(INVALID_AUTH_CONFIG_ERROR);
73+
}
74+
75+
if (auth.provider) {
76+
admin.options.env = {
77+
...admin.options.env,
78+
...auth.provider.getUiProps(),
79+
};
80+
}
81+
6182
router.use((req, _, next) => {
6283
if ((req as any)._body) {
6384
next(new OldBodyParserUsedError());
@@ -76,10 +97,11 @@ export const buildAuthenticatedRouter = (
7697
router.use(formidableMiddleware(formidableOptions) as any);
7798

7899
withLogin(router, admin, auth);
79-
withLogout(router, admin);
100+
withLogout(router, admin, auth);
80101
buildAssets({ admin, assets, routes, router });
81102

82103
withProtectedRoutesHandler(router, admin);
104+
withRefresh(router, admin, auth);
83105
buildRoutes({ admin, routes, router });
84106

85107
return router;

src/types.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { BaseAuthProvider } from "adminjs";
12
import { Request, Response } from "express";
23

34
export type FormidableOptions = {
@@ -37,7 +38,7 @@ export type AuthenticationMaxRetriesOptions = {
3738
export type AuthenticationOptions = {
3839
cookiePassword: string;
3940
cookieName?: string;
40-
authenticate: (
41+
authenticate?: (
4142
email: string,
4243
password: string,
4344
context?: AuthenticationContext
@@ -46,4 +47,5 @@ export type AuthenticationOptions = {
4647
* @description Maximum number of authorization attempts (if number - per minute)
4748
*/
4849
maxRetries?: number | AuthenticationMaxRetriesOptions;
50+
provider?: BaseAuthProvider;
4951
};

yarn.lock

+12-60
Original file line numberDiff line numberDiff line change
@@ -3583,44 +3583,6 @@
35833583
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e"
35843584
integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==
35853585

3586-
"@types/babel-core@^6.25.7":
3587-
version "6.25.7"
3588-
resolved "https://registry.yarnpkg.com/@types/babel-core/-/babel-core-6.25.7.tgz#f9c22d5c085686da2f6ffbdae778edb3e6017671"
3589-
integrity sha512-WPnyzNFVRo6bxpr7bcL27qXtNKNQ3iToziNBpibaXHyKGWQA0+tTLt73QQxC/5zzbM544ih6Ni5L5xrck6rGwg==
3590-
dependencies:
3591-
"@types/babel-generator" "*"
3592-
"@types/babel-template" "*"
3593-
"@types/babel-traverse" "*"
3594-
"@types/babel-types" "*"
3595-
"@types/babylon" "*"
3596-
3597-
"@types/babel-generator@*":
3598-
version "6.25.5"
3599-
resolved "https://registry.yarnpkg.com/@types/babel-generator/-/babel-generator-6.25.5.tgz#b02723fd589349b05524376e5530228d3675d878"
3600-
integrity sha512-lhbwMlAy5rfWG+R6l8aPtJdEFX/kcv6LMFIuvUb0i89ehqgD24je9YcB+0fRspQhgJGlEsUImxpw4pQeKS/+8Q==
3601-
dependencies:
3602-
"@types/babel-types" "*"
3603-
3604-
"@types/babel-template@*":
3605-
version "6.25.2"
3606-
resolved "https://registry.yarnpkg.com/@types/babel-template/-/babel-template-6.25.2.tgz#3c4cde02dbcbbf461a58d095a9f69f35eabd5f06"
3607-
integrity sha512-QKtDQRJmAz3Y1HSxfMl0syIHebMc/NnOeH/8qeD0zjgU2juD0uyC922biMxCy5xjTNvHinigML2l8kxE8eEBmw==
3608-
dependencies:
3609-
"@types/babel-types" "*"
3610-
"@types/babylon" "*"
3611-
3612-
"@types/babel-traverse@*":
3613-
version "6.25.7"
3614-
resolved "https://registry.yarnpkg.com/@types/babel-traverse/-/babel-traverse-6.25.7.tgz#bc75fce23d8394534562a36a32dec94a54d11835"
3615-
integrity sha512-BeQiEGLnVzypzBdsexEpZAHUx+WucOMXW6srEWDkl4SegBlaCy+iBvRO+4vz6EZ+BNQg22G4MCdDdvZxf+jW5A==
3616-
dependencies:
3617-
"@types/babel-types" "*"
3618-
3619-
"@types/babel-types@*":
3620-
version "7.0.11"
3621-
resolved "https://registry.yarnpkg.com/@types/babel-types/-/babel-types-7.0.11.tgz#263b113fa396fac4373188d73225297fb86f19a9"
3622-
integrity sha512-pkPtJUUY+Vwv6B1inAz55rQvivClHJxc9aVEPPmaq2cbyeMLCiDpbKpcKyX4LAwpNGi+SHBv0tHv6+0gXv0P2A==
3623-
36243586
"@types/babel__core@^7.1.14":
36253587
version "7.20.0"
36263588
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.0.tgz#61bc5a4cae505ce98e1e36c5445e4bee060d8891"
@@ -3654,13 +3616,6 @@
36543616
dependencies:
36553617
"@babel/types" "^7.3.0"
36563618

3657-
"@types/babylon@*":
3658-
version "6.16.6"
3659-
resolved "https://registry.yarnpkg.com/@types/babylon/-/babylon-6.16.6.tgz#a1e7e01567b26a5ebad321a74d10299189d8d932"
3660-
integrity sha512-G4yqdVlhr6YhzLXFKy5F7HtRBU8Y23+iWy7UKthMq/OSQnL1hbsoeXESQ2LY8zEDlknipDG3nRGhUC9tkwvy/w==
3661-
dependencies:
3662-
"@types/babel-types" "*"
3663-
36643619
"@types/body-parser@*":
36653620
version "1.19.0"
36663621
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f"
@@ -3880,15 +3835,6 @@
38803835
"@types/scheduler" "*"
38813836
csstype "^3.0.2"
38823837

3883-
"@types/react@^18.0.28":
3884-
version "18.0.28"
3885-
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.28.tgz#accaeb8b86f4908057ad629a26635fe641480065"
3886-
integrity sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==
3887-
dependencies:
3888-
"@types/prop-types" "*"
3889-
"@types/scheduler" "*"
3890-
csstype "^3.0.2"
3891-
38923838
38933839
version "1.20.2"
38943840
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975"
@@ -4084,10 +4030,10 @@ acorn@^8.4.1, acorn@^8.5.0, acorn@^8.8.0:
40844030
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a"
40854031
integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==
40864032

4087-
adminjs@^7.0.0:
4088-
version "7.0.0"
4089-
resolved "https://registry.yarnpkg.com/adminjs/-/adminjs-7.0.0.tgz#5dad16fcdd91dfe9fd84402b3e109f9fdbb74534"
4090-
integrity sha512-6cvr04yhPpoqpK9lfy5ohxHMUI+J9lDZbRScyqzmpPTZ4P8E68unZekixx7nAGXFBmhixP5+CumLNpCNzcUeGA==
4033+
adminjs@^7.4.0:
4034+
version "7.4.0"
4035+
resolved "https://registry.yarnpkg.com/adminjs/-/adminjs-7.4.0.tgz#9551c79ac1b6047f1cc86ac1525e01660fea954a"
4036+
integrity sha512-GKot4WNEe5aQN2MLkSR216N0oE9KrpJ+COwPrYhRlF42wUMiQucwQbq36VfMb/ZsiEpF3SfBdSa9Qi6EApR0WQ==
40914037
dependencies:
40924038
"@adminjs/design-system" "^4.0.0"
40934039
"@babel/core" "^7.21.0"
@@ -4106,8 +4052,6 @@ adminjs@^7.0.0:
41064052
"@rollup/plugin-node-resolve" "^15.0.1"
41074053
"@rollup/plugin-replace" "^5.0.2"
41084054
"@rollup/plugin-terser" "^0.4.0"
4109-
"@types/babel-core" "^6.25.7"
4110-
"@types/react" "^18.0.28"
41114055
axios "^1.3.4"
41124056
commander "^10.0.0"
41134057
flat "^5.0.2"
@@ -4118,6 +4062,7 @@ adminjs@^7.0.0:
41184062
ora "^6.2.0"
41194063
prop-types "^15.8.1"
41204064
punycode "^2.3.0"
4065+
qs "^6.11.1"
41214066
react "^18.2.0"
41224067
react-dom "^18.2.0"
41234068
react-feather "^2.0.10"
@@ -10684,6 +10629,13 @@ [email protected]:
1068410629
dependencies:
1068510630
side-channel "^1.0.4"
1068610631

10632+
qs@^6.11.1:
10633+
version "6.11.2"
10634+
resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9"
10635+
integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==
10636+
dependencies:
10637+
side-channel "^1.0.4"
10638+
1068710639
qs@~6.5.2:
1068810640
version "6.5.2"
1068910641
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"

0 commit comments

Comments
 (0)