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

@@ -1,23 +1,13 @@
import { Router } from 'express';
import grant from 'grant';
import ms from 'ms';
import emitter, { emitAsyncSafe } from '../emitter';
import env from '../env';
import {
InvalidCredentialsException,
RouteNotFoundException,
ServiceUnavailableException,
InvalidPayloadException,
} from '../exceptions';
import grantConfig from '../grant';
import { InvalidPayloadException } from '../exceptions';
import { respond } from '../middleware/respond';
import { AuthenticationService, UsersService } from '../services';
import asyncHandler from '../utils/async-handler';
import { getAuthProviders } from '../utils/get-auth-providers';
import getEmailFromProfile from '../utils/get-email-from-profile';
import { toArray } from '@directus/shared/utils';
import logger from '../logger';
import { createLocalAuthRouter } from '../auth/drivers';
import { createLocalAuthRouter, createOAuth2AuthRouter, createOpenIDAuthRouter } from '../auth/drivers';
import { DEFAULT_AUTH_PROVIDER } from '../constants';
const router = Router();
@@ -30,6 +20,15 @@ for (const authProvider of authProviders) {
switch (authProvider.driver) {
case 'local':
authRouter = createLocalAuthRouter(authProvider.name);
break;
case 'oauth2':
authRouter = createOAuth2AuthRouter(authProvider.name);
break;
case 'openid':
authRouter = createOpenIDAuthRouter(authProvider.name);
break;
}
if (!authRouter) {
@@ -189,156 +188,4 @@ router.get(
respond
);
router.get(
'/oauth',
asyncHandler(async (req, res, next) => {
const providers = toArray(env.OAUTH_PROVIDERS);
res.locals.payload = { data: env.OAUTH_PROVIDERS ? providers : null };
return next();
}),
respond
);
router.get(
'/oauth/:provider',
asyncHandler(async (req, res, next) => {
const config = { ...grantConfig };
delete config.defaults;
const availableProviders = Object.keys(config);
if (availableProviders.includes(req.params.provider) === false) {
throw new RouteNotFoundException(`/auth/oauth/${req.params.provider}`);
}
if (req.query?.redirect && req.session) {
req.session.redirect = req.query.redirect as string;
}
const hookPayload = {
provider: req.params.provider,
redirect: req.query?.redirect,
};
emitAsyncSafe(`oauth.${req.params.provider}.redirect`, {
event: `oauth.${req.params.provider}.redirect`,
action: 'redirect',
schema: req.schema,
payload: hookPayload,
accountability: req.accountability,
user: null,
});
await emitter.emitAsync(`oauth.${req.params.provider}.redirect.before`, {
event: `oauth.${req.params.provider}.redirect.before`,
action: 'redirect',
schema: req.schema,
payload: hookPayload,
accountability: req.accountability,
user: null,
});
next();
})
);
router.use(grant.express()(grantConfig));
router.get(
'/oauth/:provider/callback',
asyncHandler(async (req, res, next) => {
const redirect = req.session.redirect;
const accountability = {
ip: req.ip,
userAgent: req.get('user-agent'),
role: null,
};
const authenticationService = new AuthenticationService({
accountability: accountability,
schema: req.schema,
});
let authResponse: { accessToken: any; refreshToken: any; expires: any; id?: any };
const hookPayload = req.session.grant.response;
await emitter.emitAsync(`oauth.${req.params.provider}.login.before`, hookPayload, {
event: `oauth.${req.params.provider}.login.before`,
action: 'oauth.login',
schema: req.schema,
payload: hookPayload,
accountability: accountability,
status: 'pending',
user: null,
});
const emitStatus = (status: 'fail' | 'success') => {
emitAsyncSafe(`oauth.${req.params.provider}.login`, hookPayload, {
event: `oauth.${req.params.provider}.login`,
action: 'oauth.login',
schema: req.schema,
payload: hookPayload,
accountability: accountability,
status,
user: null,
});
};
try {
const email = getEmailFromProfile(req.params.provider, req.session.grant.response?.profile);
req.session?.destroy(() => {
// Do nothing
});
// Workaround to use the default local auth provider to validate
// the email and login without a password.
authResponse = await authenticationService.login(DEFAULT_AUTH_PROVIDER, { email });
} catch (error: any) {
emitStatus('fail');
logger.warn(error);
if (redirect) {
let reason = 'UNKNOWN_EXCEPTION';
if (error instanceof ServiceUnavailableException) {
reason = 'SERVICE_UNAVAILABLE';
} else if (error instanceof InvalidCredentialsException) {
reason = 'INVALID_USER';
}
return res.redirect(`${redirect.split('?')[0]}?reason=${reason}`);
}
throw error;
}
const { accessToken, refreshToken, expires } = authResponse;
emitStatus('success');
if (redirect) {
res.cookie(env.REFRESH_TOKEN_COOKIE_NAME, refreshToken, {
httpOnly: true,
domain: env.REFRESH_TOKEN_COOKIE_DOMAIN,
maxAge: ms(env.REFRESH_TOKEN_TTL as string),
secure: env.REFRESH_TOKEN_COOKIE_SECURE ?? false,
sameSite: (env.REFRESH_TOKEN_COOKIE_SAME_SITE as 'lax' | 'strict' | 'none') || 'strict',
});
return res.redirect(redirect);
} else {
res.locals.payload = {
data: { access_token: accessToken, refresh_token: refreshToken, expires },
};
return next();
}
}),
respond
);
export default router;