@wbce-d9/api 9.29.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.
@@ -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, User } from '../../types/index.js';
5
- import { LocalAuthDriver } from './local.js';
6
- export declare class OAuth2AuthDriver extends LocalAuthDriver {
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
- private fetchUserId;
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 { errors, generators, Issuer } from 'openid-client';
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 { RecordNotUniqueException } from '../../exceptions/database/record-not-unique.js';
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 { LocalAuthDriver } from './local.js';
23
- export class OAuth2AuthDriver extends LocalAuthDriver {
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 fetchUserId(identifier) {
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, allowPublicRegistration } = this.config;
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
- const userId = await this.fetchUserId(identifier);
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,20 +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, User } from '../../types/index.js';
5
- import { LocalAuthDriver } from './local.js';
6
- export declare class OpenIDAuthDriver extends LocalAuthDriver {
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
- generateCodeVerifier(): string;
13
- generateAuthUrl(codeVerifier: string, prompt?: boolean): Promise<string>;
14
- private fetchUserId;
15
- getUserID(payload: Record<string, any>): Promise<string>;
16
- logout(user: User): Promise<void>;
17
- login(user: User): Promise<void>;
18
- 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]>;
19
19
  }
20
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 { errors, generators, Issuer } from 'openid-client';
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 { RecordNotUniqueException } from '../../exceptions/database/record-not-unique.js';
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 { LocalAuthDriver } from './local.js';
23
- export class OpenIDAuthDriver extends LocalAuthDriver {
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
- generateCodeVerifier() {
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 fetchUserId(identifier) {
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, allowPublicRegistration, requireVerifiedEmail } = this.config;
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,126 +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
- const userId = await this.fetchUserId(identifier);
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 logout(user) {
174
- let authData = user.auth_data;
175
- if (typeof authData === 'string') {
176
- try {
177
- authData = parseJSON(authData);
178
- }
179
- catch {
180
- logger.warn(`[OpenID] Session data isn't valid JSON: ${authData}`);
181
- }
182
- }
183
- if (authData?.['refreshToken']) {
184
- try {
185
- const client = await this.client;
186
- await client.revoke(authData['refreshToken']);
187
- await this.usersService.updateOne(user.id, {
188
- auth_data: null,
189
- });
190
- logger.info(`[OpenID] Session closed for user.`);
191
- }
192
- catch (e) {
193
- throw handleError(e);
194
- }
195
- }
196
- }
197
- async login(user) {
198
- return this.refresh(user);
199
- }
200
- async refresh(user) {
201
- let authData = user.auth_data;
202
- if (typeof authData === 'string') {
203
- try {
204
- authData = parseJSON(authData);
205
- }
206
- catch {
207
- logger.warn(`[OpenID] Session data isn't valid JSON: ${authData}`);
208
- }
209
- }
210
- if (authData?.['refreshToken']) {
211
- try {
212
- const client = await this.client;
213
- const tokenSet = await client.refresh(authData['refreshToken']);
214
- // Update user refreshToken if provided
215
- if (tokenSet.refresh_token) {
216
- await this.usersService.updateOne(user.id, {
217
- auth_data: JSON.stringify({ refreshToken: tokenSet.refresh_token }),
218
- });
219
- }
220
- }
221
- catch (e) {
222
- if (e.error === 'invalid_grant') {
223
- // Remove invalid token from the user store
224
- await this.usersService.updateOne(user.id, {
225
- auth_data: null,
226
- });
227
- }
228
- throw handleError(e);
229
- }
230
- }
132
+ return [tokenSet, userInfo, userPayload];
231
133
  }
232
134
  }
233
- const handleError = (e) => {
234
- if (e instanceof errors.OPError) {
235
- if (e.error === 'invalid_grant') {
236
- // Invalid token
237
- logger.trace(e, `[OpenID] Invalid grant`);
238
- return new InvalidTokenException();
239
- }
240
- // Server response error
241
- logger.trace(e, `[OpenID] Unknown OP error`);
242
- return new ServiceUnavailableException('Service returned unexpected response', {
243
- service: 'openid',
244
- message: e.error_description,
245
- });
246
- }
247
- else if (e instanceof errors.RPError) {
248
- // Internal client error
249
- logger.trace(e, `[OpenID] Unknown RP error`);
250
- return new InvalidCredentialsException();
251
- }
252
- logger.trace(e, `[OpenID] Unknown error`);
253
- return e;
254
- };
255
135
  export function createOpenIDAuthRouter(providerName) {
256
136
  const router = Router();
257
137
  router.get('/', asyncHandler(async (req, res) => {
@@ -266,7 +146,10 @@ export function createOpenIDAuthRouter(providerName) {
266
146
  httpOnly: true,
267
147
  sameSite: 'lax',
268
148
  });
269
- return res.redirect(await provider.generateAuthUrl(codeVerifier, prompt));
149
+ const additionalParams = { ...req.query };
150
+ delete additionalParams['prompt'];
151
+ delete additionalParams['redirect'];
152
+ return res.redirect(await provider.generateAuthUrl(codeVerifier, prompt, additionalParams));
270
153
  }), respond);
271
154
  router.post('/callback', express.urlencoded({ extended: false }), (req, res) => {
272
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
- if (!providers.has(provider)) {
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(provider);
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": "9.29.0",
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": [
@@ -144,19 +144,19 @@
144
144
  "vm2": "^3.9.19",
145
145
  "wellknown": "0.5.0",
146
146
  "@wbce-d9/app": "9.29.0",
147
- "@wbce-d9/constants": "9.26.3",
148
- "@wbce-d9/schema": "9.26.3",
149
- "@wbce-d9/extensions-sdk": "9.26.5",
150
147
  "@wbce-d9/exceptions": "9.26.3",
148
+ "@wbce-d9/schema": "9.26.3",
151
149
  "@wbce-d9/specs": "9.26.3",
150
+ "@wbce-d9/extensions-sdk": "9.26.5",
152
151
  "@wbce-d9/storage": "9.26.3",
152
+ "@wbce-d9/constants": "9.26.3",
153
153
  "@wbce-d9/storage-driver-azure": "9.26.5",
154
- "@wbce-d9/storage-driver-cloudinary": "9.26.5",
155
154
  "@wbce-d9/storage-driver-gcs": "9.26.5",
156
155
  "@wbce-d9/storage-driver-local": "9.26.5",
157
- "@wbce-d9/utils": "9.28.0",
156
+ "@wbce-d9/storage-driver-cloudinary": "9.26.5",
158
157
  "@wbce-d9/storage-driver-s3": "9.26.7",
159
- "@wbce-d9/update-check": "9.26.3"
158
+ "@wbce-d9/update-check": "9.26.3",
159
+ "@wbce-d9/utils": "9.28.0"
160
160
  },
161
161
  "devDependencies": {
162
162
  "@ngneat/falso": "6.4.0",