next-auth icon indicating copy to clipboard operation
next-auth copied to clipboard

Apple provider broken - blowing up when not finding userinfo endpoint

Open kyllerss opened this issue 2 years ago • 27 comments

Provider type

Apple

Environment

System: OS: Linux 5.19 KDE neon 5.27 5.27 CPU: (16) x64 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz Memory: 6.74 GB / 15.34 GB Container: Yes Shell: 5.1.16 - /bin/bash Binaries: Node: 16.18.0 - ~/apps/node-v16.18.0-linux-x64/bin/node npm: 9.3.0 - ~/apps/node-v16.18.0-linux-x64/bin/npm Browsers: Brave Browser: 110.1.48.164 Chrome: 110.0.5481.100 Firefox: 110.0

Reproduction URL

Needed?

Describe the issue

Seems as if Apple another one of those providers that does not provide user info.

When the redirect endpoint is invoked I get the following error:

[auth][cause]: TypeError: TODO: Authorization server did not provide a userinfo endpoint.
     at handleOAuth (file:///xxx/node_modules/@auth/sveltekit/node_modules/@auth/core/lib/oauth/callback.js:28:19)
     ...

The line in question is here.

I found a relevant issue that had a specific work-around, but pertained to Todoist.

Doesn't seem to me that requiring a user-info endpoint is part of the OAuth spec, but I could be wrong. I would rather have this restriction be a configuration option or handled explicitly by some callback. I wouldn't be surprised if the push for privacy is going to push additional partners to follow a similar pattern.

How to reproduce

Try to do a sign-in using Apple.

Expected behavior

For it to work and have the client specify how to handle the missing information.

kyllerss avatar Feb 22 '23 21:02 kyllerss

I'm running into this too. @kyllerss did you find a workaround?

jschlesser avatar Mar 21 '23 20:03 jschlesser

I've put this task aside for now, so I haven't had a chance to do a workaround.

kyllerss avatar Mar 21 '23 21:03 kyllerss

@kyllerss @jschlesser The debug message here is a bit unclear. If you provide a token endpoint, a userinfo endpoint is not necessary.

Additionally the authorization endpoint returns the relevant data only in the body of the response, instead of the query, like Auth.js would expect it. Therefor you have to use response_mode: 'query' for the authorization config. This comes with the downside, that you cannot request the email, or name of the user. I'm currently working on a fix to this.

For now, if you still want to use Sign in with Apple using Auth.js you can use this Apple provider configuration example to get it working:

Apple({
    clientId: APPLE_ID,
    clientSecret: APPLE_SECRET,
    wellKnown: "https://appleid.apple.com/.well-known/openid-configuration",
    checks: ["pkce"],
    token: {
      url: `https://appleid.apple.com/auth/token`,
    },
    authorization: {
      url: 'https://appleid.apple.com/auth/authorize',
      params: {
        scope: '',
        response_type: 'code',
        response_mode: 'query',
        state: crypto.randomUUID()
      },
    },
    client: {
      token_endpoint_auth_method: "client_secret_post",
    },
  })

In addition you'll need to set the secret for jwt encoding and the pkceCodeVerifier cookie on the top level of the auth configuration:

    cookies: {
      pkceCodeVerifier: {
        name: "next-auth.pkce.code_verifier",
        options: {
          httpOnly: true,
          sameSite: "none",
          path: "/",
          secure: true,
        },
      },
    },
    secret: AUTH_SECRET

I hope this helps :)

ChrGrb avatar Jul 31 '23 16:07 ChrGrb

Any news on this? @ChrGrb configuration doesn't work for me :(

Artur-Galstyan avatar Nov 08 '23 14:11 Artur-Galstyan

@ChrGrb it sort of works, but it doesn't return email/name and setting authorization.params.scope to something other then "" gets other errors.

There is no point using it if you get this: image

RomanistHere avatar Nov 23 '23 17:11 RomanistHere

so do I understand that it is impossible to get the email and name from apple sign in?

pingustar avatar Feb 12 '24 20:02 pingustar

Any updates? The same issue still occurs with version 5.0.0-beta.16. @ChrGrb method enables Apple login but cannot get the name.

How can I get other values defined in the name or scope?

The problem seems to be quite old. Are there any plans to resolve this issue? I like using next-auth and it has been very helpful, so I would like to continue using it. Thank you for creating such a great package.

Jay-flow avatar Apr 16 '24 10:04 Jay-flow

I'm also curious about the state of this issue.

