@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.
- package/dist/auth/drivers/baseoauth.d.ts +28 -0
- package/dist/auth/drivers/baseoauth.js +162 -0
- package/dist/auth/drivers/ldap.js +2 -9
- package/dist/auth/drivers/local.js +2 -2
- package/dist/auth/drivers/oauth2.d.ts +10 -9
- package/dist/auth/drivers/oauth2.js +26 -121
- package/dist/auth/drivers/openid.d.ts +11 -11
- package/dist/auth/drivers/openid.js +32 -155
- package/dist/auth/drivers/saml.js +2 -2
- package/dist/auth.js +4 -3
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +6 -0
- package/dist/controllers/auth.js +2 -2
- package/dist/controllers/shares.js +2 -3
- package/dist/services/authentication.js +1 -1
- package/package.json +4 -4
|
@@ -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.
|
|
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 {
|
|
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.
|
|
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
|
|
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,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 {
|
|
5
|
+
import { generators, Issuer } from 'openid-client';
|
|
7
6
|
import { getAuthProvider } from '../../auth.js';
|
|
8
|
-
import
|
|
9
|
-
import emitter from '../../emitter.js';
|
|
7
|
+
import { GET_SET_HEADER } from '../../constants.js';
|
|
10
8
|
import env from '../../env.js';
|
|
11
|
-
import {
|
|
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 {
|
|
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) => {
|
|
@@ -275,13 +186,7 @@ export function createOAuth2AuthRouter(providerName) {
|
|
|
275
186
|
}
|
|
276
187
|
const { accessToken, refreshToken, expires } = authResponse;
|
|
277
188
|
if (redirect) {
|
|
278
|
-
res.
|
|
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
|
|
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
|
-
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 {
|
|
5
|
+
import { generators, Issuer } from 'openid-client';
|
|
7
6
|
import { getAuthProvider } from '../../auth.js';
|
|
8
|
-
import
|
|
9
|
-
import emitter from '../../emitter.js';
|
|
7
|
+
import { GET_SET_HEADER } from '../../constants.js';
|
|
10
8
|
import env from '../../env.js';
|
|
11
|
-
import {
|
|
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 {
|
|
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,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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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 = {}) {
|
package/dist/constants.d.ts
CHANGED
|
@@ -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'];
|
package/dist/controllers/auth.js
CHANGED
|
@@ -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 {
|
|
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.
|
|
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 {
|
|
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.
|
|
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": "
|
|
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": {
|