nextauthjs / next-auth

Authentication for the Web.
https://authjs.dev
ISC License
24.1k stars 3.34k forks source link

FusionAuth - Provider Type Conflict #10867

Open alex-fusionauth opened 4 months ago

alex-fusionauth commented 4 months ago

Provider type

FusionAuth

Environment

  System:
    OS: macOS 14.4.1
    CPU: (12) arm64 Apple M2 Pro
    Memory: 192.58 MB / 32.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 21.7.3 - /opt/homebrew/bin/node
    Yarn: 1.22.22 - /opt/homebrew/bin/yarn
    npm: 10.5.0 - /opt/homebrew/bin/npm
    pnpm: 9.0.5 - /opt/homebrew/bin/pnpm
    bun: 1.1.4 - /opt/homebrew/bin/bun
  Browsers:
    Brave Browser: 124.1.65.126
    Chrome: 124.0.6367.119
    Safari: 17.4.1
  npmPackages:
    @auth/sveltekit: ^1.0.1 => 1.0.1 

Reproduction URL

https://github.com/alex-fusionauth/fusionauth-sveltekit

Describe the issue

Within the current provider it is set as type: "oauth".

https://github.com/nextauthjs/next-auth/blob/main/packages/core/src/providers/fusionauth.ts

Then it sets the scopes as requesting openid.

