@wbce-d9/api 9.29.0 → 10.1.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,9 +1,9 @@
1
1
  import { Router } from 'express';
2
2
  import Joi from 'joi';
3
3
  import ldap from 'ldapjs';
4
+ import { GET_SET_HEADER } from '../../constants.js';
4
5
  import getDatabase from '../../database/index.js';
5
6
  import emitter from '../../emitter.js';
6
- import env from '../../env.js';
7
7
  import { RecordNotUniqueException } from '../../exceptions/database/record-not-unique.js';
8
8
  import { InvalidConfigException, InvalidCredentialsException, InvalidPayloadException, InvalidProviderException, ServiceUnavailableException, UnexpectedResponseException, } from '../../exceptions/index.js';
9
9
  import logger from '../../logger.js';
@@ -12,7 +12,6 @@ import { AuthenticationService } from '../../services/authentication.js';
12
12
  import { UsersService } from '../../services/users.js';
13
13
  import asyncHandler from '../../utils/async-handler.js';
14
14
  import { getIPFromReq } from '../../utils/get-ip-from-req.js';
15
- import { getMilliseconds } from '../../utils/get-milliseconds.js';
16
15
  import { AuthDriver } from '../auth.js';
17
16
  // 0x2: ACCOUNTDISABLE
18
17
  // 0x10: LOCKOUT
@@ -315,13 +314,7 @@ export function createLDAPAuthRouter(provider) {
315
314
  payload['data']['refresh_token'] = refreshToken;
316
315
  }
317
316
  if (mode === 'cookie') {
318
- res.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], refreshToken, {
319
- httpOnly: true,
320
- domain: env['REFRESH_TOKEN_COOKIE_DOMAIN'],
321
- maxAge: getMilliseconds(env['REFRESH_TOKEN_TTL']),
322
- secure: env['REFRESH_TOKEN_COOKIE_SECURE'] ?? false,
323
- sameSite: env['REFRESH_TOKEN_COOKIE_SAME_SITE'] || 'strict',
324
- });
317
+ res.setHeader('Set-Cookie', GET_SET_HEADER(refreshToken));
325
318
  }
326
319
  res.locals['payload'] = payload;
327
320
  return next();
@@ -2,7 +2,7 @@ import argon2 from 'argon2';
2
2
  import { Router } from 'express';
3
3
  import Joi from 'joi';
4
4
  import { performance } from 'perf_hooks';
5
- import { COOKIE_OPTIONS } from '../../constants.js';
5
+ import { GET_SET_HEADER } from '../../constants.js';
6
6
  import env from '../../env.js';
7
7
  import { InvalidCredentialsException, InvalidPayloadException } from '../../exceptions/index.js';
8
8
  import { respond } from '../../middleware/respond.js';
@@ -74,7 +74,7 @@ export function createLocalAuthRouter(provider) {
74
74
  payload['data']['refresh_token'] = refreshToken;
75
75
  }
76
76
  if (mode === 'cookie') {
77
- res.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], refreshToken, COOKIE_OPTIONS);
77
+ res.setHeader('Set-Cookie', GET_SET_HEADER(refreshToken));
78
78
  }
79
79
  res.locals['payload'] = payload;
80
80
  return next();
@@ -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,12 @@
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';
7
+ import { GET_SET_HEADER } from '../../constants.js';
10
8
  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';
9
+ import { InvalidConfigException, InvalidCredentialsException, InvalidTokenException } from '../../exceptions/index.js';
13
10
  import logger from '../../logger.js';
14
11
  import { respond } from '../../middleware/respond.js';
15
12
  import { AuthenticationService } from '../../services/authentication.js';
@@ -17,14 +14,28 @@ import { UsersService } from '../../services/users.js';
17
14
  import asyncHandler from '../../utils/async-handler.js';
18
15
  import { getConfigFromEnv } from '../../utils/get-config-from-env.js';
19
16
  import { getIPFromReq } from '../../utils/get-ip-from-req.js';
20
- 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) => {
@@ -275,13 +186,7 @@ export function createOAuth2AuthRouter(providerName) {
275
186
  }
276
187
  const { accessToken, refreshToken, expires } = authResponse;
277
188
  if (redirect) {
278
- res.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], refreshToken, {
279
- httpOnly: true,
280
- domain: env['REFRESH_TOKEN_COOKIE_DOMAIN'],
281
- maxAge: getMilliseconds(env['REFRESH_TOKEN_TTL']),
282
- secure: env['REFRESH_TOKEN_COOKIE_SECURE'] ?? false,
283
- sameSite: env['REFRESH_TOKEN_COOKIE_SAME_SITE'] || 'strict',
284
- });
189
+ res.setHeader('Set-Cookie', GET_SET_HEADER(refreshToken));
285
190
  return res.redirect(redirect);
