mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
New OpenID and OAuth2 drivers (#8660)
* Moved over oauth impl to new interface * Fixed most build issues and started addind schema to auth drivers * Finished up OAuth2 and OpenID drivers * Removed unused migration and utils * Fixed minor todos * Removed old oauth flow * Changed oauth flow to re-use refresh token * Added new oauth frontend * Added font awesome social icons * Updated authentication documentation * Update api/src/auth/drivers/oauth2.ts * Tested implementation and fixed incorrect validation * Updated docs * Improved OAuth error handling and re-enabled creating users with provider/identifier * Removed Session config from docs * Update app/src/components/v-icon/v-icon.vue * Removed oauth need to define default roleID * Added FormatTitle to SSO links * Prevent local auth without password * Store OAuth access token in session data * Update docs/guides/api-config.md * Fixed copy and removed fontawesome-vue dependency * More docs fixes * Crucialy importend type fiks * Update package-lock * Remove is-email-allowed check In favor of more advanced version based on filtering coming later * Fix JSON type casting * Delete unused util * Update type signature to include name * Add warning when code isn't found in oauth url and remove obsolete imports * Auto-continue on successful SSO login * Tweak type signature * More type casting shenanigans * Please the TS gods * Check for missing token before crashing Co-authored-by: rijkvanzanten <rijkvanzanten@me.com>
This commit is contained in:
@@ -14,9 +14,10 @@ import { TFAService } from './tfa';
|
||||
import { AbstractServiceOptions, Action, SchemaOverview, Session, User, SessionData } from '../types';
|
||||
import { Accountability } from '@directus/shared/types';
|
||||
import { SettingsService } from './settings';
|
||||
import { merge, clone, cloneDeep } from 'lodash';
|
||||
import { merge, clone, cloneDeep, omit } from 'lodash';
|
||||
import { performance } from 'perf_hooks';
|
||||
import { stall } from '../utils/stall';
|
||||
import logger from '../logger';
|
||||
|
||||
const loginAttemptsLimiter = createRateLimiter({ duration: 0 });
|
||||
|
||||
@@ -60,7 +61,8 @@ export class AuthenticationService {
|
||||
'role',
|
||||
'tfa_secret',
|
||||
'provider',
|
||||
'external_identifier'
|
||||
'external_identifier',
|
||||
'auth_data'
|
||||
)
|
||||
.from('directus_users')
|
||||
.where('id', await provider.getUserID(cloneDeep(payload)))
|
||||
@@ -191,7 +193,7 @@ export class AuthenticationService {
|
||||
expires: refreshTokenExpiration,
|
||||
ip: this.accountability?.ip,
|
||||
user_agent: this.accountability?.userAgent,
|
||||
data: sessionData,
|
||||
data: sessionData && JSON.stringify(sessionData),
|
||||
});
|
||||
|
||||
await this.knex('directus_sessions').delete().where('expires', '<', new Date());
|
||||
@@ -242,7 +244,8 @@ export class AuthenticationService {
|
||||
'u.status',
|
||||
'u.role',
|
||||
'u.provider',
|
||||
'u.external_identifier'
|
||||
'u.external_identifier',
|
||||
'u.auth_data'
|
||||
)
|
||||
.from('directus_sessions as s')
|
||||
.innerJoin('directus_users as u', 's.user', 'u.id')
|
||||
@@ -253,10 +256,20 @@ export class AuthenticationService {
|
||||
throw new InvalidCredentialsException();
|
||||
}
|
||||
|
||||
const { data: sessionData, ...user } = record;
|
||||
let { data: sessionData } = record;
|
||||
const user = omit(record, 'data');
|
||||
|
||||
if (typeof sessionData === 'string') {
|
||||
try {
|
||||
sessionData = JSON.parse(sessionData);
|
||||
} catch {
|
||||
logger.warn(`Session data isn't valid JSON: ${sessionData}`);
|
||||
}
|
||||
}
|
||||
|
||||
const provider = getAuthProvider(user.provider);
|
||||
await provider.refresh(clone(user), sessionData);
|
||||
|
||||
const newSessionData = await provider.refresh(clone(user), sessionData as SessionData);
|
||||
|
||||
const accessToken = jwt.sign({ id: user.id }, env.SECRET as string, {
|
||||
expiresIn: env.ACCESS_TOKEN_TTL,
|
||||
@@ -267,7 +280,11 @@ export class AuthenticationService {
|
||||
const refreshTokenExpiration = new Date(Date.now() + ms(env.REFRESH_TOKEN_TTL as string));
|
||||
|
||||
await this.knex('directus_sessions')
|
||||
.update({ token: newRefreshToken, expires: refreshTokenExpiration })
|
||||
.update({
|
||||
token: newRefreshToken,
|
||||
expires: refreshTokenExpiration,
|
||||
data: newSessionData && JSON.stringify(newSessionData),
|
||||
})
|
||||
.where({ token: refreshToken });
|
||||
|
||||
await this.knex('directus_users').update({ last_access: new Date() }).where({ id: user.id });
|
||||
@@ -292,6 +309,7 @@ export class AuthenticationService {
|
||||
'u.role',
|
||||
'u.provider',
|
||||
'u.external_identifier',
|
||||
'u.auth_data',
|
||||
's.data'
|
||||
)
|
||||
.from('directus_sessions as s')
|
||||
@@ -300,10 +318,19 @@ export class AuthenticationService {
|
||||
.first();
|
||||
|
||||
if (record) {
|
||||
const { data: sessionData, ...user } = record;
|
||||
let { data: sessionData } = record;
|
||||
const user = omit(record, 'data');
|
||||
|
||||
if (typeof sessionData === 'string') {
|
||||
try {
|
||||
sessionData = JSON.parse(sessionData);
|
||||
} catch {
|
||||
logger.warn(`Session data isn't valid JSON: ${sessionData}`);
|
||||
}
|
||||
}
|
||||
|
||||
const provider = getAuthProvider(user.provider);
|
||||
await provider.logout(clone(user), sessionData);
|
||||
await provider.logout(clone(user), sessionData as SessionData);
|
||||
|
||||
await this.knex.delete().from('directus_sessions').where('token', refreshToken);
|
||||
}
|
||||
@@ -320,7 +347,8 @@ export class AuthenticationService {
|
||||
'status',
|
||||
'role',
|
||||
'provider',
|
||||
'external_identifier'
|
||||
'external_identifier',
|
||||
'auth_data'
|
||||
)
|
||||
.from('directus_users')
|
||||
.where('id', userID)
|
||||
|
||||
@@ -82,21 +82,23 @@ export class UsersService extends ItemsService {
|
||||
fields: ['auth_password_policy'],
|
||||
});
|
||||
|
||||
if (policyRegExString) {
|
||||
const wrapped = policyRegExString.startsWith('/') && policyRegExString.endsWith('/');
|
||||
const regex = new RegExp(wrapped ? policyRegExString.slice(1, -1) : policyRegExString);
|
||||
if (!policyRegExString) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const password of passwords) {
|
||||
if (regex.test(password) === false) {
|
||||
throw new FailedValidationException({
|
||||
message: `Provided password doesn't match password policy`,
|
||||
path: ['password'],
|
||||
type: 'custom.pattern.base',
|
||||
context: {
|
||||
value: password,
|
||||
},
|
||||
});
|
||||
}
|
||||
const wrapped = policyRegExString.startsWith('/') && policyRegExString.endsWith('/');
|
||||
const regex = new RegExp(wrapped ? policyRegExString.slice(1, -1) : policyRegExString);
|
||||
|
||||
for (const password of passwords) {
|
||||
if (!regex.test(password)) {
|
||||
throw new FailedValidationException({
|
||||
message: `Provided password doesn't match password policy`,
|
||||
path: ['password'],
|
||||
type: 'custom.pattern.base',
|
||||
context: {
|
||||
value: password,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,16 +143,6 @@ export class UsersService extends ItemsService {
|
||||
await this.checkPasswordPolicy(passwords);
|
||||
}
|
||||
|
||||
for (const user of data) {
|
||||
if (user.provider !== undefined) {
|
||||
throw new InvalidPayloadException(`You can't set the "provider" value manually.`);
|
||||
}
|
||||
|
||||
if (user.external_identifier !== undefined) {
|
||||
throw new InvalidPayloadException(`You can't set the "external_identifier" value manually.`);
|
||||
}
|
||||
}
|
||||
|
||||
return await super.createMany(data, opts);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user