supertokens / supertokens-node

Node SDK for SuperTokens core
https://supertokens.com
Other
302 stars 81 forks source link

Apple Provider error for SignInUp #580

Closed LukasKnuth closed 1 year ago

LukasKnuth commented 1 year ago

I haven't found any documentation on how to setup specific providers, so I have somewhat guessed my way to where I am now.

Using code

I use https://appleid.apple.com/auth/authorize?response_type=code id_token&redirect_uri=<redirect-url>&client_id=<client-id>&response_mode=form_post from the Apple Documentation

I assume the callback URL must be /auth/callback/apple from the FDI documentation so I have registered that with Apple.

As far as I can tell the code I get back is useless and only the id_token is required. When the Apple page redirects me to the apple callback, it immediately redirects me to my API domain with state and code in the query parameters.

The code is the code from the apple response and doesn't work with /auth/signinup, I get the following Stack Trace in the response:

Request:

{
    "redirectURI": <redirect-url>,
    "thirdPartyId": "apple",
    "authCodeResponse": {
        "access_token": <code>       
    }
}

Response

TypeError: Cannot read properties of null (reading 'header')
    at /app/node_modules/verify-apple-id-token/dist/lib/verifyAppleIdToken.js:74:30
    at step (/app/node_modules/verify-apple-id-token/dist/lib/verifyAppleIdToken.js:33:23)
    at Object.next (/app/node_modules/verify-apple-id-token/dist/lib/verifyAppleIdToken.js:14:53)
    at /app/node_modules/verify-apple-id-token/dist/lib/verifyAppleIdToken.js:8:71
    at new Promise (<anonymous>)
    at __awaiter (/app/node_modules/verify-apple-id-token/dist/lib/verifyAppleIdToken.js:4:12)
    at Object.verifyToken [as default] (/app/node_modules/verify-apple-id-token/dist/lib/verifyAppleIdToken.js:68:46)
    at Object.<anonymous> (/app/node_modules/supertokens-node/lib/build/recipe/thirdparty/providers/apple.js:115:70)
    at Generator.next (<anonymous>)
    at /app/node_modules/supertokens-node/lib/build/recipe/thirdparty/providers/apple.js:30:75

Using id_token

When I open the "Network inspector" in FireFox on the Apple Page I can see that the POST request it makes to my callback-URL contains the id_token as well. That token is lost when the callback redirects me again (as described above).

If I use this token as the access_token I get the same error as above. However, reading the source of the Apple provider:

https://github.com/supertokens/supertokens-node/blob/b3e1c19c431bd70de6a2e365a1e2117241a67fbb/lib/ts/recipe/thirdparty/providers/apple.ts#L106-L139

The access_token is never used in that function, instead only id_token is used. Providing this in the request gives another error:

Request

{
    "redirectURI": <redirect-url>,
    "thirdPartyId": "apple",
    "authCodeResponse": {
        "id_token": <id-token>       
    }
}

Response

{"message":"Please provide the access_token inside the authCodeResponse request param"}

I assume this is a static validation rule, because if I specify anything for access_token and also specify the id_token, I get another error:

Request

{
    "redirectURI": <redirect-url>,
    "thirdPartyId": "apple",
    "authCodeResponse": {
       "access_token": "literally-anything",
        "id_token": <id-token>
    }
}

Response

Error: SuperTokens core threw an error for a POST request to path: '/recipe/signinup' with status code: 400 and message: Field name 'id' is invalid in JSON input

    at Querier.<anonymous> (/app/node_modules/supertokens-node/lib/build/querier.js:260:31)
    at Generator.throw (<anonymous>)
    at rejected (/app/node_modules/supertokens-node/lib/build/querier.js:22:44)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)

This is where I currently am stuck. I feel like I'm probably misunderstanding something fundamentally here, all the other Providers I have used before where very straight forward.

Can you help me get this setup?

rishabhpoddar commented 1 year ago

Hey. So for apple login, apple should redirect the user to the {apiDomain}/auth/callback/apple with the code and state.

