Implementing Salesforce login in NextAuth

NextAuth makes authentication super easy in Next JS. Getting SalesForce oauth flow working required a little digging though.

I set up a SalesForceProvider with NextAuth using the example:

// /api/auth/[...nextauth].ts
import NextAuth from "next-auth/next";
import SalesFoceProvider from "next-auth/providers/salesforce";

export default NextAuth({
  debug: true,
  secret: "NEXTAUTH_SECRET",
  providers: [
    SalesFoceProvider({
      clientId: "SALESFORCE_CLIENT_ID",
      clientSecret: "SALESFORCE_CLIENT_SECRET",
      },
    }),
  ],
});

But got stuck at this callback error:

[next-auth][error][OAUTH_CALLBACK_ERROR]
https://next-auth.js.org/errors#oauth_callback_error id_token detected in the response, you must use client.callback() instead of client.oauthCallback() {
  error: {
    message: 'id_token detected in the response, you must use client.callback() instead of client.oauthCallback()',
    stack: 'RPError: id_token detected in the response, you must use client.callback() instead of client.oauthCallback()\n' +
      '    at Client.oauthCallback (node_modules/openid-client/lib/client.js:626:15)\n' +
      '    at runMicrotasks (<anonymous>)\n' +
      '    at processTicksAndRejections (internal/process/task_queues.js:95:5)\n' +
      '    at async oAuthCallback (node_modules/next-auth/core/lib/oauth/callback.js:114:16)\n' +
      '    at async Object.callback (node_modules/next-auth/core/routes/callback.js:50:11)\n' +
      '    at async NextAuthHandler (node_modules/next-auth/core/index.js:141:28)\n' +
      '    at async NextAuthNextHandler (node_modules/next-auth/next/index.js:20:19)\n' +
      '    at async node_modules/next-auth/next/index.js:56:32\n' +
      '    at async Object.apiResolver (node_modules/next/dist/server/api-utils.js:102:9)\n' +
      '    at async DevServer.handleApiRequest (node_modules/next/dist/server/next-server.js:1064:9)',
    name: 'RPError'
  },
  providerId: 'salesforce',
  message: 'id_token detected in the response, you must use client.callback() instead of client.oauthCallback()'
}

After some investigation I realized I needed to add scope as an authorization parameter and voilá - the login flow is working.

// /api/auth/[...nextauth].ts
import NextAuth from "next-auth/next";
import SalesFoceProvider from "next-auth/providers/salesforce";

export default NextAuth({
  debug: true,
  secret: "NEXTAUTH_SECRET",
  providers: [
    SalesFoceProvider({
      clientId: "SALESFORCE_CLIENT_ID",
      clientSecret: "SALESFORCE_CLIENT_SECRET",
      authorization: {
        params: {
          scope: "api id web",
        },
      },
    }),
  ],
});

Custom login/community users

In order to be able to log in with community users and redirect the users to our custom login page, we need to override the default SalesForceProvider settings.

// .env.local
SALESFORCE_URL="https://myhostname-dev-ed.lightning.force.com"
SALESFORCE_CLIENT_ID=""
SALESFORCE_CLIENT_SECRET=""
// /api/auth/[...nextauth].ts
import NextAuth from "next-auth/next";
import SalesFoceProvider from "next-auth/providers/salesforce";

const {
  NEXTAUTH_SECRET,
  SALESFORCE_URL,
  SALESFORCE_CLIENT_ID,
  SALESFORCE_CLIENT_SECRET,
} = process.env;

export default NextAuth({
  debug: true,
  secret: NEXTAUTH_SECRET,
  providers: [
    SalesFoceProvider({
      clientId: SALESFORCE_CLIENT_ID,
      clientSecret: SALESFORCE_CLIENT_SECRET,
      token: `${SALESFORCE_URL}/services/oauth2/token`,
      userinfo: `${SALESFORCE_URL}/services/oauth2/userinfo`,
      authorization: {
        url: `${SALESFORCE_URL}/services/oauth2/authorize?display=page`,
        params: {
          scope: "api id web",
        },
      },
    }),
  ],
});

Properly log out of Salesforce

Even after logging out using signOut() in NextAuth, the user will still be logged in at Salesforce. Meaning that when the user attempts to sign in the user will automatically be logged in if the previous session hasn’t expired.

This can be resolved by adding ?prompt=login to the authorization url, forcing the login screen at Salesforce.

url: `${SALESFORCE_URL}/services/oauth2/authorize?prompt=login`,

Also you’ll probably want to revoke the oauth bearer token on sign out.

events: {
  async signOut({ session, token }) {
    fetch(`${SALESFORCE_URL}/services/oauth2/revoke`, {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: `token=${token.idToken}`,
    });
  },
},

Complete source code

// /api/auth/[...nextauth].ts
import NextAuth from "next-auth/next";
import SalesFoceProvider from "next-auth/providers/salesforce";

const {
  NEXTAUTH_SECRET,
  SALESFORCE_URL,
  SALESFORCE_CLIENT_ID,
  SALESFORCE_CLIENT_SECRET,
} = process.env;

export default NextAuth({
  debug: true,
  secret: NEXTAUTH_SECRET,
  events: {
    async signOut({ session, token }) {
      fetch(`${SALESFORCE_URL}/services/oauth2/revoke`, {
        method: "POST",
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
        body: `token=${token.idToken}`,
      });
    },
  },
  providers: [
    SalesFoceProvider({
      clientId: SALESFORCE_CLIENT_ID,
      clientSecret: SALESFORCE_CLIENT_SECRET,
      token: `${SALESFORCE_URL}/services/oauth2/token`,
      userinfo: `${SALESFORCE_URL}/services/oauth2/userinfo`,
      authorization: {
        url: `${SALESFORCE_URL}/services/oauth2/authorize?prompt=login`,
        params: {
          scope: "api id web",
        },
      },
      profile(profile) {
        return {
          name: profile.name,
          email: profile.email,
          id: profile.user_id,
          image: profile?.picture,
        };
      },
    }),
  ],
});

Add a comment