101 lines
3.8 KiB
TypeScript
101 lines
3.8 KiB
TypeScript
import passport from "@outlinewiki/koa-passport";
|
|
import { Context } from "koa";
|
|
import { InternalOAuthError } from "passport-oauth2";
|
|
import { Client } from "@shared/types";
|
|
import env from "@server/env";
|
|
import { AuthenticationError, OAuthStateMismatchError } from "@server/errors";
|
|
import Logger from "@server/logging/Logger";
|
|
import { AuthenticationResult } from "@server/types";
|
|
import { signIn } from "@server/utils/authentication";
|
|
import { parseState } from "@server/utils/passport";
|
|
|
|
export default function createMiddleware(providerName: string) {
|
|
return function passportMiddleware(ctx: Context) {
|
|
return passport.authorize(
|
|
providerName,
|
|
{
|
|
session: false,
|
|
},
|
|
async (err, user, result: AuthenticationResult) => {
|
|
if (err) {
|
|
Logger.error(
|
|
"Error during authentication",
|
|
err instanceof InternalOAuthError ? err.oauthError : err
|
|
);
|
|
|
|
if (err.id) {
|
|
const notice = err.id.replace(/_/g, "-");
|
|
const redirectPath = err.redirectPath ?? "/";
|
|
const hasQueryString = redirectPath?.includes("?");
|
|
|
|
// Every authentication action is routed through the apex domain.
|
|
// But when there is an error, we want to redirect the user on the
|
|
// same domain or subdomain that they originated from (found in state).
|
|
|
|
// get original host
|
|
const stateString = ctx.cookies.get("state");
|
|
const state = stateString ? parseState(stateString) : undefined;
|
|
|
|
// form a URL object with the err.redirectPath and replace the host
|
|
const reqProtocol =
|
|
state?.client === Client.Desktop ? "outline" : ctx.protocol;
|
|
|
|
// `state.host` cannot be trusted if the error is a state mismatch, use `ctx.hostname`
|
|
const requestHost =
|
|
err instanceof OAuthStateMismatchError
|
|
? ctx.hostname
|
|
: state?.host ?? ctx.hostname;
|
|
const url = new URL(
|
|
env.isCloudHosted
|
|
? `${reqProtocol}://${requestHost}${redirectPath}`
|
|
: `${env.URL}${redirectPath}`
|
|
);
|
|
|
|
return ctx.redirect(
|
|
`${url.toString()}${hasQueryString ? "&" : "?"}notice=${notice}`
|
|
);
|
|
}
|
|
|
|
if (env.isDevelopment) {
|
|
throw err;
|
|
}
|
|
|
|
return ctx.redirect(`/?notice=auth-error`);
|
|
}
|
|
|
|
// Passport.js may invoke this callback with err=null and user=null in
|
|
// the event that error=access_denied is received from the OAuth server.
|
|
// I'm not sure why this exception to the rule exists, but it does:
|
|
// https://github.com/jaredhanson/passport-oauth2/blob/e20f26aad60ed54f0e7952928cbb64979ef8da2b/lib/strategy.js#L135
|
|
if (!user && !result?.user) {
|
|
Logger.error(
|
|
"No user returned during authentication",
|
|
AuthenticationError()
|
|
);
|
|
return ctx.redirect(`/?notice=auth-error`);
|
|
}
|
|
|
|
// Handle errors from Azure which come in the format: message, Trace ID,
|
|
// Correlation ID, Timestamp in these two query string parameters.
|
|
const { error, error_description } = ctx.request.query;
|
|
|
|
if (error && error_description) {
|
|
Logger.error(
|
|
"Error from Azure during authentication",
|
|
new Error(String(error_description))
|
|
);
|
|
// Display only the descriptive message to the user, log the rest
|
|
const description = String(error_description).split("Trace ID")[0];
|
|
return ctx.redirect(`/?notice=auth-error&description=${description}`);
|
|
}
|
|
|
|
if (result.user.isSuspended) {
|
|
return ctx.redirect("/?notice=user-suspended");
|
|
}
|
|
|
|
await signIn(ctx, providerName, result);
|
|
}
|
|
)(ctx);
|
|
};
|
|
}
|