This API, further redirects the user to the websiteDomain/auth/callback/apple with the same code and state. On this page, our prebuilt UI checks for the state and calls the signinup API using the code sent by apple (just like how something like google sign in works). This API then exchanges the code + secret to get the id token on the backend which then gives the user info and creates a user in supertokens.

LukasKnuth commented 1 year ago

Currently, we're not using the frontend at all, since we're building the logins for Apps only.

What would the App be expected to call? I assume it's auth/signinup with the code from the apple redirect? And if so, why does neither code nor id_token work for that API?

I feel like I'm missing a step here.

This API then exchanges the code + secret to get the id token on the backend

What API on the backend does that?

rishabhpoddar commented 1 year ago

Right. I thought you were making for webapp.

So for mobile apps, you can see this guide here: https://supertokens.com/docs/thirdparty/custom-ui/thirdparty-login (click on the mobile code tab), and you need to pass the code to the signinup API just like the curl command shown in the docs.

You can also see how we do it for react native app here: https://github.com/supertokens/supertokens-react-native/blob/master/examples/with-thirdpartyemailpassword/apple.js#L48.

This doesn't seem to be a bug in our sdk, so i am closing this issue, but feel free to continue the conversation in the closed issue or as on our discord.

LukasKnuth commented 1 year ago

So, I have started debugging the application locally because when I send the code received from Apple to /signinup, I get the following stack trace:

JWSInvalid: Compact JWS must be a string or Uint8Array
    at compactVerify (/Users/lukasknuth/dev/identity/node_modules/jose/dist/node/cjs/jws/compact/verify.js:12:15)
    at Object.jwtVerify (/Users/lukasknuth/dev/identity/node_modules/jose/dist/node/cjs/jwt/verify.js:9:58)
    at Object.<anonymous> (/Users/lukasknuth/dev/identity/node_modules/supertokens-node/lib/build/recipe/thirdparty/providers/utils.js:74:40)
    at Generator.next (<anonymous>)
    at /Users/lukasknuth/dev/identity/node_modules/supertokens-node/lib/build/recipe/thirdparty/providers/utils.js:66:75
    at new Promise (<anonymous>)
    at __awaiter (/Users/lukasknuth/dev/identity/node_modules/supertokens-node/lib/build/recipe/thirdparty/providers/utils.js:48:16)
    at Object.verifyIdTokenFromJWKSEndpoint (/Users/lukasknuth/dev/identity/node_modules/supertokens-node/lib/build/recipe/thirdparty/providers/utils.js:73:12)
    at Object.<anonymous> (/Users/lukasknuth/dev/identity/node_modules/supertokens-node/lib/build/recipe/thirdparty/providers/apple.js:159:51)
    at Generator.next (<anonymous>)

While debugging, I noticed that this is simply a follow-up error. It happens in https://github.com/supertokens/supertokens-node/blob/0faebfae435fd661f4b6657e2ca510101da012f5/lib/ts/recipe/thirdparty/api/implementation.ts#L128-L136 when the response is not successful.

In my case, it returns the following body:

{
  error: "invalid_grant",
  error_description: "redirect_uri mismatch. The code was not issued to https://<my-domain>/auth/callback/apple.",
}

After the code from apple is expired (after 5min) it returns:

{
  error: "invalid_grant",
  error_description: "The code has expired or has been revoked.",
}

Both of these legitimate error responses cause the same stack trace and HTTP 500 response, which obscures the actual underlying problem.

This is because the code assumes the response is always successful and accesses accessTokenAPIResponse.id_token in https://github.com/supertokens/supertokens-node/blob/0faebfae435fd661f4b6657e2ca510101da012f5/lib/ts/recipe/thirdparty/providers/apple.ts#L118-L121 which in case of an error is simply undefined, causing the crash and stack trace.


So the issue seems to be that I don't have a correct redirect URI registered with apple.

The SDK assumes the redirect URI that is configured with Apple is always the /callback/apple endpoint provided by SuperTokens: https://github.com/supertokens/supertokens-node/blob/0faebfae435fd661f4b6657e2ca510101da012f5/lib/ts/recipe/thirdparty/providers/apple.ts#L139-L146