286
191
  }
287
192
  res.locals['payload'] = {
@@ -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,12 @@
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';
7
+ import { GET_SET_HEADER } from '../../constants.js';
10
8
  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';
9
+ import { InvalidConfigException, InvalidCredentialsException, InvalidTokenException } from '../../exceptions/index.js';
13
10
  import logger from '../../logger.js';
14
11
  import { respond } from '../../middleware/respond.js';
15
12
  import { AuthenticationService } from '../../services/authentication.js';
@@ -17,14 +14,28 @@ import { UsersService } from '../../services/users.js';
17
14
  import asyncHandler from '../../utils/async-handler.js';
18
15
  import { getConfigFromEnv } from '../../utils/get-config-from-env.js';
19
16
  import { getIPFromReq } from '../../utils/get-ip-from-req.js';
20
- 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)}`);
@@ -328,13 +211,7 @@ export function createOpenIDAuthRouter(providerName) {
328
211
  }
329
212
  const { accessToken, refreshToken, expires } = authResponse;
330
213
  if (redirect) {
331
- res.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], refreshToken, {
332
- httpOnly: true,
333
- domain: env['REFRESH_TOKEN_COOKIE_DOMAIN'],
334
- maxAge: getMilliseconds(env['REFRESH_TOKEN_TTL']),
335
- secure: env['REFRESH_TOKEN_COOKIE_SECURE'] ?? false,
336
- sameSite: env['REFRESH_TOKEN_COOKIE_SAME_SITE'] || 'strict',
337
- });
214
+ res.setHeader('Set-Cookie', GET_SET_HEADER(refreshToken));
338
215
  return res.redirect(redirect);
339
216
  }
340
217
  res.locals['payload'] = {
@@ -3,7 +3,7 @@ import { BaseException } from '@wbce-d9/exceptions';
3
3
  import express, { Router } from 'express';
4
4
  import * as samlify from 'samlify';
5
5
  import { getAuthProvider } from '../../auth.js';
6
- import { COOKIE_OPTIONS } from '../../constants.js';
6
+ import { COOKIE_OPTIONS, GET_SET_HEADER } from '../../constants.js';
7
7
  import getDatabase from '../../database/index.js';
8
8
  import emitter from '../../emitter.js';
9
9
  import env from '../../env.js';
@@ -121,7 +121,7 @@ export function createSAMLAuthRouter(providerName) {
121
121
  },
122
122
  };
123
123
  if (relayState) {
124
- res.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], refreshToken, COOKIE_OPTIONS);
124
+ res.setHeader('Set-Cookie', GET_SET_HEADER(refreshToken));
125
125
  return res.redirect(relayState);
126
126
  }
127
127
  return next();
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 = {}) {
@@ -9,6 +9,7 @@ export declare const COLUMN_TRANSFORMS: string[];
9
9
  export declare const GENERATE_SPECIAL: string[];
10
10
  export declare const UUID_REGEX = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
11
11
  export declare const COOKIE_OPTIONS: CookieOptions;
12
+ export declare const GET_SET_HEADER: (cookieValue: string) => string;
12
13
  export declare const OAS_REQUIRED_SCHEMAS: string[];
13
14
  /** Formats from which transformation is supported */
14
15
  export declare const SUPPORTED_IMAGE_TRANSFORM_FORMATS: string[];
package/dist/constants.js CHANGED
@@ -55,6 +55,12 @@ export const COOKIE_OPTIONS = {
55
55
  secure: env['REFRESH_TOKEN_COOKIE_SECURE'] ?? false,
56
56
  sameSite: env['REFRESH_TOKEN_COOKIE_SAME_SITE'] || 'strict',
57
57
  };
58
+ export const GET_SET_HEADER = (cookieValue) => {
59
+ const domainHeader = env['REFRESH_TOKEN_COOKIE_DOMAIN'] ? ` Domain=${env['REFRESH_TOKEN_COOKIE_DOMAIN']};` : '';
60
+ const partitionedHeader = env['REFRESH_TOKEN_COOKIE_PARTITIONED'] == false ? '' : ` Partitioned;`;
61
+ const cookieHeaderValue = `${env['REFRESH_TOKEN_COOKIE_NAME']}=${cookieValue}; HttpOnly; Max-Age=${getMilliseconds(env['REFRESH_TOKEN_TTL'])}; Secure=${env['REFRESH_TOKEN_COOKIE_SECURE'] ?? false}; SameSite=${env['REFRESH_TOKEN_COOKIE_SAME_SITE'] || 'strict'}; Path=/;${domainHeader}${partitionedHeader}`;
62
+ return cookieHeaderValue;
63
+ };
58
64
  export const OAS_REQUIRED_SCHEMAS = ['Diff', 'Schema', 'Query', 'x-metadata'];
59
65
  /** Formats from which transformation is supported */
60
66
  export const SUPPORTED_IMAGE_TRANSFORM_FORMATS = ['image/jpeg', 'image/png', 'image/webp', 'image/tiff', 'image/avif'];
@@ -1,6 +1,6 @@
1
1
  import { Router } from 'express';
2
2
  import { createLDAPAuthRouter, createLocalAuthRouter, createOAuth2AuthRouter, createOpenIDAuthRouter, createSAMLAuthRouter, } from '../auth/drivers/index.js';
3
- import { COOKIE_OPTIONS, DEFAULT_AUTH_PROVIDER } from '../constants.js';
3
+ import { DEFAULT_AUTH_PROVIDER, GET_SET_HEADER } from '../constants.js';
4
4
  import env from '../env.js';
5
5
  import { InvalidPayloadException } from '../exceptions/index.js';
6
6
  import logger from '../logger.js';
@@ -68,7 +68,7 @@ router.post('/refresh', asyncHandler(async (req, res, next) => {
68
68
  payload['data']['refresh_token'] = refreshToken;
69
69
  }
70
70
  if (mode === 'cookie') {
71
- res.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], refreshToken, COOKIE_OPTIONS);
71
+ res.setHeader('Set-Cookie', GET_SET_HEADER(refreshToken));
72
72
  }
73
73
  res.locals['payload'] = payload;
74
74
  return next();
@@ -1,7 +1,6 @@
1
1
  import express from 'express';
2
2
  import Joi from 'joi';
3
- import { COOKIE_OPTIONS, UUID_REGEX } from '../constants.js';
4
- import env from '../env.js';
3
+ import { GET_SET_HEADER, UUID_REGEX } from '../constants.js';
5
4
  import { ForbiddenException, InvalidPayloadException } from '../exceptions/index.js';
6
5
  import { respond } from '../middleware/respond.js';
7
6
  import useCollection from '../middleware/use-collection.js';
@@ -25,7 +24,7 @@ router.post('/auth', asyncHandler(async (req, res, next) => {
25
24
  throw new InvalidPayloadException(error.message);
26
25
  }
27
26
  const { accessToken, refreshToken, expires } = await service.login(req.body);
28
- res.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], refreshToken, COOKIE_OPTIONS);
27
+ res.setHeader('Set-Cookie', GET_SET_HEADER(refreshToken));
29
28
  res.locals['payload'] = { data: { access_token: accessToken, expires } };
30
29
  return next();
31
30
  }), respond);
@@ -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.1.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": [
@@ -145,17 +145,17 @@
145
145
  "wellknown": "0.5.0",
146
146
  "@wbce-d9/app": "9.29.0",
147
147
  "@wbce-d9/constants": "9.26.3",
148
+ "@wbce-d9/exceptions": "9.26.3",
148
149
  "@wbce-d9/schema": "9.26.3",
149
150
  "@wbce-d9/extensions-sdk": "9.26.5",
150
- "@wbce-d9/exceptions": "9.26.3",
151
151
  "@wbce-d9/specs": "9.26.3",
152
152
  "@wbce-d9/storage": "9.26.3",
153
153
  "@wbce-d9/storage-driver-azure": "9.26.5",
154
154
  "@wbce-d9/storage-driver-cloudinary": "9.26.5",
155
- "@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",
158
156
  "@wbce-d9/storage-driver-s3": "9.26.7",
157
+ "@wbce-d9/utils": "9.28.0",
158
+ "@wbce-d9/storage-driver-gcs": "9.26.5",
159
159
  "@wbce-d9/update-check": "9.26.3"
160
160
  },
161
161
  "devDependencies": {