Skip to content

Commit 69c503f

Browse files
committed
feat: add support for auth providers
1 parent f811585 commit 69c503f

File tree

4 files changed

+136
-87
lines changed

4 files changed

+136
-87
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"@hapi/cookie": "^11.0.2 || ^12.0.0",
2424
"@hapi/hapi": "^20.2.1 || ^21.0.0",
2525
"@hapi/inert": "^6.0.5 || ^7.0.0",
26-
"adminjs": "^7.0.0"
26+
"adminjs": "^7.4.0"
2727
},
2828
"peerDependenciesMeta": {
2929
"@hapi/inert": {
@@ -57,7 +57,7 @@
5757
"@types/node": "^18.15.5",
5858
"@typescript-eslint/eslint-plugin": "^5.56.0",
5959
"@typescript-eslint/parser": "^5.56.0",
60-
"adminjs": "^7.0.0",
60+
"adminjs": "^7.4.0",
6161
"eslint": "^8.36.0",
6262
"eslint-config-airbnb-base": "^15.0.0",
6363
"eslint-config-prettier": "^8.8.0",

src/extensions/session-auth.ts

Lines changed: 82 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,31 @@ interface AuthRequestPayload {
99
password: string;
1010
}
1111

12+
const MISSING_PROVIDER_ERROR = '"provider" has to be configured to use refresh token mechanism';
13+
1214
/**
1315
* Creates authentication logic for admin users
1416
*/
1517
const sessionAuth = async (server: Hapi.Server, adminJs: AdminJS) => {
1618
const options = adminJs.options as ExtendedAdminJSOptionsWithDefault;
17-
const { loginPath, logoutPath, rootPath } = options;
19+
const { loginPath, logoutPath, refreshTokenPath, rootPath } = options;
1820
const {
1921
cookiePassword,
2022
authenticate,
23+
provider,
2124
isSecure,
2225
defaultMessage,
2326
cookieName,
2427
strategy = 'simple',
2528
...other
2629
} = options.auth;
2730

28-
if (!authenticate) {
29-
throw new Error('"authenticate" function must be provided for authenticated access');
31+
if (!authenticate && !provider) {
32+
throw new Error('"authenticate" or "provider" must be configured for authenticated access');
3033
}
3134

35+
const providerProps = provider ? provider.getUiProps() : {};
36+
3237
// example authentication is based on the cookie store
3338
await server.register(HapiAuthCookie);
3439

@@ -51,23 +56,34 @@ const sessionAuth = async (server: Hapi.Server, adminJs: AdminJS) => {
5156
},
5257
handler: async (request, h) => {
5358
try {
54-
let errorMessage = defaultMessage;
59+
let errorMessage = defaultMessage as string;
5560
if (request.method === 'post') {
56-
const { email, password } = request.payload as AuthRequestPayload;
57-
const admin = await authenticate(email, password);
58-
if (admin) {
59-
request.cookieAuth.set(admin);
61+
const ctx = { request, h };
62+
let adminUser;
63+
if (provider) {
64+
adminUser = await provider.handleLogin(
65+
{
66+
headers: request.headers,
67+
query: request.query,
68+
params: request.params,
69+
data: (request.payload as Record<string, unknown>) ?? {},
70+
},
71+
ctx
72+
);
73+
} else {
74+
const { email, password } = request.payload as AuthRequestPayload;
75+
adminUser = await authenticate!(email, password, ctx);
76+
}
77+
78+
if (adminUser) {
79+
request.cookieAuth.set(adminUser);
6080
return h.redirect(rootPath);
6181
}
6282
errorMessage = 'invalidCredentials';
6383
}
6484

65-
// AdminJS exposes function which renders login form for us.
66-
// It takes 2 arguments:
67-
// - options.action (with login path)
68-
// - [errorMessage] optional error message - visible when user
69-
// gives wrong credentials
70-
return adminJs.renderLogin({ action: loginPath, errorMessage });
85+
const baseProps = { action: loginPath, errorMessage };
86+
return adminJs.renderLogin({ ...baseProps, ...providerProps });
7187
} catch (e) {
7288
console.log(e);
7389
throw e;
@@ -80,10 +96,62 @@ const sessionAuth = async (server: Hapi.Server, adminJs: AdminJS) => {
8096
path: logoutPath,
8197
options: { auth: false },
8298
handler: async (request, h) => {
99+
if (provider) {
100+
await provider.handleLogout({
101+
headers: request.headers,
102+
query: request.query,
103+
params: request.params,
104+
data: (request.payload as Record<string, unknown>) ?? {},
105+
});
106+
}
83107
request.cookieAuth.clear();
84108
return h.redirect(loginPath);
85109
},
86110
});
111+
112+
server.route({
113+
method: 'POST',
114+
path: refreshTokenPath,
115+
options: {
116+
auth: { mode: 'try', strategy: 'session' },
117+
plugins: { cookie: { redirectTo: false } },
118+
},
119+
handler: async (request, h) => {
120+
if (!provider) {
121+
throw new Error(MISSING_PROVIDER_ERROR);
122+
}
123+
124+
const updatedAuthInfo = await provider.handleRefreshToken(
125+
{
126+
data: (request.payload as Record<string, unknown>) ?? {},
127+
query: request.query,
128+
params: request.params,
129+
headers: request.headers,
130+
},
131+
{ request, h }
132+
);
133+
134+
let adminObject = Array.isArray(request.auth?.credentials)
135+
? request.auth?.credentials?.[0]
136+
: request.auth?.credentials;
137+
138+
if (!adminObject) {
139+
adminObject = {};
140+
}
141+
142+
if (!adminObject._auth) {
143+
adminObject._auth = {};
144+
}
145+
146+
adminObject._auth = {
147+
...adminObject._auth,
148+
...updatedAuthInfo,
149+
};
150+
151+
request.cookieAuth.set(adminObject);
152+
return h.response(adminObject);
153+
},
154+
});
87155
};
88156

89157
export default sessionAuth;

src/plugin.ts

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,35 @@
11
import Boom from '@hapi/boom';
22
import Hapi, { Plugin, RouteOptions } from '@hapi/hapi';
33
import inert from '@hapi/inert';
4-
import AdminJS, { AdminJSOptions, AdminJSOptionsWithDefault, Router as AdminRouter } from 'adminjs';
4+
import AdminJS, { AdminJSOptions, AdminJSOptionsWithDefault, Router as AdminRouter, BaseAuthProvider } from 'adminjs';
55
import path from 'path';
66
import sessionAuth from './extensions/session-auth.js';
77
import info from './info.js';
88

9+
const MISSING_AUTH_CONFIG_ERROR = 'You must configure either "authenticate" method or assign an auth "provider"';
10+
const INVALID_AUTH_CONFIG_ERROR =
11+
'You cannot configure both "authenticate" and "provider". "authenticate" will be removed in next major release.';
12+
913
/**
1014
* Plugin definition for Hapi.js framework.
1115
* @private
1216
*/
1317

1418
export type AuthenticateResult = Promise<Record<string, unknown> | null | false> | null | false;
19+
export type AuthenticationContext = {
20+
request: Hapi.Request;
21+
h: Hapi.ResponseToolkit;
22+
};
1523
export type AuthOptions = {
1624
/**
1725
* Function takes email and password as argument. Should return a logged-in user object or null/false.
1826
* If provided, the strategy is set to 'session'.
1927
*/
20-
authenticate?: (email: string, password: string) => AuthenticateResult;
28+
authenticate?: (email: string, password: string, context?: AuthenticationContext) => AuthenticateResult;
29+
/**
30+
* Auth Provider
31+
*/
32+
provider?: BaseAuthProvider;
2133
/**
2234
* Auth strategy for Hapi routes.
2335
*/
@@ -104,23 +116,40 @@ const register = async (server: Hapi.Server, options: ExtendedAdminJSOptions) =>
104116
const { registerInert = true } = options;
105117
const { routes, assets } = AdminRouter;
106118

107-
if (options.auth?.authenticate) {
108-
if (options.auth.strategy && options.auth.strategy !== 'session') {
109-
throw new Error(`When you give auth.authenticate as a parameter, auth strategy is set to 'session'.
110-
Please remove auth.strategy from authentication parameters.
111-
`);
119+
if (options.auth) {
120+
if (!options.auth.authenticate && !options.auth.provider) {
121+
throw new Error(MISSING_AUTH_CONFIG_ERROR);
122+
}
123+
124+
if (options.auth.authenticate && options.auth.provider) {
125+
throw new Error(INVALID_AUTH_CONFIG_ERROR);
126+
}
127+
128+
if (options.auth.authenticate || options.auth.provider) {
129+
if (options.auth.strategy && options.auth.strategy !== 'session') {
130+
throw new Error(`When you give auth.authenticate as a parameter, auth strategy is set to 'session'.
131+
Please remove auth.strategy from authentication parameters.
132+
`);
133+
}
134+
options.auth.strategy = 'session';
135+
136+
if (!options.auth.cookiePassword) {
137+
throw new Error('You have to give auth.cookiePassword parameter if you want to use authenticate function.');
138+
}
112139
}
113-
options.auth.strategy = 'session';
114140

115-
if (!options.auth.cookiePassword) {
116-
throw new Error('You have to give auth.cookiePassword parameter if you want to use authenticate function.');
141+
if (options.auth.provider) {
142+
options.env = {
143+
...options.env,
144+
...options.auth.provider.getUiProps(),
145+
};
117146
}
118147
}
119148

120149
const admin = new AdminJS(options);
121150
await admin.initialize();
122151

123-
if (options.auth?.authenticate) {
152+
if (options.auth?.authenticate || options.auth?.provider) {
124153
await sessionAuth(server, admin);
125154
}
126155

yarn.lock

Lines changed: 12 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -2868,51 +2868,6 @@
28682868
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e"
28692869
integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==
28702870

2871-
"@types/babel-core@^6.25.7":
2872-
version "6.25.7"
2873-
resolved "https://registry.yarnpkg.com/@types/babel-core/-/babel-core-6.25.7.tgz#f9c22d5c085686da2f6ffbdae778edb3e6017671"
2874-
integrity sha512-WPnyzNFVRo6bxpr7bcL27qXtNKNQ3iToziNBpibaXHyKGWQA0+tTLt73QQxC/5zzbM544ih6Ni5L5xrck6rGwg==
2875-
dependencies:
2876-
"@types/babel-generator" "*"
2877-
"@types/babel-template" "*"
2878-
"@types/babel-traverse" "*"
2879-
"@types/babel-types" "*"
2880-
"@types/babylon" "*"
2881-
2882-
"@types/babel-generator@*":
2883-
version "6.25.5"
2884-
resolved "https://registry.yarnpkg.com/@types/babel-generator/-/babel-generator-6.25.5.tgz#b02723fd589349b05524376e5530228d3675d878"
2885-
integrity sha512-lhbwMlAy5rfWG+R6l8aPtJdEFX/kcv6LMFIuvUb0i89ehqgD24je9YcB+0fRspQhgJGlEsUImxpw4pQeKS/+8Q==
2886-
dependencies:
2887-
"@types/babel-types" "*"
2888-
2889-
"@types/babel-template@*":
2890-
version "6.25.2"
2891-
resolved "https://registry.yarnpkg.com/@types/babel-template/-/babel-template-6.25.2.tgz#3c4cde02dbcbbf461a58d095a9f69f35eabd5f06"
2892-
integrity sha512-QKtDQRJmAz3Y1HSxfMl0syIHebMc/NnOeH/8qeD0zjgU2juD0uyC922biMxCy5xjTNvHinigML2l8kxE8eEBmw==
2893-
dependencies:
2894-
"@types/babel-types" "*"
2895-
"@types/babylon" "*"
2896-
2897-
"@types/babel-traverse@*":
2898-
version "6.25.7"
2899-
resolved "https://registry.yarnpkg.com/@types/babel-traverse/-/babel-traverse-6.25.7.tgz#bc75fce23d8394534562a36a32dec94a54d11835"
2900-
integrity sha512-BeQiEGLnVzypzBdsexEpZAHUx+WucOMXW6srEWDkl4SegBlaCy+iBvRO+4vz6EZ+BNQg22G4MCdDdvZxf+jW5A==
2901-
dependencies:
2902-
"@types/babel-types" "*"
2903-
2904-
"@types/babel-types@*":
2905-
version "7.0.11"
2906-
resolved "https://registry.yarnpkg.com/@types/babel-types/-/babel-types-7.0.11.tgz#263b113fa396fac4373188d73225297fb86f19a9"
2907-
integrity sha512-pkPtJUUY+Vwv6B1inAz55rQvivClHJxc9aVEPPmaq2cbyeMLCiDpbKpcKyX4LAwpNGi+SHBv0tHv6+0gXv0P2A==
2908-
2909-
"@types/babylon@*":
2910-
version "6.16.6"
2911-
resolved "https://registry.yarnpkg.com/@types/babylon/-/babylon-6.16.6.tgz#a1e7e01567b26a5ebad321a74d10299189d8d932"
2912-
integrity sha512-G4yqdVlhr6YhzLXFKy5F7HtRBU8Y23+iWy7UKthMq/OSQnL1hbsoeXESQ2LY8zEDlknipDG3nRGhUC9tkwvy/w==
2913-
dependencies:
2914-
"@types/babel-types" "*"
2915-
29162871
"@types/estree@*":
29172872
version "0.0.51"
29182873
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40"
@@ -3020,15 +2975,6 @@
30202975
"@types/scheduler" "*"
30212976
csstype "^3.0.2"
30222977

3023-
"@types/react@^18.0.28":
3024-
version "18.0.28"
3025-
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.28.tgz#accaeb8b86f4908057ad629a26635fe641480065"
3026-
integrity sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==
3027-
dependencies:
3028-
"@types/prop-types" "*"
3029-
"@types/scheduler" "*"
3030-
csstype "^3.0.2"
3031-
30322978
"@types/resolve@1.20.2":
30332979
version "1.20.2"
30342980
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975"
@@ -3176,10 +3122,10 @@ acorn@^8.4.1, acorn@^8.5.0, acorn@^8.8.0:
31763122
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a"
31773123
integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==
31783124

3179-
adminjs@^7.0.0:
3180-
version "7.0.0"
3181-
resolved "https://registry.yarnpkg.com/adminjs/-/adminjs-7.0.0.tgz#5dad16fcdd91dfe9fd84402b3e109f9fdbb74534"
3182-
integrity sha512-6cvr04yhPpoqpK9lfy5ohxHMUI+J9lDZbRScyqzmpPTZ4P8E68unZekixx7nAGXFBmhixP5+CumLNpCNzcUeGA==
3125+
adminjs@^7.4.0:
3126+
version "7.4.0"
3127+
resolved "https://registry.yarnpkg.com/adminjs/-/adminjs-7.4.0.tgz#9551c79ac1b6047f1cc86ac1525e01660fea954a"
3128+
integrity sha512-GKot4WNEe5aQN2MLkSR216N0oE9KrpJ+COwPrYhRlF42wUMiQucwQbq36VfMb/ZsiEpF3SfBdSa9Qi6EApR0WQ==
31833129
dependencies:
31843130
"@adminjs/design-system" "^4.0.0"
31853131
"@babel/core" "^7.21.0"
@@ -3198,8 +3144,6 @@ adminjs@^7.0.0:
31983144
"@rollup/plugin-node-resolve" "^15.0.1"
31993145
"@rollup/plugin-replace" "^5.0.2"
32003146
"@rollup/plugin-terser" "^0.4.0"
3201-
"@types/babel-core" "^6.25.7"
3202-
"@types/react" "^18.0.28"
32033147
axios "^1.3.4"
32043148
commander "^10.0.0"
32053149
flat "^5.0.2"
@@ -3210,6 +3154,7 @@ adminjs@^7.0.0:
32103154
ora "^6.2.0"
32113155
prop-types "^15.8.1"
32123156
punycode "^2.3.0"
3157+
qs "^6.11.1"
32133158
react "^18.2.0"
32143159
react-dom "^18.2.0"
32153160
react-feather "^2.0.10"
@@ -7699,6 +7644,13 @@ qrcode-terminal@^0.12.0:
76997644
resolved "https://registry.yarnpkg.com/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz#bb5b699ef7f9f0505092a3748be4464fe71b5819"
77007645
integrity sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==
77017646

7647+
qs@^6.11.1:
7648+
version "6.11.2"
7649+
resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9"
7650+
integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==
7651+
dependencies:
7652+
side-channel "^1.0.4"
7653+
77027654
queue-microtask@^1.2.2:
77037655
version "1.2.3"
77047656
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"

0 commit comments

Comments
 (0)