As far as I understand, this isn't required when implementing the flow for iOS Apps which can use the official Apple SDK. The problem is that if I configure a different callback URL in Apples Developer Console and pass it to /signinup, it's ignored and SuperTokens will still always use it's own callback URL when calling https://appleid.apple.com/auth/token to swap the code for an auth token with the Apple Server: https://github.com/supertokens/supertokens-node/blob/0faebfae435fd661f4b6657e2ca510101da012f5/lib/ts/recipe/thirdparty/providers/apple.ts#L71-L77

This API call verifies that the redirect_uri parameter it receives is the same that is set in the code. If it doesn't match, the above error is returned. AFAIK, it's not documented that one must use the SuperTokens callback.


So with the redirect URI fixed and pointing to where SuperTokens expects, I give the resulting code to /signinup and get the following response from the Backend SDK:

Error: SuperTokens core threw an error for a POST request to path: '/recipe/signinup' with status code: 400 and message: Field name 'id' is invalid in JSON input

    at Querier.<anonymous> (/app/node_modules/supertokens-node/lib/build/querier.js:310:31)
    at Generator.next (<anonymous>)
    at fulfilled (/app/node_modules/supertokens-node/lib/build/querier.js:51:36)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)

This is the stack-trace logged by SuperTokens Core:

ERROR | pid: 62c11da4-8a2d-44f9-82b6-2dff3ef8ac38 | [http-nio-0.0.0.0-3567-exec-5] thread | io.supertokens.webserver.WebserverAPI.service(WebserverAPI.java:174) | jakarta.servlet.ServletException: io.supertokens.webserver.WebserverAPI$BadRequestException: Field name 'id' is invalid in JSON input
    at io.supertokens.webserver.InputParser.parseStringOrThrowError(InputParser.java:142)
    at io.supertokens.webserver.api.thirdparty.SignInUpAPI.doPost(SignInUpAPI.java:93)
    at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:731)
    at io.supertokens.webserver.WebserverAPI.service(WebserverAPI.java:172)
    at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:814)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:223)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:119)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357)
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:400)
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:861)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1739)
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
    at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
    at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.base/java.lang.Thread.run(Thread.java:832)
Caused by: io.supertokens.webserver.WebserverAPI$BadRequestException: Field name 'id' is invalid in JSON input
    ... 23 more

Let me re-state what I'm trying to do: Setup "Sign in with Apple" for an Android App where (as I understand) the same flow as for Web is used.

LukasKnuth commented 1 year ago

Looking deeper into this, I can now see that the object that the backend SDK sends to SuperTokens Core looks as follows:

{
  "thirdPartyId": "...",
  "thirdPartyUserId": "...",
  "email": { "id":  null }
}

It makes sense that this would cause an error in the core, although the Error message it returns makes it seem like and id property is not expected or allowed in the request, but it's just not allowed to be null.


This happens because https://github.com/supertokens/supertokens-node/blob/0faebfae435fd661f4b6657e2ca510101da012f5/lib/ts/recipe/thirdparty/providers/apple.ts#L125-L137 attempts to extract email and email_verified properties from the id_token returned by Apple.

The id_token in my case does not have that information in its payload:

{
  "iss": "https://appleid.apple.com",
  "aud": "<redacted>",
  "exp": 1689664380,
  "iat": 1689577980,
  "sub": "<redacted>",
  "at_hash": "sqt3b7LXLshzCmf7wYiGIg",
  "auth_time": 1689577942,
  "nonce_supported": true
}

The URL I'm using to request the code (which is then exchanged for the token) includes scope=name%20email, so I am requesting the email scope specifically.

LukasKnuth commented 1 year ago

I have it working!

The issue was that the user I was using to test the login was in a bad state for what I was trying to do. Initially, I didn't set the scope query parameter in my request to apple. This caused the first Sign Up with Apple prompt to not include the email scope. Subsequent request, even with scope=email didn't add the email and also didn't re-prompt me to share my email either.

I fixed it by going to https://appleid.apple.com/account/manage, clicking on "Sign In with Apple" -> My application -> "Stop using Sign In with Apple".

After this, I did the same request to Apple again but this time I was asked to provide either my email or the privacy setting. And now, my id_token has the expected email and email_verified fields and the request goes through and the SuperTokens user is created!