I added 5.0.0-beta.16 with the GitHub provider for proof-of-concept and all went well. But with Apple sign-in I ran into the current issue. When I saw it was unresolved I downgraded to v4 and ran into #4061, which I also cannot fix atm.

Is there any working example of Apple sign-in using next-auth v4 or v5?

glencoden avatar Apr 16 '24 13:04 glencoden

I am also watching this closely, I am only able to use Sign in with Apple for my application, so this is key for me.

I have tried the configuration above from @ChrGrb but am getting a 'checks.state argument is missing', I suspect I am missing something dumb.

gordonturner avatar Apr 18 '24 13:04 gordonturner

I've been watching this for a year to see if anyone came up with anything. I gave up on trying to get email from Apple via AuthJS. It's not impossible but in my estimation it looks like it would take a lot of work that's beyond my desire. These notes are for anyone else who is feeling ambitious.

In a nutshell, the valid way that Apple is providing the email scope is only through using the response_mode of 'form_post'. Search for response_mode and response_type in the Apple docs. AuthJS on the other hand only supports 'query' mode. Implementing 'form_post' in AuthJS looks non trivial.

If you want to push for this, I would start a discussion with one of the core contributors like @balazsorban44 or @ThangHuuVu . I'm neither an expert in OAuth nor deeply familiar with all of the nuances that are going on in packages/core/src/lib/[actions|pages]

Otherwise the default Apple provider config should be replaced with the one that @ChrGrb has provided above and in the Apple provider docs clearly let everyone know that they won't be getting the email scope from Apple when using AuthJS.

Note for @balazsorban44 , the docs say that you test 20 or so of the providers and we can try out the sample app. When I click on Sign In on the Apple button (the first one at the top) and try to create an account, it fails no matter which options I choose.

The basis for my findings. Search for 'form_post' in the entire repo. It only shows up in the default Apple provider source file and if you have read through this thread, you will know that the provided Apple default config doesn't work. The workaround that @ChrGrb provided will get you logged in but not an account with the email scope. Note that the response mode is 'query' in that workaround config. Now go back to the Apple docs in the link above and read the section about getting the identity_token and you will see that it's only provided in the modes of 'fragment' or 'form_post'. If you search for 'fragment' in the codebase you will not see it show up in any provider code. The user section and identity_token of the response are the only ways to get email, and they must be requested in the scope and that cant be done with 'query' mode with Apple. In the back of my mind I can hear a security guy at Apple telling the authentication team to not put email in the url, because it seems like the right call. On the other hand it seems like Apple is the only player not using 'query'.

Does anyone see any other way?

jschlesser avatar Apr 19 '24 07:04 jschlesser

Hey @jschlesser I believe your details with regards to the Sign in with Apple are correct, I have a working implementation in Python FastAPI, and my experience there is consistent with your comments.

The only other thing I would add (or call out explicitly) is that userInfo is only returned from the first Sign in with Apple request.

In order to get the userInfo details again, you must go to https://appleid.apple.com/ and remove the app in 'Sign in with Apple' section.

Question, are the changes from https://github.com/nextauthjs/next-auth/pull/8189 (or equivalent) in the latest build or beta build?

~~If we apply that patch ourselves to the latest code, can we get things working/is that a good approach?~~ I see the 8189 pull/patch is quite out of date, so there is rebasing required.

gordonturner avatar Apr 20 '24 15:04 gordonturner

Until the apple provider is fixed I was able to get this working by adding an override route to handle the apple callback.

// app/api/auth/[...nextauth]/route.ts

import { handlers } from 'auth'

export const { GET, POST } = handlers
// app/api/auth/callback/apple/route.ts

import { NextRequest, NextResponse } from 'next/server'
import { GET as NextAuthGET } from '../../[...nextauth]/route'

// re-export the next-auth GET handler for this endpoint.
export { NextAuthGET as GET } 

export async function POST(req: NextRequest) {

  // contains user name and email if you need to process anything.
  const data = await req.formData() 

  const queryParams: { [key: string]: string } = {}
  data.forEach((value, key) => {
    queryParams[key] = value.toString()
  })

  const searchParams = new URLSearchParams(queryParams)

  const cookie = req.headers.get('cookie') || ''

  // redirect to the next-auth handler which expects GET with query params. 
  return NextResponse.redirect(
    `https://${req.headers.get('host')}/api/auth/callback/apple?` +
      searchParams.toString(),
    {
      status: 302,
      headers: {
        cookie,
      },
    },
  )
}

bsin1 avatar May 06 '24 21:05 bsin1

Until the apple provider is fixed I was able to get this working by adding an override route to handle the apple callback.

