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:
Aiden Foxx
2021-10-21 23:45:01 +02:00
committed by GitHub
parent 1b64b4472a
commit fa3b1171e8
36 changed files with 1747 additions and 822 deletions

View File

@@ -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)

View File

@@ -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);
}