@wbce-d9/api 9.28.0 → 10.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/drivers/baseoauth.d.ts +28 -0
- package/dist/auth/drivers/baseoauth.js +162 -0
- package/dist/auth/drivers/oauth2.d.ts +10 -9
- package/dist/auth/drivers/oauth2.js +24 -113
- package/dist/auth/drivers/openid.d.ts +11 -10
- package/dist/auth/drivers/openid.js +30 -123
- package/dist/auth.js +4 -3
- package/dist/services/authentication.js +1 -1
- package/package.json +7 -7
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Client, TokenSet } from 'openid-client';
|
|
2
|
+
import { UsersService } from '../../services/users.js';
|
|
3
|
+
import type { User } from '../../types/index.js';
|
|
4
|
+
import { LocalAuthDriver } from './local.js';
|
|
5
|
+
export interface UserPayload {
|
|
6
|
+
provider: string;
|
|
7
|
+
first_name: any;
|
|
8
|
+
last_name: any;
|
|
9
|
+
email: any;
|
|
10
|
+
external_identifier: string;
|
|
11
|
+
role: string;
|
|
12
|
+
auth_data: any;
|
|
13
|
+
}
|
|
14
|
+
export declare abstract class BaseOAuthDriver extends LocalAuthDriver {
|
|
15
|
+
abstract getClient(): Promise<Client>;
|
|
16
|
+
abstract getredirectUrl(): string;
|
|
17
|
+
abstract getUserService(): UsersService;
|
|
18
|
+
abstract getConfig(): Record<string, any>;
|
|
19
|
+
abstract getClientName(): string;
|
|
20
|
+
abstract getTokenSetAndUserInfo(payload: Record<string, any>): Promise<[TokenSet, Record<string, unknown>, UserPayload]>;
|
|
21
|
+
generateCodeVerifier(): string;
|
|
22
|
+
fetchUserId(identifier: string): Promise<string | undefined>;
|
|
23
|
+
getUserID(payload: Record<string, any>): Promise<string>;
|
|
24
|
+
logout(user: User): Promise<void>;
|
|
25
|
+
login(user: User): Promise<void>;
|
|
26
|
+
refresh(user: User): Promise<void>;
|
|
27
|
+
handleError: (e: any) => any;
|
|
28
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { parseJSON } from '@wbce-d9/utils';
|
|
2
|
+
import { errors, generators } from 'openid-client';
|
|
3
|
+
import getDatabase from '../../database/index.js';
|
|
4
|
+
import emitter from '../../emitter.js';
|
|
5
|
+
import { RecordNotUniqueException } from '../../exceptions/database/record-not-unique.js';
|
|
6
|
+
import { InvalidCredentialsException, InvalidProviderException, InvalidTokenException, ServiceUnavailableException, } from '../../exceptions/index.js';
|
|
7
|
+
import logger from '../../logger.js';
|
|
8
|
+
import { UsersService } from '../../services/users.js';
|
|
9
|
+
import { LocalAuthDriver } from './local.js';
|
|
10
|
+
export class BaseOAuthDriver extends LocalAuthDriver {
|
|
11
|
+
generateCodeVerifier() {
|
|
12
|
+
return generators.codeVerifier();
|
|
13
|
+
}
|
|
14
|
+
async fetchUserId(identifier) {
|
|
15
|
+
const user = await this.knex.select('id').from('directus_users').where('external_identifier', identifier).first();
|
|
16
|
+
return user?.id;
|
|
17
|
+
}
|
|
18
|
+
async getUserID(payload) {
|
|
19
|
+
if (!payload['code'] || !payload['codeVerifier'] || !payload['state']) {
|
|
20
|
+
logger.warn(`[${this.getClientName()}] No code, codeVerifier or state in payload`);
|
|
21
|
+
throw new InvalidCredentialsException();
|
|
22
|
+
}
|
|
23
|
+
const [tokenSet, userInfo, userPayload] = await this.getTokenSetAndUserInfo(payload);
|
|
24
|
+
const identifier = userPayload.external_identifier;
|
|
25
|
+
const userId = await this.fetchUserId(identifier);
|
|
26
|
+
if (userId) {
|
|
27
|
+
let updateReq = {};
|
|
28
|
+
if (this.getConfig()['updateUserOnLogin'] ?? false) {
|
|
29
|
+
updateReq = {
|
|
30
|
+
first_name: userPayload.first_name,
|
|
31
|
+
last_name: userPayload.last_name,
|
|
32
|
+
email: userPayload.email,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
// Run hook so the end user has the chance to augment the
|
|
36
|
+
// user that is about to be updated
|
|
37
|
+
const updatedUserPayload = await emitter.emitFilter(`auth.update`, updateReq, {
|
|
38
|
+
identifier,
|
|
39
|
+
provider: this.getConfig()['provider'],
|
|
40
|
+
providerPayload: { accessToken: tokenSet.access_token, userInfo },
|
|
41
|
+
}, { database: getDatabase(), schema: this.schema, accountability: null });
|
|
42
|
+
// Update user to update refresh_token and other properties that might have changed
|
|
43
|
+
await this.getUserService().updateOne(userId, {
|
|
44
|
+
...updatedUserPayload,
|
|
45
|
+
auth_data: userPayload.auth_data,
|
|
46
|
+
});
|
|
47
|
+
return userId;
|
|
48
|
+
}
|
|
49
|
+
// --- NEW USER ----
|
|
50
|
+
const { provider, allowPublicRegistration, requireVerifiedEmail } = this.getConfig();
|
|
51
|
+
const isEmailVerified = !requireVerifiedEmail || userInfo['email_verified'];
|
|
52
|
+
// Is public registration allowed?
|
|
53
|
+
if (!allowPublicRegistration || !isEmailVerified) {
|
|
54
|
+
logger.warn(`[${this.getClientName()}] User doesn't exist, and public registration not allowed for provider "${provider}"`);
|
|
55
|
+
throw new InvalidCredentialsException();
|
|
56
|
+
}
|
|
57
|
+
// Run hook so the end user has the chance to augment the
|
|
58
|
+
// user that is about to be created
|
|
59
|
+
const updatedUserPayload = await emitter.emitFilter(`auth.create`, userPayload, {
|
|
60
|
+
identifier,
|
|
61
|
+
provider: this.getConfig()['provider'],
|
|
62
|
+
providerPayload: { accessToken: tokenSet.access_token, userInfo },
|
|
63
|
+
}, { database: getDatabase(), schema: this.schema, accountability: null });
|
|
64
|
+
try {
|
|
65
|
+
await this.getUserService().createOne(updatedUserPayload);
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
if (e instanceof RecordNotUniqueException) {
|
|
69
|
+
logger.warn(e, `[${this.getClientName()}] Failed to register user. User not unique`);
|
|
70
|
+
throw new InvalidProviderException();
|
|
71
|
+
}
|
|
72
|
+
throw e;
|
|
73
|
+
}
|
|
74
|
+
return (await this.fetchUserId(identifier));
|
|
75
|
+
}
|
|
76
|
+
async logout(user) {
|
|
77
|
+
let authData = user.auth_data;
|
|
78
|
+
if (typeof authData === 'string') {
|
|
79
|
+
try {
|
|
80
|
+
authData = parseJSON(authData);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
logger.warn(`[${this.getClientName()}] Session data isn't valid JSON: ${authData}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (authData?.['refreshToken']) {
|
|
87
|
+
try {
|
|
88
|
+
const client = await this.getClient();
|
|
89
|
+
const tokenSet = await client.refresh(authData['refreshToken']);
|
|
90
|
+
if (tokenSet.refresh_token) {
|
|
91
|
+
await client.revoke(tokenSet.refresh_token, 'refresh_token');
|
|
92
|
+
}
|
|
93
|
+
await this.getUserService().updateOne(user.id, {
|
|
94
|
+
auth_data: null,
|
|
95
|
+
});
|
|
96
|
+
logger.info(`[${this.getClientName()}] Session closed for user.`);
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
throw this.handleError(e);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
logger.warn(`[${this.getClientName()}] Session closed for user without auth_data`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async login(user) {
|
|
107
|
+
return this.refresh(user);
|
|
108
|
+
}
|
|
109
|
+
async refresh(user) {
|
|
110
|
+
let authData = user.auth_data;
|
|
111
|
+
if (typeof authData === 'string') {
|
|
112
|
+
try {
|
|
113
|
+
authData = parseJSON(authData);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
logger.warn(`[${this.getClientName()}] Session data isn't valid JSON: ${authData}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (authData?.['refreshToken']) {
|
|
120
|
+
try {
|
|
121
|
+
const client = await this.getClient();
|
|
122
|
+
const tokenSet = await client.refresh(authData['refreshToken']);
|
|
123
|
+
// Update user refreshToken if provided
|
|
124
|
+
if (tokenSet.refresh_token) {
|
|
125
|
+
await this.getUserService().updateOne(user.id, {
|
|
126
|
+
auth_data: JSON.stringify({ refreshToken: tokenSet.refresh_token }),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
throw this.handleError(e);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
// If no auth connexion, throw user out
|
|
136
|
+
logger.error(`No [${this.getClientName()}] Session found`);
|
|
137
|
+
throw new InvalidCredentialsException();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
handleError = (e) => {
|
|
141
|
+
if (e instanceof errors.OPError) {
|
|
142
|
+
if (e.error === 'invalid_grant') {
|
|
143
|
+
// Invalid token
|
|
144
|
+
logger.trace(e, `[${this.getClientName()}] Invalid grant`);
|
|
145
|
+
return new InvalidTokenException();
|
|
146
|
+
}
|
|
147
|
+
// Server response error
|
|
148
|
+
logger.trace(e, `[${this.getClientName()}] Unknown OP error`);
|
|
149
|
+
return new ServiceUnavailableException('Service returned unexpected response', {
|
|
150
|
+
service: 'openid',
|
|
151
|
+
message: e.error_description,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
else if (e instanceof errors.RPError) {
|
|
155
|
+
// Internal client error
|
|
156
|
+
logger.trace(e, `[${this.getClientName()}] Unknown RP error`);
|
|
157
|
+
return new InvalidCredentialsException();
|
|
158
|
+
}
|
|
159
|
+
logger.trace(e, `[${this.getClientName()}] Unknown error`);
|
|
160
|
+
return e;
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
|
-
import type { Client } from 'openid-client';
|
|
2
|
+
import type { BaseClient, Client, TokenSet } from 'openid-client';
|
|
3
3
|
import { UsersService } from '../../services/users.js';
|
|
4
|
-
import type { AuthDriverOptions
|
|
5
|
-
import {
|
|
6
|
-
export declare class OAuth2AuthDriver extends
|
|
4
|
+
import type { AuthDriverOptions } from '../../types/index.js';
|
|
5
|
+
import { BaseOAuthDriver, type UserPayload } from './baseoauth.js';
|
|
6
|
+
export declare class OAuth2AuthDriver extends BaseOAuthDriver {
|
|
7
7
|
client: Client;
|
|
8
8
|
redirectUrl: string;
|
|
9
9
|
usersService: UsersService;
|
|
10
10
|
config: Record<string, any>;
|
|
11
|
+
getClient(): Promise<BaseClient>;
|
|
12
|
+
getredirectUrl(): string;
|
|
13
|
+
getUserService(): UsersService;
|
|
14
|
+
getConfig(): Record<string, any>;
|
|
15
|
+
getClientName(): string;
|
|
11
16
|
constructor(options: AuthDriverOptions, config: Record<string, any>);
|
|
12
|
-
generateCodeVerifier(): string;
|
|
13
17
|
generateAuthUrl(codeVerifier: string, prompt?: boolean): string;
|
|
14
|
-
|
|
15
|
-
getUserID(payload: Record<string, any>): Promise<string>;
|
|
16
|
-
login(user: User): Promise<void>;
|
|
17
|
-
refresh(user: User): Promise<void>;
|
|
18
|
+
getTokenSetAndUserInfo(payload: Record<string, any>): Promise<[TokenSet, Record<string, unknown>, UserPayload]>;
|
|
18
19
|
}
|
|
19
20
|
export declare function createOAuth2AuthRouter(providerName: string): Router;
|
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
import { BaseException } from '@wbce-d9/exceptions';
|
|
2
|
-
import { parseJSON } from '@wbce-d9/utils';
|
|
3
2
|
import express, { Router } from 'express';
|
|
4
3
|
import flatten from 'flat';
|
|
5
4
|
import jwt from 'jsonwebtoken';
|
|
6
|
-
import {
|
|
5
|
+
import { generators, Issuer } from 'openid-client';
|
|
7
6
|
import { getAuthProvider } from '../../auth.js';
|
|
8
|
-
import getDatabase from '../../database/index.js';
|
|
9
|
-
import emitter from '../../emitter.js';
|
|
10
7
|
import env from '../../env.js';
|
|
11
|
-
import {
|
|
12
|
-
import { InvalidConfigException, InvalidCredentialsException, InvalidProviderException, InvalidTokenException, ServiceUnavailableException, } from '../../exceptions/index.js';
|
|
8
|
+
import { InvalidConfigException, InvalidCredentialsException, InvalidTokenException } from '../../exceptions/index.js';
|
|
13
9
|
import logger from '../../logger.js';
|
|
14
10
|
import { respond } from '../../middleware/respond.js';
|
|
15
11
|
import { AuthenticationService } from '../../services/authentication.js';
|
|
@@ -19,12 +15,27 @@ import { getConfigFromEnv } from '../../utils/get-config-from-env.js';
|
|
|
19
15
|
import { getIPFromReq } from '../../utils/get-ip-from-req.js';
|
|
20
16
|
import { getMilliseconds } from '../../utils/get-milliseconds.js';
|
|
21
17
|
import { Url } from '../../utils/url.js';
|
|
22
|
-
import {
|
|
23
|
-
export class OAuth2AuthDriver extends
|
|
18
|
+
import { BaseOAuthDriver } from './baseoauth.js';
|
|
19
|
+
export class OAuth2AuthDriver extends BaseOAuthDriver {
|
|
24
20
|
client;
|
|
25
21
|
redirectUrl;
|
|
26
22
|
usersService;
|
|
27
23
|
config;
|
|
24
|
+
getClient() {
|
|
25
|
+
return Promise.resolve(this.client);
|
|
26
|
+
}
|
|
27
|
+
getredirectUrl() {
|
|
28
|
+
return this.redirectUrl;
|
|
29
|
+
}
|
|
30
|
+
getUserService() {
|
|
31
|
+
return this.usersService;
|
|
32
|
+
}
|
|
33
|
+
getConfig() {
|
|
34
|
+
return this.config;
|
|
35
|
+
}
|
|
36
|
+
getClientName() {
|
|
37
|
+
return 'OAuth2';
|
|
38
|
+
}
|
|
28
39
|
constructor(options, config) {
|
|
29
40
|
super(options, config);
|
|
30
41
|
const { authorizeUrl, accessUrl, profileUrl, clientId, clientSecret, ...additionalConfig } = config;
|
|
@@ -50,9 +61,6 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
|
|
|
50
61
|
...clientOptionsOverrides,
|
|
51
62
|
});
|
|
52
63
|
}
|
|
53
|
-
generateCodeVerifier() {
|
|
54
|
-
return generators.codeVerifier();
|
|
55
|
-
}
|
|
56
64
|
generateAuthUrl(codeVerifier, prompt = false) {
|
|
57
65
|
try {
|
|
58
66
|
const codeChallenge = generators.codeChallenge(codeVerifier);
|
|
@@ -69,22 +77,10 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
|
|
|
69
77
|
});
|
|
70
78
|
}
|
|
71
79
|
catch (e) {
|
|
72
|
-
throw handleError(e);
|
|
80
|
+
throw this.handleError(e);
|
|
73
81
|
}
|
|
74
82
|
}
|
|
75
|
-
async
|
|
76
|
-
const user = await this.knex
|
|
77
|
-
.select('id')
|
|
78
|
-
.from('directus_users')
|
|
79
|
-
.whereRaw('LOWER(??) = ?', ['external_identifier', identifier.toLowerCase()])
|
|
80
|
-
.first();
|
|
81
|
-
return user?.id;
|
|
82
|
-
}
|
|
83
|
-
async getUserID(payload) {
|
|
84
|
-
if (!payload['code'] || !payload['codeVerifier'] || !payload['state']) {
|
|
85
|
-
logger.warn('[OAuth2] No code, codeVerifier or state in payload');
|
|
86
|
-
throw new InvalidCredentialsException();
|
|
87
|
-
}
|
|
83
|
+
async getTokenSetAndUserInfo(payload) {
|
|
88
84
|
let tokenSet;
|
|
89
85
|
let userInfo;
|
|
90
86
|
try {
|
|
@@ -92,11 +88,11 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
|
|
|
92
88
|
userInfo = await this.client.userinfo(tokenSet.access_token);
|
|
93
89
|
}
|
|
94
90
|
catch (e) {
|
|
95
|
-
throw handleError(e);
|
|
91
|
+
throw this.handleError(e);
|
|
96
92
|
}
|
|
97
93
|
// Flatten response to support dot indexes
|
|
98
94
|
userInfo = flatten(userInfo);
|
|
99
|
-
const { provider, emailKey, identifierKey
|
|
95
|
+
const { provider, emailKey, identifierKey } = this.config;
|
|
100
96
|
const email = userInfo[emailKey ?? 'email'] ? String(userInfo[emailKey ?? 'email']) : undefined;
|
|
101
97
|
// Fallback to email if explicit identifier not found
|
|
102
98
|
const identifier = userInfo[identifierKey] ? String(userInfo[identifierKey]) : email;
|
|
@@ -113,94 +109,9 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
|
|
|
113
109
|
role: this.config['defaultRoleId'],
|
|
114
110
|
auth_data: tokenSet.refresh_token && JSON.stringify({ refreshToken: tokenSet.refresh_token }),
|
|
115
111
|
};
|
|
116
|
-
|
|
117
|
-
if (userId) {
|
|
118
|
-
// Run hook so the end user has the chance to augment the
|
|
119
|
-
// user that is about to be updated
|
|
120
|
-
const updatedUserPayload = await emitter.emitFilter(`auth.update`, {}, {
|
|
121
|
-
identifier,
|
|
122
|
-
provider: this.config['provider'],
|
|
123
|
-
providerPayload: { accessToken: tokenSet.access_token, userInfo },
|
|
124
|
-
}, { database: getDatabase(), schema: this.schema, accountability: null });
|
|
125
|
-
// Update user to update refresh_token and other properties that might have changed
|
|
126
|
-
await this.usersService.updateOne(userId, updatedUserPayload);
|
|
127
|
-
return userId;
|
|
128
|
-
}
|
|
129
|
-
// Is public registration allowed?
|
|
130
|
-
if (!allowPublicRegistration) {
|
|
131
|
-
logger.warn(`[OAuth2] User doesn't exist, and public registration not allowed for provider "${provider}"`);
|
|
132
|
-
throw new InvalidCredentialsException();
|
|
133
|
-
}
|
|
134
|
-
// Run hook so the end user has the chance to augment the
|
|
135
|
-
// user that is about to be created
|
|
136
|
-
const updatedUserPayload = await emitter.emitFilter(`auth.create`, userPayload, {
|
|
137
|
-
identifier,
|
|
138
|
-
provider: this.config['provider'],
|
|
139
|
-
providerPayload: { accessToken: tokenSet.access_token, userInfo },
|
|
140
|
-
}, { database: getDatabase(), schema: this.schema, accountability: null });
|
|
141
|
-
try {
|
|
142
|
-
await this.usersService.createOne(updatedUserPayload);
|
|
143
|
-
}
|
|
144
|
-
catch (e) {
|
|
145
|
-
if (e instanceof RecordNotUniqueException) {
|
|
146
|
-
logger.warn(e, '[OAuth2] Failed to register user. User not unique');
|
|
147
|
-
throw new InvalidProviderException();
|
|
148
|
-
}
|
|
149
|
-
throw e;
|
|
150
|
-
}
|
|
151
|
-
return (await this.fetchUserId(identifier));
|
|
152
|
-
}
|
|
153
|
-
async login(user) {
|
|
154
|
-
return this.refresh(user);
|
|
155
|
-
}
|
|
156
|
-
async refresh(user) {
|
|
157
|
-
let authData = user.auth_data;
|
|
158
|
-
if (typeof authData === 'string') {
|
|
159
|
-
try {
|
|
160
|
-
authData = parseJSON(authData);
|
|
161
|
-
}
|
|
162
|
-
catch {
|
|
163
|
-
logger.warn(`[OAuth2] Session data isn't valid JSON: ${authData}`);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
if (authData?.['refreshToken']) {
|
|
167
|
-
try {
|
|
168
|
-
const tokenSet = await this.client.refresh(authData['refreshToken']);
|
|
169
|
-
// Update user refreshToken if provided
|
|
170
|
-
if (tokenSet.refresh_token) {
|
|
171
|
-
await this.usersService.updateOne(user.id, {
|
|
172
|
-
auth_data: JSON.stringify({ refreshToken: tokenSet.refresh_token }),
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
catch (e) {
|
|
177
|
-
throw handleError(e);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
112
|
+
return [tokenSet, userInfo, userPayload];
|
|
180
113
|
}
|
|
181
114
|
}
|
|
182
|
-
const handleError = (e) => {
|
|
183
|
-
if (e instanceof errors.OPError) {
|
|
184
|
-
if (e.error === 'invalid_grant') {
|
|
185
|
-
// Invalid token
|
|
186
|
-
logger.trace(e, `[OAuth2] Invalid grant`);
|
|
187
|
-
return new InvalidTokenException();
|
|
188
|
-
}
|
|
189
|
-
// Server response error
|
|
190
|
-
logger.trace(e, `[OAuth2] Unknown OP error`);
|
|
191
|
-
return new ServiceUnavailableException('Service returned unexpected response', {
|
|
192
|
-
service: 'oauth2',
|
|
193
|
-
message: e.error_description,
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
else if (e instanceof errors.RPError) {
|
|
197
|
-
// Internal client error
|
|
198
|
-
logger.trace(e, `[OAuth2] Unknown RP error`);
|
|
199
|
-
return new InvalidCredentialsException();
|
|
200
|
-
}
|
|
201
|
-
logger.trace(e, `[OAuth2] Unknown error`);
|
|
202
|
-
return e;
|
|
203
|
-
};
|
|
204
115
|
export function createOAuth2AuthRouter(providerName) {
|
|
205
116
|
const router = Router();
|
|
206
117
|
router.get('/', (req, res) => {
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
|
-
import type { Client } from 'openid-client';
|
|
2
|
+
import type { AuthorizationParameters, BaseClient, Client, TokenSet } from 'openid-client';
|
|
3
3
|
import { UsersService } from '../../services/users.js';
|
|
4
|
-
import type { AuthDriverOptions
|
|
5
|
-
import {
|
|
6
|
-
export declare class OpenIDAuthDriver extends
|
|
4
|
+
import type { AuthDriverOptions } from '../../types/index.js';
|
|
5
|
+
import { BaseOAuthDriver, type UserPayload } from './baseoauth.js';
|
|
6
|
+
export declare class OpenIDAuthDriver extends BaseOAuthDriver {
|
|
7
7
|
client: Promise<Client>;
|
|
8
8
|
redirectUrl: string;
|
|
9
9
|
usersService: UsersService;
|
|
10
10
|
config: Record<string, any>;
|
|
11
|
+
getClient(): Promise<BaseClient>;
|
|
12
|
+
getredirectUrl(): string;
|
|
13
|
+
getUserService(): UsersService;
|
|
14
|
+
getConfig(): Record<string, any>;
|
|
15
|
+
getClientName(): string;
|
|
11
16
|
constructor(options: AuthDriverOptions, config: Record<string, any>);
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
private fetchUserId;
|
|
15
|
-
getUserID(payload: Record<string, any>): Promise<string>;
|
|
16
|
-
login(user: User): Promise<void>;
|
|
17
|
-
refresh(user: User): Promise<void>;
|
|
17
|
+
generateAuthUrl(codeVerifier: string, prompt?: boolean, additionalParams?: AuthorizationParameters | undefined): Promise<string>;
|
|
18
|
+
getTokenSetAndUserInfo(payload: Record<string, any>): Promise<[TokenSet, Record<string, unknown>, UserPayload]>;
|
|
18
19
|
}
|
|
19
20
|
export declare function createOpenIDAuthRouter(providerName: string): Router;
|
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
import { BaseException } from '@wbce-d9/exceptions';
|
|
2
|
-
import { parseJSON } from '@wbce-d9/utils';
|
|
3
2
|
import express, { Router } from 'express';
|
|
4
3
|
import flatten from 'flat';
|
|
5
4
|
import jwt from 'jsonwebtoken';
|
|
6
|
-
import {
|
|
5
|
+
import { generators, Issuer } from 'openid-client';
|
|
7
6
|
import { getAuthProvider } from '../../auth.js';
|
|
8
|
-
import getDatabase from '../../database/index.js';
|
|
9
|
-
import emitter from '../../emitter.js';
|
|
10
7
|
import env from '../../env.js';
|
|
11
|
-
import {
|
|
12
|
-
import { InvalidConfigException, InvalidCredentialsException, InvalidProviderException, InvalidTokenException, ServiceUnavailableException, } from '../../exceptions/index.js';
|
|
8
|
+
import { InvalidConfigException, InvalidCredentialsException, InvalidTokenException } from '../../exceptions/index.js';
|
|
13
9
|
import logger from '../../logger.js';
|
|
14
10
|
import { respond } from '../../middleware/respond.js';
|
|
15
11
|
import { AuthenticationService } from '../../services/authentication.js';
|
|
@@ -19,12 +15,27 @@ import { getConfigFromEnv } from '../../utils/get-config-from-env.js';
|
|
|
19
15
|
import { getIPFromReq } from '../../utils/get-ip-from-req.js';
|
|
20
16
|
import { getMilliseconds } from '../../utils/get-milliseconds.js';
|
|
21
17
|
import { Url } from '../../utils/url.js';
|
|
22
|
-
import {
|
|
23
|
-
export class OpenIDAuthDriver extends
|
|
18
|
+
import { BaseOAuthDriver } from './baseoauth.js';
|
|
19
|
+
export class OpenIDAuthDriver extends BaseOAuthDriver {
|
|
24
20
|
client;
|
|
25
21
|
redirectUrl;
|
|
26
22
|
usersService;
|
|
27
23
|
config;
|
|
24
|
+
getClient() {
|
|
25
|
+
return this.client;
|
|
26
|
+
}
|
|
27
|
+
getredirectUrl() {
|
|
28
|
+
return this.redirectUrl;
|
|
29
|
+
}
|
|
30
|
+
getUserService() {
|
|
31
|
+
return this.usersService;
|
|
32
|
+
}
|
|
33
|
+
getConfig() {
|
|
34
|
+
return this.config;
|
|
35
|
+
}
|
|
36
|
+
getClientName() {
|
|
37
|
+
return 'OpenID';
|
|
38
|
+
}
|
|
28
39
|
constructor(options, config) {
|
|
29
40
|
super(options, config);
|
|
30
41
|
const { issuerUrl, clientId, clientSecret, ...additionalConfig } = config;
|
|
@@ -59,10 +70,7 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
|
|
|
59
70
|
});
|
|
60
71
|
});
|
|
61
72
|
}
|
|
62
|
-
|
|
63
|
-
return generators.codeVerifier();
|
|
64
|
-
}
|
|
65
|
-
async generateAuthUrl(codeVerifier, prompt = false) {
|
|
73
|
+
async generateAuthUrl(codeVerifier, prompt = false, additionalParams) {
|
|
66
74
|
try {
|
|
67
75
|
const client = await this.client;
|
|
68
76
|
const codeChallenge = generators.codeChallenge(codeVerifier);
|
|
@@ -77,25 +85,14 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
|
|
|
77
85
|
// Some providers require state even with PKCE
|
|
78
86
|
state: codeChallenge,
|
|
79
87
|
nonce: codeChallenge,
|
|
88
|
+
...additionalParams,
|
|
80
89
|
});
|
|
81
90
|
}
|
|
82
91
|
catch (e) {
|
|
83
|
-
throw handleError(e);
|
|
92
|
+
throw this.handleError(e);
|
|
84
93
|
}
|
|
85
94
|
}
|
|
86
|
-
async
|
|
87
|
-
const user = await this.knex
|
|
88
|
-
.select('id')
|
|
89
|
-
.from('directus_users')
|
|
90
|
-
.whereRaw('LOWER(??) = ?', ['external_identifier', identifier.toLowerCase()])
|
|
91
|
-
.first();
|
|
92
|
-
return user?.id;
|
|
93
|
-
}
|
|
94
|
-
async getUserID(payload) {
|
|
95
|
-
if (!payload['code'] || !payload['codeVerifier'] || !payload['state']) {
|
|
96
|
-
logger.warn('[OpenID] No code, codeVerifier or state in payload');
|
|
97
|
-
throw new InvalidCredentialsException();
|
|
98
|
-
}
|
|
95
|
+
async getTokenSetAndUserInfo(payload) {
|
|
99
96
|
let tokenSet;
|
|
100
97
|
let userInfo;
|
|
101
98
|
try {
|
|
@@ -111,11 +108,11 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
|
|
|
111
108
|
}
|
|
112
109
|
}
|
|
113
110
|
catch (e) {
|
|
114
|
-
throw handleError(e);
|
|
111
|
+
throw this.handleError(e);
|
|
115
112
|
}
|
|
116
113
|
// Flatten response to support dot indexes
|
|
117
114
|
userInfo = flatten(userInfo);
|
|
118
|
-
const { provider, identifierKey
|
|
115
|
+
const { provider, identifierKey } = this.config;
|
|
119
116
|
const email = userInfo['email'] ? String(userInfo['email']) : undefined;
|
|
120
117
|
// Fallback to email if explicit identifier not found
|
|
121
118
|
const identifier = userInfo[identifierKey ?? 'sub'] ? String(userInfo[identifierKey ?? 'sub']) : email;
|
|
@@ -132,102 +129,9 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
|
|
|
132
129
|
role: this.config['defaultRoleId'],
|
|
133
130
|
auth_data: tokenSet.refresh_token && JSON.stringify({ refreshToken: tokenSet.refresh_token }),
|
|
134
131
|
};
|
|
135
|
-
|
|
136
|
-
if (userId) {
|
|
137
|
-
// Run hook so the end user has the chance to augment the
|
|
138
|
-
// user that is about to be updated
|
|
139
|
-
const updatedUserPayload = await emitter.emitFilter(`auth.update`, {}, {
|
|
140
|
-
identifier,
|
|
141
|
-
provider: this.config['provider'],
|
|
142
|
-
providerPayload: { accessToken: tokenSet.access_token, userInfo },
|
|
143
|
-
}, { database: getDatabase(), schema: this.schema, accountability: null });
|
|
144
|
-
// Update user to update refresh_token and other properties that might have changed
|
|
145
|
-
await this.usersService.updateOne(userId, updatedUserPayload);
|
|
146
|
-
return userId;
|
|
147
|
-
}
|
|
148
|
-
const isEmailVerified = !requireVerifiedEmail || userInfo['email_verified'];
|
|
149
|
-
// Is public registration allowed?
|
|
150
|
-
if (!allowPublicRegistration || !isEmailVerified) {
|
|
151
|
-
logger.warn(`[OpenID] User doesn't exist, and public registration not allowed for provider "${provider}"`);
|
|
152
|
-
throw new InvalidCredentialsException();
|
|
153
|
-
}
|
|
154
|
-
// Run hook so the end user has the chance to augment the
|
|
155
|
-
// user that is about to be created
|
|
156
|
-
const updatedUserPayload = await emitter.emitFilter(`auth.create`, userPayload, {
|
|
157
|
-
identifier,
|
|
158
|
-
provider: this.config['provider'],
|
|
159
|
-
providerPayload: { accessToken: tokenSet.access_token, userInfo },
|
|
160
|
-
}, { database: getDatabase(), schema: this.schema, accountability: null });
|
|
161
|
-
try {
|
|
162
|
-
await this.usersService.createOne(updatedUserPayload);
|
|
163
|
-
}
|
|
164
|
-
catch (e) {
|
|
165
|
-
if (e instanceof RecordNotUniqueException) {
|
|
166
|
-
logger.warn(e, '[OpenID] Failed to register user. User not unique');
|
|
167
|
-
throw new InvalidProviderException();
|
|
168
|
-
}
|
|
169
|
-
throw e;
|
|
170
|
-
}
|
|
171
|
-
return (await this.fetchUserId(identifier));
|
|
172
|
-
}
|
|
173
|
-
async login(user) {
|
|
174
|
-
return this.refresh(user);
|
|
175
|
-
}
|
|
176
|
-
async refresh(user) {
|
|
177
|
-
let authData = user.auth_data;
|
|
178
|
-
if (typeof authData === 'string') {
|
|
179
|
-
try {
|
|
180
|
-
authData = parseJSON(authData);
|
|
181
|
-
}
|
|
182
|
-
catch {
|
|
183
|
-
logger.warn(`[OpenID] Session data isn't valid JSON: ${authData}`);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
if (authData?.['refreshToken']) {
|
|
187
|
-
try {
|
|
188
|
-
const client = await this.client;
|
|
189
|
-
const tokenSet = await client.refresh(authData['refreshToken']);
|
|
190
|
-
// Update user refreshToken if provided
|
|
191
|
-
if (tokenSet.refresh_token) {
|
|
192
|
-
await this.usersService.updateOne(user.id, {
|
|
193
|
-
auth_data: JSON.stringify({ refreshToken: tokenSet.refresh_token }),
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
catch (e) {
|
|
198
|
-
if (e.error === 'invalid_grant') {
|
|
199
|
-
// Remove invalid token from the user store
|
|
200
|
-
await this.usersService.updateOne(user.id, {
|
|
201
|
-
auth_data: null,
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
throw handleError(e);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
132
|
+
return [tokenSet, userInfo, userPayload];
|
|
207
133
|
}
|
|
208
134
|
}
|
|
209
|
-
const handleError = (e) => {
|
|
210
|
-
if (e instanceof errors.OPError) {
|
|
211
|
-
if (e.error === 'invalid_grant') {
|
|
212
|
-
// Invalid token
|
|
213
|
-
logger.trace(e, `[OpenID] Invalid grant`);
|
|
214
|
-
return new InvalidTokenException();
|
|
215
|
-
}
|
|
216
|
-
// Server response error
|
|
217
|
-
logger.trace(e, `[OpenID] Unknown OP error`);
|
|
218
|
-
return new ServiceUnavailableException('Service returned unexpected response', {
|
|
219
|
-
service: 'openid',
|
|
220
|
-
message: e.error_description,
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
else if (e instanceof errors.RPError) {
|
|
224
|
-
// Internal client error
|
|
225
|
-
logger.trace(e, `[OpenID] Unknown RP error`);
|
|
226
|
-
return new InvalidCredentialsException();
|
|
227
|
-
}
|
|
228
|
-
logger.trace(e, `[OpenID] Unknown error`);
|
|
229
|
-
return e;
|
|
230
|
-
};
|
|
231
135
|
export function createOpenIDAuthRouter(providerName) {
|
|
232
136
|
const router = Router();
|
|
233
137
|
router.get('/', asyncHandler(async (req, res) => {
|
|
@@ -242,7 +146,10 @@ export function createOpenIDAuthRouter(providerName) {
|
|
|
242
146
|
httpOnly: true,
|
|
243
147
|
sameSite: 'lax',
|
|
244
148
|
});
|
|
245
|
-
|
|
149
|
+
const additionalParams = { ...req.query };
|
|
150
|
+
delete additionalParams['prompt'];
|
|
151
|
+
delete additionalParams['redirect'];
|
|
152
|
+
return res.redirect(await provider.generateAuthUrl(codeVerifier, prompt, additionalParams));
|
|
246
153
|
}), respond);
|
|
247
154
|
router.post('/callback', express.urlencoded({ extended: false }), (req, res) => {
|
|
248
155
|
res.redirect(303, `./callback?${new URLSearchParams(req.body)}`);
|
package/dist/auth.js
CHANGED
|
@@ -10,10 +10,11 @@ import { getSchema } from './utils/get-schema.js';
|
|
|
10
10
|
const providerNames = toArray(env['AUTH_PROVIDERS']);
|
|
11
11
|
const providers = new Map();
|
|
12
12
|
export function getAuthProvider(provider) {
|
|
13
|
-
|
|
13
|
+
const name = provider.toLowerCase();
|
|
14
|
+
if (!providers.has(name)) {
|
|
14
15
|
throw new InvalidConfigException('Auth provider not configured', { provider });
|
|
15
16
|
}
|
|
16
|
-
return providers.get(
|
|
17
|
+
return providers.get(name);
|
|
17
18
|
}
|
|
18
19
|
export async function registerAuthProviders() {
|
|
19
20
|
const options = { knex: getDatabase(), schema: await getSchema() };
|
|
@@ -42,7 +43,7 @@ export async function registerAuthProviders() {
|
|
|
42
43
|
logger.warn(`Invalid "${driver}" auth driver.`);
|
|
43
44
|
return;
|
|
44
45
|
}
|
|
45
|
-
providers.set(name, provider);
|
|
46
|
+
providers.set(name.toLowerCase(), provider);
|
|
46
47
|
});
|
|
47
48
|
}
|
|
48
49
|
function getProviderInstance(driver, options, config = {}) {
|
|
@@ -83,7 +83,7 @@ export class AuthenticationService {
|
|
|
83
83
|
throw new InvalidCredentialsException();
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
-
else if (user.provider !== providerName) {
|
|
86
|
+
else if (user.provider.toLowerCase() !== providerName.toLowerCase()) {
|
|
87
87
|
await stall(STALL_TIME, timeStart);
|
|
88
88
|
throw new InvalidProviderException();
|
|
89
89
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wbce-d9/api",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "10.0.0",
|
|
4
4
|
"description": "Directus is a real-time API and App dashboard for managing SQL database content",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -143,18 +143,18 @@
|
|
|
143
143
|
"uuid-validate": "0.0.3",
|
|
144
144
|
"vm2": "^3.9.19",
|
|
145
145
|
"wellknown": "0.5.0",
|
|
146
|
-
"@wbce-d9/constants": "9.26.3",
|
|
147
|
-
"@wbce-d9/exceptions": "9.26.3",
|
|
148
146
|
"@wbce-d9/app": "9.29.0",
|
|
147
|
+
"@wbce-d9/exceptions": "9.26.3",
|
|
149
148
|
"@wbce-d9/schema": "9.26.3",
|
|
150
|
-
"@wbce-d9/extensions-sdk": "9.26.5",
|
|
151
|
-
"@wbce-d9/storage-driver-cloudinary": "9.26.5",
|
|
152
149
|
"@wbce-d9/specs": "9.26.3",
|
|
153
|
-
"@wbce-d9/
|
|
150
|
+
"@wbce-d9/extensions-sdk": "9.26.5",
|
|
154
151
|
"@wbce-d9/storage": "9.26.3",
|
|
152
|
+
"@wbce-d9/constants": "9.26.3",
|
|
153
|
+
"@wbce-d9/storage-driver-azure": "9.26.5",
|
|
155
154
|
"@wbce-d9/storage-driver-gcs": "9.26.5",
|
|
156
|
-
"@wbce-d9/storage-driver-s3": "9.26.7",
|
|
157
155
|
"@wbce-d9/storage-driver-local": "9.26.5",
|
|
156
|
+
"@wbce-d9/storage-driver-cloudinary": "9.26.5",
|
|
157
|
+
"@wbce-d9/storage-driver-s3": "9.26.7",
|
|
158
158
|
"@wbce-d9/update-check": "9.26.3",
|
|
159
159
|
"@wbce-d9/utils": "9.28.0"
|
|
160
160
|
},
|