// app/api/auth/[...nextauth]/route.ts

import { handlers } from 'auth'

export const { GET, POST } = handlers
// app/api/auth/callback/apple/route.ts

import { NextRequest, NextResponse } from 'next/server'
import { GET as NextAuthGET } from '../../[...nextauth]/route'

// re-export the next-auth GET handler for this endpoint.
export { NextAuthGET as GET } 

export async function POST(req: NextRequest) {

  // contains user name and email if you need to process anything.
  const data = await req.formData() 

  const queryParams: { [key: string]: string } = {}
  data.forEach((value, key) => {
    queryParams[key] = value.toString()
  })

  const searchParams = new URLSearchParams(queryParams)

  const cookie = req.headers.get('cookie') || ''

  // redirect to the next-auth handler which expects GET with query params. 
  return NextResponse.redirect(
    `https://${req.headers.get('host')}/api/auth/callback/apple?` +
      searchParams.toString(),
    {
      status: 302,
      headers: {
        cookie,
      },
    },
  )
}

I tried this route handler, but the authentication process seems still to be missing the email address from the Apple account. Here's the error log from the related Vercel function:

[31m[auth][cause][0m: u: null value in column "email" of relation "user" violates not-null constraint at K (/var/task/.next/server/chunks/458.js:420:2291) at /var/task/.next/server/chunks/458.js:420:3320 at Socket.eQ (/var/task/.next/server/chunks/458.js:420:3325) at Socket.emit (node:events:517:28) at addChunk (node:internal/streams/readable:368:12) at readableAddChunk (node:internal/streams/readable:341:9) at Readable.push (node:internal/streams/readable:278:10) at TCP.onStreamRead (node:internal/stream_base_commons:190:23) at TCP.callbackTrampoline (node:internal/async_hooks:128:17)

glencoden avatar May 08 '24 07:05 glencoden

@glencoden as @gordonturner mentioned the user's email will only be delivered the first time they sign in. Subsequent sign ins will deliver the apple sub which is used to retrieve an account by id along with the associated user. After signing in with apple are you prompted to share or mask your email address?

I'm not sure what your adapter looks like but the PrismaAdapter is working fine out of the box for me with 5.0.0-beta.17

bsin1 avatar May 15 '24 07:05 bsin1

Hey everyone, thanks for discussing about this issue. I want to summarize the findings & workaround so far to track it better. I believe there are some separate issues that are happening at once, and we can deal with them one by one before getting the Apple provider working.

Issue 1: @auth/core is throwing an error: TypeError: TODO: Authorization server did not provide a userinfo endpoint.

https://github.com/nextauthjs/next-auth/blob/7e2c2d4509e7de83040dabc14b6d92bb105b9de4/packages/core/src/lib/actions/callback/oauth/callback.ts#L58-L61

See Apple's .well-known. @auth/core is requiring userinfo_endpoint although it is technically optional from the spec. I might be wrong here. I read through the OpenID Connect Core Spec, but couldn't find the linfo about the userinfo endpoint being required for the Authorization Server.

Plan to fix: I will confirm with @balazsorban44 before raising a PR

Issue 2: Apple requires response_mode=form_post in the Authorization request if requesting for any scopes

Thanks @jschlesser for pointing to this info on the Apple documentation page. Currently, @auth/core isn't supporting the response_mode=form_post. Plan to fix: @ChrGrb raised a PR to fix here: https://github.com/nextauthjs/next-auth/pull/8428

Issue 3: Apple isn't returning the claims in id_token

Auth.js normally expects the claims are in id_token if the provider is OIDC type. https://github.com/nextauthjs/next-auth/blob/7e2c2d4509e7de83040dabc14b6d92bb105b9de4/packages/core/src/lib/actions/callback/oauth/callback.ts#L157 Instead, Apple returns the user info as a JSON string in the query params, for reasons only they know. This is not an issue with Auth.js.

Workaround

To workaround the issues, you can use patch-package following this PR change: https://github.com/nextauthjs/next-auth/pull/8189/. You could check the PR comments for details of why we haven't merged it yet.

Hope that helps. I also confirm that our deployed example isn't working as expected at the moment. The Apple provider is indeed annoying to work with:

  • It requires a fee for a developer account.
  • The account then requires 2FA on phone only, which makes it even harder to share the account between contributors

ThangHuuVu avatar Jun 08 '24 18:06 ThangHuuVu

To everyone facing issues with the Apple provider not providing a userinfo endpoint, here's a workaround to get it functioning correctly with NextAuth.js:

Configuration for Apple Provider in NextAuth.js

  1. Configure the Apple provider in your NextAuth.js configuration file, typically found in pages/api/auth/[...nextauth].ts:
import NextAuth from "next-auth"
  import Providers from "next-auth/providers"

  export default NextAuth({
    providers: [
      Providers.Apple({
        clientId: process.env.APPLE_ID,
        clientSecret: process.env.APPLE_SECRET,
        wellKnown: "https://appleid.apple.com/.well-known/openid-configuration",
        checks: ["pkce"],
        authorization: {
          url: 'https://appleid.apple.com/auth/authorize',
          params: {
            scope: "name email",
            response_type: "code",
            response_mode: "form_post",
            state: crypto.randomUUID(),
          },
        },
        token: {
          url: `https://appleid.apple.com/auth/token`,
        },
        client: {
          token_endpoint_auth_method: "client_secret_post",
        },
        profile(profile) {
          return {
            id: profile.sub,
            name: profile.name || null,
            email: profile.email || null,
            image: null,
          }
        },
        profileConform(profile, query) {
          if (query.user) {
            const user = JSON.parse(query.user);
            if (user.name) {
              profile.name = Object.values(user.name).join(" ");
            }
          }
          return profile;
        },
      }),
    ],
    session: {
      jwt: true,
    },
    jwt: {
      secret: process.env.AUTH_SECRET,
    },
    },
  })`

Override the Apple Callback Route

To handle the Apple callback correctly, create an override route for the Apple callback:

  1. Create a file at pages/api/auth/callback/apple.ts:


    import { NextRequest, NextResponse } from 'next/server'
    import { GET as NextAuthGET } from '../../[...nextauth]'

    export { NextAuthGET as GET }

    export async function POST(req: NextRequest) {
      const data = await req.formData();
      const queryParams: { [key: string]: string } = {};
      data.forEach((value, key) => {
        queryParams[key] = value.toString();
      });

      const searchParams = new URLSearchParams(queryParams);
      const cookie = req.headers.get('cookie') || '';

      return NextResponse.redirect(
        `https://${req.headers.get('host')}/api/auth/callback/apple?${searchParams.toString()}`,
        {
          status: 302,
          headers: {
            cookie,
          },
       
      );
    }


