fix: refactor auth flow to explicitly pass in a host (#3909)

* fix: refactor auth flow to explicitly pass in a host

* add new error handler to all SSO providers

* refactor passport error into middleware
This commit is contained in:
Nan Yu
2022-08-04 05:00:52 -04:00
committed by GitHub
parent 0a6cfe5a6a
commit 74d9409cc3
4 changed files with 38 additions and 42 deletions

View File

@@ -89,11 +89,15 @@ function AuthenticationProvider(props: Props) {
);
}
// If we're on a custom domain then the auth must point to the root
// app.getoutline.com for authentication so that the state cookie can be set
// and read.
const isCustomDomain = parseDomain(window.location.origin).custom;
const href = `${isCustomDomain ? env.URL : ""}${authUrl}`;
// If we're on a custom domain or a subdomain then the auth must point to the
// apex (env.URL) for authentication so that the state cookie can be set and read.
// We pass the host into the auth URL so that the server can redirect on error
// and keep the user on the same page.
const { custom, teamSubdomain, host } = parseDomain(window.location.origin);
const needsRedirect = custom || teamSubdomain;
const href = needsRedirect
? `${env.URL}${authUrl}?host=${encodeURI(host)}`
: authUrl;
return (
<Wrapper>

View File

@@ -161,16 +161,6 @@ export function GmailAccountCreationError(
});
}
export function AuthRedirectError(
message = "Redirect to the correct domain after authentication",
redirectUrl: string
) {
return httpErrors(400, message, {
id: "auth_redirect",
redirectUrl,
});
}
export function OIDCMalformedUserInfoError(
message = "User profile information malformed"
) {

View File

@@ -3,6 +3,7 @@ import { Context } from "koa";
import env from "@server/env";
import Logger from "@server/logging/Logger";
import { signIn } from "@server/utils/authentication";
import { parseState } from "@server/utils/passport";
import { AccountProvisionerResult } from "../commands/accountProvisioner";
export default function createMiddleware(providerName: string) {
@@ -18,12 +19,28 @@ export default function createMiddleware(providerName: string) {
if (err.id) {
const notice = err.id.replace(/_/g, "-");
const hasQueryString = err.redirectUrl?.includes("?");
const redirectUrl = err.redirectUrl ?? "/";
const hasQueryString = redirectUrl?.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 state = ctx.cookies.get("state");
const host = state ? parseState(state).host : ctx.hostname;
// form a URL object with the err.redirectUrl and replace the host
const reqProtocol = ctx.protocol;
const requestHost = ctx.get("host");
const url = new URL(
`${reqProtocol}://${requestHost}${redirectUrl}`
);
url.host = host;
return ctx.redirect(
`${err.redirectUrl || "/"}${
hasQueryString ? "&" : "?"
}notice=${notice}`
`${url.toString()}${hasQueryString ? "&" : "?"}notice=${notice}`
);
}

View File

@@ -9,18 +9,19 @@ import {
import { getCookieDomain, parseDomain } from "@shared/utils/domains";
import env from "@server/env";
import { Team } from "@server/models";
import { AuthRedirectError, OAuthStateMismatchError } from "../errors";
import { OAuthStateMismatchError } from "../errors";
export class StateStore {
key = "state";
store = (ctx: Context, callback: StateStoreStoreCallback) => {
// token is a short lived one-time pad to prevent replay attacks
// appDomain is the domain the user originated from when attempting auth
// we expect it to be a team subdomain, custom domain, or apex domain
const token = crypto.randomBytes(8).toString("hex");
const appDomain = parseDomain(ctx.hostname);
const state = buildState(appDomain.host, token);
// We expect host to be a team subdomain, custom domain, or apex domain
// that is passed via query param from the auth provider component.
const host = ctx.query.host?.toString() || parseDomain(ctx.hostname).host;
const state = buildState(host, token);
ctx.cookies.set(this.key, state, {
httpOnly: false,
@@ -46,24 +47,7 @@ export class StateStore {
);
}
const { host, token } = parseState(state);
// Oauth callbacks are hard-coded to come to the apex domain, so we
// redirect to the original app domain before attempting authentication.
// If there is an error during auth, the user will end up on the same domain
// that they started from.
const appDomain = parseDomain(host);
if (appDomain.host !== parseDomain(ctx.hostname).host) {
const reqProtocol = ctx.protocol;
const requestHost = ctx.get("host");
const requestPath = ctx.originalUrl;
const requestUrl = `${reqProtocol}://${requestHost}${requestPath}`;
const url = new URL(requestUrl);
url.host = appDomain.host;
return callback(AuthRedirectError(``, url.toString()), false, token);
}
const { token } = parseState(state);
// Destroy the one-time pad token and ensure it matches
ctx.cookies.set(this.key, "", {
@@ -106,6 +90,7 @@ export async function getTeamFromContext(ctx: Context) {
// we use it to infer the team they intend on signing into
const state = ctx.cookies.get("state");
const host = state ? parseState(state).host : ctx.hostname;
const domain = parseDomain(host);
let team;