authorization: {
  params: {
    scope: "openid offline_access",

Ideally we would like to have this set to our standard and not require someone to override the provider. Currently this causes errors as it expects to go down the oauth only path and then is trying to fetch openid details without setting it as the correct type. While I haven't seen this in a problem using next-auth I do see it causing more issues in things like SvelteKit using the direct @auth/core package which is used within @auth/sveltekit.

I would like to have our provider updated to reflect the changes in this file https://github.com/alex-fusionauth/fusionauth-sveltekit/blob/afb3d9134aa43f5d540de972692b782928971aa4/complete-application/src/auth.ts

import { SvelteKitAuth } from "@auth/sveltekit"
import FusionAuth from "@auth/core/providers/fusionauth"
import { FUSIONAUTH_ISSUER, FUSIONAUTH_CLIENT_ID, FUSIONAUTH_CLIENT_SECRET, FUSIONAUTH_URL, FUSIONAUTH_TENANT_ID } from "$env/static/private"

const fusionAuth =     FusionAuth({
  issuer: FUSIONAUTH_ISSUER,
  clientId: FUSIONAUTH_CLIENT_ID,
  clientSecret: FUSIONAUTH_CLIENT_SECRET,
  // wellKnown: `${FUSIONAUTH_URL}/.well-known/openid-configuration/${FUSIONAUTH_TENANT_ID}`,
  tenantId: FUSIONAUTH_TENANT_ID, // Only required if you're using multi-tenancy
  authorization: {
    params: {
      scope: "offline_access email openid profile",
      tenantId: FUSIONAUTH_TENANT_ID,
    },
  },
  userinfo: `${FUSIONAUTH_URL}/oauth2/userinfo`,
  // This is due to a known processing issue
  // TODO: https://github.com/nextauthjs/next-auth/issues/8745#issuecomment-1907799026
  token: {
    url: `${FUSIONAUTH_URL}/oauth2/token`,
    conform: async (response: Response) => {
      if (response.status === 401) return response;

      const newHeaders = Array.from(response.headers.entries())
        .filter(([key]) => key.toLowerCase() !== "www-authenticate")
        .reduce((headers, [key, value]) => (headers.append(key, value), headers), new Headers());

      return new Response(response.body, {
        status: response.status,
        statusText: response.statusText,
        headers: newHeaders,
      });
    },
  },
})

// reset to oidc provider
fusionAuth.type = 'oidc';

export const { handle } = SvelteKitAuth({
  providers: [
    fusionAuth
  ],
})

How to reproduce

if you set type back to its default value fusionAuth.type = 'oauth'; you will get an error like below

[auth][error] CallbackRouteError: Read more at https://errors.authjs.dev#callbackrouteerror
[auth][cause]: OperationProcessingError: Unexpected ID Token returned, use processAuthorizationCodeOpenIDResponse() for OpenID Connect callback processing

Expected behavior

PR added: #10868

If you then set it back fusionAuth.type = 'oidc'; it will then have success and you can access details on the profile.

I would like to propose that we update the provider to

export default function FusionAuth<P extends FusionAuthProfile>(
  // tenantId only needed if there is more than one tenant configured on the server
  options: OAuthUserConfig<P> & { tenantId?: string }
): OAuthConfig<P> {
  return {
    id: "fusionauth",
    name: "FusionAuth",
    type: "oidc",
    wellKnown: options?.tenantId
      ? `${options.issuer}/.well-known/openid-configuration?tenantId=${options.tenantId}`
      : `${options.issuer}/.well-known/openid-configuration`,
    authorization: {
      params: {
        scope: "openid offline_access email profile",
        ...(options?.tenantId && { tenantId: options.tenantId }),
      },
    },
  userinfo: `${options.issuer}/oauth2/userinfo`,
  // This is due to a known processing issue
  // TODO: https://github.com/nextauthjs/next-auth/issues/8745#issuecomment-1907799026
  token: {
    url: `${options.issuer}/oauth2/token`,
    conform: async (response: Response) => {
      if (response.status === 401) return response;

      const newHeaders = Array.from(response.headers.entries())
        .filter(([key]) => key.toLowerCase() !== "www-authenticate")
        .reduce((headers, [key, value]) => (headers.append(key, value), headers), new Headers());

      return new Response(response.body, {
        status: response.status,
        statusText: response.statusText,
        headers: newHeaders,
      });
    },
  },
    checks: ["pkce", "state"],
    profile(profile) {
      return {
        id: profile.sub,
        email: profile.email,
        name: profile?.preferred_username,
      }
    },
    options,
  }
}

Also addresses users needing to update to beta but it is not available in core. https://github.com/nextauthjs/next-auth/issues/8745#issuecomment-1907799026

theogravity commented 3 weeks ago

I spent a good 5 hours troubleshooting this until I got to this thread.

Adding in faProvider.type = "oidc"; did fix the issue.

Here's my full configuration. I'm using next-auth@5.0.0-beta.20:

import { serverEnvs } from "@/config/constants.server";
import NextAuth from "next-auth";
import FusionAuthProvider from "next-auth/providers/fusionauth";

const faProvider = FusionAuthProvider({
  issuer: serverEnvs.FUSIONAUTH_ISSUER,
  clientId: serverEnvs.FUSIONAUTH_CLIENT_ID,
  clientSecret: serverEnvs.FUSIONAUTH_CLIENT_SECRET,
  wellKnown: `${serverEnvs.FUSIONAUTH_URL}/.well-known/openid-configuration/${serverEnvs.FUSIONAUTH_TENANT_ID}`,
  tenantId: serverEnvs.FUSIONAUTH_TENANT_ID,
  authorization: {
    url: `${serverEnvs.FUSIONAUTH_URL}/oauth2/authorize`,
    params: {
      scope: "openid offline_access email profile",
      tenantId: serverEnvs.FUSIONAUTH_TENANT_ID,
    },
  },
  userinfo: `${serverEnvs.FUSIONAUTH_URL}/oauth2/userinfo`,
  token: {
    url: `${serverEnvs.FUSIONAUTH_URL}/oauth2/token`,
    conform: async (response: Response) => {
      if (response.status === 401) return response;

      const newHeaders = Array.from(response.headers.entries())
        .filter(([key]) => key.toLowerCase() !== "www-authenticate")
        .reduce((headers, [key, value]) => (headers.append(key, value), headers), new Headers());

      return new Response(response.body, {
        status: response.status,
        statusText: response.statusText,
        headers: newHeaders,
      });
    },
  },
});

// https://github.com/nextauthjs/next-auth/issues/10867
faProvider.type = "oidc";

export const { handlers, signIn, signOut, auth } = NextAuth({
  session: { strategy: "jwt" },
  providers: [faProvider],
  pages: {
    // https://github.com/nextauthjs/next-auth/discussions/4078#discussioncomment-9806999
    signIn: "/api/sign-in",
  },
  callbacks: {
    authorized: async ({ auth }) => {
      // Logged in users are authenticated, otherwise redirect to login page
      return !!auth;
    },
  },
});

And my .env file

NEXTAUTH_URL=http://localhost:3000
FUSIONAUTH_ISSUER=http://localhost:9011
FUSIONAUTH_CLIENT_ID="..."
FUSIONAUTH_TENANT_ID=...
FUSIONAUTH_URL=http://localhost:9011
FUSIONAUTH_CLIENT_SECRET="..."
NEXTAUTH_SECRET="..."

One other thing to note is that you must configure the issuer in the tenant to match the FUSIONAUTH_ISSUER value. I got burned by that one as well as oauth4webapi does a check against the issuer values and I had a different configuration at the time where I forgot to include the tenantId in the authorization.params, which would end up getting the default tenant issuer.