Explanation:

  • Apple Provider Configuration:

    • The authorization parameter includes response_mode: "form_post" to comply with Apple's requirements.
    • The profileConform function processes the user information returned in the authorization response.
  • Session and JWT:

    • The session is configured to use JWT, and the jwt configuration includes the secret required for encoding.
  • PKCE Code Verifier Cookie:

    • A custom cookie configuration for pkceCodeVerifier to handle the PKCE flow securely.
  • Handling the Apple Callback:

    • The POST handler processes the form data returned from Apple, converts it to query parameters, and redirects it to the NextAuth.js handler with the necessary cookies.

By following these steps, you should be able to implement a workaround for the Apple provider issue in NextAuth.js until a permanent fix is merged into the library.

mamimotu avatar Jun 15 '24 11:06 mamimotu

Any idea when this will be updated to work by default?

kiikoh avatar Jul 22 '24 02:07 kiikoh

Thanks to all for workarounds here. Just for reference, on latest beta 5.0.0-beta.20 thanks to @ChrGrb and #8428 there not need to override callback. In my case I decided not to try to get the name, and just allow user to change it. Here is my working configuration

export const authConfig = {
  cookies: {
    pkceCodeVerifier: {
      name: "next-auth.pkce.code_verifier",
      options: {
        httpOnly: true,
        sameSite: "none",
        path: "/",
        secure: true,
      },
     },
  },
  providers: [
    GitHub,
    Google,
    Apple({
      clientId: process.env.AUTH_APPLE_ID,
      clientSecret: ""+process.env.AUTH_APPLE_SECRET,
      checks: ["pkce"],
      token: {
        url: `https://appleid.apple.com/auth/token`,
      },
      client: {
        token_endpoint_auth_method: "client_secret_post",
      },
      authorization: {
        params: {
          response_mode: "form_post",
          response_type: "code",//do not set to "code id_token" as it will not work
          scope: "name email"
        },},
        profile(profile) {
          return {
            id: profile.sub,
            name: "Person Doe",//profile.name.givenName + " " + profile.name.familyName, but apple does not return name...
            email: profile.email,
            image: "",
          }
        }
    }),
}

sl45sms avatar Aug 04 '24 20:08 sl45sms

@sl45sms your working configuration works great, thank you!

lucassalicanorc avatar Aug 09 '24 21:08 lucassalicanorc

@sl45sms thx, saved my day!

boehmbe avatar Aug 10 '24 12:08 boehmbe

I am getting this error, I believe because I am using the AUTH_REDIRECT_PROXY documented here. https://authjs.dev/getting-started/deployment#securing-a-preview-deployment

InvalidCheck: Missing state in query, but required for redirect proxy. Read more at https://errors.authjs.dev#invalidcheck

Also, getting this error in production where the redirect proxy should be used

Error: TODO: Handle OIDC response body error.

I am using https://github.com/t3-oss/create-t3-turbo as a starter. I need to implement apple auth before I can publish to the app store so this is a major blocker, I believe for all of the project using this template.

Is there another workaround?

kiikoh avatar Aug 10 '24 15:08 kiikoh

I am getting this error, I believe because I am using the AUTH_REDIRECT_PROXY documented here. https://authjs.dev/getting-started/deployment#securing-a-preview-deployment

InvalidCheck: Missing state in query, but required for redirect proxy. Read more at https://errors.authjs.dev#invalidcheck

Also, getting this error in production where the redirect proxy should be used

Error: TODO: Handle OIDC response body error.

I am using https://github.com/t3-oss/create-t3-turbo as a starter. I need to implement apple auth before I can publish to the app store so this is a major blocker, I believe for all of the project using this template.

Is there another workaround? @kiikoh Try to set state cookie with samesite none (as pkceCodeVerifier do)

sl45sms avatar Aug 10 '24 17:08 sl45sms

@sl45sms I have update my config to include this, in both the proxy and the nextjs route

cookies: {
    pkceCodeVerifier: {
      name: "next-auth.pkce.code_verifier",
      options: {
        httpOnly: true,
        sameSite: "none",
        path: "/",
        secure: true,
      },
    },
    state: {
		// do i need to add a name? what should it be
      options: {
        httpOnly: true,
        sameSite: "none",
        path: "/",
        secure: true,
      },
    },
  },

Still seeing the same errors

In production, without the proxy

Error: TODO: Handle OIDC response body error
  at ig (/var/task/apps/nextjs/.next/server/chunks/621.js:393:29432)
  at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
  at async iP (/var/task/apps/nextjs/.next/server/chunks/621.js:393:35570)
  at async iU (/var/task/apps/nextjs/.next/server/chunks/621.js:393:47370)
  at async iK (/var/task/apps/nextjs/.next/server/chunks/621.js:393:52671)
  at async /var/task/apps/nextjs/.next/server/chunks/189.js:13:50805
  at async /var/task/node_modules/next/dist/compiled/next-server/app-route.runtime.prod.js:6:34666
  at async eS.execute (/var/task/node_modules/next/dist/compiled/next-server/app-route.runtime.prod.js:6:25813)
  at async eS.handle (/var/task/node_modules/next/dist/compiled/next-server/app-route.runtime.prod.js:6:35920)
  at async es (/var/task/node_modules/next/dist/compiled/next-server/server.runtime.prod.js:16:25461)

And Missing state in query, but required for redirect proxy on local development, which uses the proxy

Thank you for taking a look into this

kiikoh avatar Aug 10 '24 17:08 kiikoh

@kiikoh are you sure you have set the correct clientId? I had some strange problems because of this, which didn't seem to be related at all.

sl45sms avatar Aug 11 '24 14:08 sl45sms

@kiikoh are you sure you have set the correct clientId? I had some strange problems because of this, which didn't seem to be related at all.

I'm pretty sure it's the correct client id because on apples login page it shows my app name.

image @sl45sms

kiikoh avatar Aug 11 '24 14:08 kiikoh

@kiikoh apple call this as "serviceID" and you can found it under https://developer.apple.com/account/resources/identifiers/list/serviceId looks like Στιγμιότυπο οθόνης 2024-08-11, 18 25 12 in my case is "ai.prompt2" may you use the app id

sl45sms avatar Aug 11 '24 15:08 sl45sms

@kiikoh apple call this as "serviceID" and you can found it under developer.apple.com/account/resources/identifiers/list/serviceId looks like Στιγμιότυπο οθόνης 2024-08-11, 18 25 12 in my case is "ai.prompt2" may you use the app id

@sl45sms right, I was just saying that since that text appeared, and I didn't manually enter that in it must have picked it up correctly. Everything works up until the redirect back to my app.

image

In my env I have, AUTH_APPLE_ID=com.musicbridge.authsrv

kiikoh avatar Aug 11 '24 15:08 kiikoh