mirror of
https://github.com/directus/directus.git
synced 2026-02-16 06:24:56 -05: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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user