diff --git a/server/env.ts b/server/env.ts index 4864e06bdb6b..4bab9883584d 100644 --- a/server/env.ts +++ b/server/env.ts @@ -543,10 +543,11 @@ export class Environment { /** * The authentication type to use. When set to "SSO", the server will trust - * X-Auth-Request-Email and X-Auth-Request-User headers injected by a reverse - * proxy (e.g. oauth2-proxy, Authelia) for authentication and automatic user - * provisioning. Only enable this when Outline is deployed behind a trusted - * authenticating proxy on a self-hosted instance. + * the X-Auth-Request-Email header injected by a reverse proxy + * (e.g. oauth2-proxy, Authelia) for authentication and automatic user + * provisioning. The display name is derived from the email local-part. + * Only enable this when Outline is deployed behind a trusted authenticating + * proxy on a self-hosted instance. */ @Public @IsOptional() diff --git a/server/middlewares/authentication.test.ts b/server/middlewares/authentication.test.ts index 4de2ef760ec6..802485fa281b 100644 --- a/server/middlewares/authentication.test.ts +++ b/server/middlewares/authentication.test.ts @@ -511,9 +511,6 @@ describe("Authentication middleware", () => { if (header === "x-auth-request-email") { return newEmail; } - if (header === "x-auth-request-user") { - return "New User"; - } return ""; }), }, @@ -530,36 +527,6 @@ describe("Authentication middleware", () => { where: { email: newEmail.toLowerCase() }, }); expect(provisioned).not.toBeNull(); - expect(state.auth.user.email).toEqual(newEmail.toLowerCase()); - expect(state.auth.user.name).toEqual("New User"); - }); - - it("should use email prefix as name when X-Auth-Request-User is absent", async () => { - await buildTeam(); - const state = {} as DefaultState; - const authMiddleware = auth(); - const newEmail = `prefix-${randomString(6)}@example.com`; - - await authMiddleware( - { - // @ts-expect-error mock request - request: { - get: jest.fn((header: string) => { - if (header === "x-auth-request-email") { - return newEmail; - } - return ""; - }), - }, - // @ts-expect-error mock cookies - cookies: { get: jest.fn(() => undefined), set: jest.fn() }, - state, - ip: "127.0.0.1", - cache: {}, - }, - jest.fn() - ); - expect(state.auth.user.email).toEqual(newEmail.toLowerCase()); expect(state.auth.user.name).toEqual( newEmail.toLowerCase().split("@")[0] @@ -601,6 +568,37 @@ describe("Authentication middleware", () => { } }); + it("should reject a forwarded email with no local part", async () => { + await buildTeam(); + const state = {} as DefaultState; + const authMiddleware = auth(); + + // "@example.com" normalises to "@" with an empty + // local part. User.name enforces min length 1, so we reject up front + // rather than letting provisioning fail with an opaque validation error. + await expect( + authMiddleware( + { + // @ts-expect-error mock request + request: { + get: jest.fn((header: string) => { + if (header === "x-auth-request-email") { + return "@example.com"; + } + return ""; + }), + }, + // @ts-expect-error mock cookies + cookies: { get: jest.fn(() => undefined), set: jest.fn() }, + state, + ip: "127.0.0.1", + cache: {}, + }, + jest.fn() + ) + ).rejects.toThrow("Invalid forwarded email: missing local part"); + }); + it("should not match existing users via SQL LIKE wildcard characters", async () => { const team = await buildTeam(); const existingUser = await buildUser({ teamId: team.id }); diff --git a/server/middlewares/authentication.ts b/server/middlewares/authentication.ts index 2e9db16aac92..9e68c502c82b 100644 --- a/server/middlewares/authentication.ts +++ b/server/middlewares/authentication.ts @@ -370,9 +370,16 @@ async function validateAuthentication( service = FORWARDAUTH_SERVICE; const email = normalizeProxyEmail(token.slice(4)); - const localPart = email.split("@")[0]; - const displayName = ctx.request.get("x-auth-request-user") || localPart; - const { domain } = parseEmail(email); + const { local: localPart, domain } = parseEmail(email); + + // A malformed forwarded email with no local part (e.g. "@example.com") + // normalises to "@" and yields an empty localPart. + // User.name enforces a min length of 1, so provisioning would otherwise + // fail with an opaque validation error — reject explicitly so the failure + // mode is deterministic. + if (!localPart) { + throw AuthenticationError("Invalid forwarded email: missing local part"); + } // Concurrent-creation race guard. The SPA on first-ever login fires // multiple parallel API requests (docs, team, access tokens, …) with @@ -444,7 +451,7 @@ async function validateAuthentication( }); const created = await User.create( { - name: displayName, + name: localPart, email, teamId: team.id, // First user into a brand-new team becomes admin.