@directus/api
Version:
Directus is a real-time API and App dashboard for managing SQL database content
161 lines (159 loc) • 6.82 kB
JavaScript
import async_handler_default from "../../utils/async-handler.js";
import { getConfigFromEnv } from "../../utils/get-config-from-env.js";
import { useLogger } from "../../logger/index.js";
import { REFRESH_COOKIE_OPTIONS, SESSION_COOKIE_OPTIONS } from "../../constants.js";
import database_default from "../../database/index.js";
import emitter_default from "../../emitter.js";
import { getSchema } from "../../utils/get-schema.js";
import { respond } from "../../middleware/respond.js";
import { LocalAuthDriver } from "./local.js";
import { checkSsoEnabled } from "../utils/check-sso-enabled.js";
import { resolveLoginRedirect } from "../utils/resolve-login-redirect.js";
import { getAuthProvider } from "../../auth.js";
import { AuthenticationService } from "../../services/authentication.js";
import { useEnv } from "@directus/env";
import { ErrorCode, InvalidCredentialsError, InvalidPayloadError, InvalidProviderError, isDirectusError } from "@directus/errors";
import express, { Router } from "express";
import * as validator from "@authenio/samlify-node-xmllint";
import * as samlify from "samlify";
//#region src/auth/drivers/saml.ts
samlify.setSchemaValidator(validator);
var SAMLAuthDriver = class extends LocalAuthDriver {
sp;
idp;
config;
constructor(options, config) {
super(options, config);
this.config = config;
this.sp = samlify.ServiceProvider(getConfigFromEnv(`AUTH_${config["provider"].toUpperCase()}_SP`));
this.idp = samlify.IdentityProvider(getConfigFromEnv(`AUTH_${config["provider"].toUpperCase()}_IDP`));
}
async fetchUserID(identifier) {
return (await this.knex.select("id").from("directus_users").whereRaw("LOWER(??) = ?", ["external_identifier", identifier.toLowerCase()]).first())?.id;
}
async getUserID(payload) {
const logger = useLogger();
const { provider, emailKey, identifierKey, givenNameKey, familyNameKey, allowPublicRegistration } = this.config;
const email = payload[emailKey ?? "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"];
const identifier = payload[identifierKey || "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"];
if (!identifier) {
logger.warn(`[SAML] Failed to find user identifier for provider "${provider}"`);
throw new InvalidCredentialsError();
}
const userID = await this.fetchUserID(identifier);
if (userID) return userID;
if (!allowPublicRegistration) {
logger.warn(`[SAML] User doesn't exist, and public registration not allowed for provider "${provider}"`);
throw new InvalidCredentialsError();
}
const userPayload = {
provider,
first_name: payload[givenNameKey ?? "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"],
last_name: payload[familyNameKey ?? "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"],
email,
external_identifier: identifier.toLowerCase(),
role: this.config["defaultRoleId"]
};
const schema = await getSchema();
const updatedUserPayload = await emitter_default.emitFilter(`auth.create`, userPayload, {
identifier: identifier.toLowerCase(),
provider: this.config["provider"],
providerPayload: { ...payload }
}, {
database: database_default(),
schema,
accountability: null
});
try {
return await this.getUsersService(schema).createOne(updatedUserPayload);
} catch (error) {
if (isDirectusError(error, ErrorCode.RecordNotUnique)) {
logger.warn(error, "[SAML] Failed to register user. User not unique");
throw new InvalidProviderError();
}
throw error;
}
}
async login(_user) {}
};
function createSAMLAuthRouter(providerName) {
const router = Router();
const env = useEnv();
router.use(checkSsoEnabled);
router.get("/metadata", async_handler_default(async (_req, res) => {
const { sp } = getAuthProvider(providerName);
return res.header("Content-Type", "text/xml").send(sp.getMetadata());
}));
router.get("/", async_handler_default(async (req, res) => {
const { sp, idp } = getAuthProvider(providerName);
const { context: url } = sp.createLoginRequest(idp, "redirect");
const parsedUrl = new URL(url);
if (req.query["redirect"]) {
let redirect = req.query["redirect"];
try {
redirect = resolveLoginRedirect(redirect, { provider: providerName });
} catch (e) {
useLogger().error(e);
throw new InvalidPayloadError({ reason: `URL "${redirect}" can't be used to redirect after login` });
}
parsedUrl.searchParams.append("RelayState", redirect);
}
return res.redirect(parsedUrl.toString());
}));
router.post("/logout", async_handler_default(async (req, res) => {
const { sp, idp } = getAuthProvider(providerName);
const { context } = sp.createLogoutRequest(idp, "redirect", req.body);
const authService = new AuthenticationService({
accountability: req.accountability,
schema: req.schema
});
const sessionCookieName = env["SESSION_COOKIE_NAME"];
if (req.cookies[sessionCookieName]) {
await authService.logout(req.cookies[sessionCookieName]);
res.clearCookie(sessionCookieName, SESSION_COOKIE_OPTIONS);
}
return res.redirect(context);
}));
router.post("/acs", express.urlencoded({ extended: false }), async_handler_default(async (req, res, next) => {
const logger = useLogger();
let redirect = req.body?.RelayState;
const authMode = env[`AUTH_${providerName.toUpperCase()}_MODE`] ?? "session";
if (redirect) try {
redirect = resolveLoginRedirect(redirect, { provider: providerName });
} catch (e) {
useLogger().error(e);
throw new InvalidPayloadError({ reason: `URL "${redirect}" can't be used to redirect after login` });
}
try {
const { sp, idp } = getAuthProvider(providerName);
const { extract } = await sp.parseLoginResponse(idp, "post", req);
const { accessToken, refreshToken, expires } = await new AuthenticationService({
accountability: req.accountability,
schema: req.schema
}).login(providerName, extract.attributes, { session: authMode === "session" });
res.locals["payload"] = { data: {
access_token: accessToken,
refresh_token: refreshToken,
expires
} };
if (redirect) {
if (authMode === "session") res.cookie(env["SESSION_COOKIE_NAME"], accessToken, SESSION_COOKIE_OPTIONS);
else res.cookie(env["REFRESH_TOKEN_COOKIE_NAME"], refreshToken, REFRESH_COOKIE_OPTIONS);
return res.redirect(redirect);
}
return next();
} catch (error) {
if (redirect) {
let reason = "UNKNOWN_EXCEPTION";
if (isDirectusError(error)) reason = error.code;
else logger.warn(error, `[SAML] Unexpected error during SAML login`);
return res.redirect(`${redirect.split("?")[0]}?reason=${reason}`);
}
logger.warn(error, `[SAML] Unexpected error during SAML login`);
throw error;
}
}), respond);
return router;
}
//#endregion
export { SAMLAuthDriver, createSAMLAuthRouter };