Sign in with Apple: verify token at server side

Viewed 3157

Hi I am trying to verify the apple auth credentials provided by the client-side application, on the server-side, I am getting these fields from the client: authorizationCode, identityToken along with a lot of other fields.

I've tried reading a lot of blogs but none of them exactly mentions these fields. what is the easiest way to verify and get user details by using these fields to some apple API

I've done that for Google, where the client passes the access token to backend and using https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=YourToken we can verify the token and get user details.

Please suggest some similar way for apple. Thanks. I am using ROR, in case that helps.

2 Answers

Finally, I was able to figure out how to verify the access token received from the client-side. Apple does not provide any API for validating access-token, I highly recommend this blog It explains the whole Process quite neatly. This is the ruby code link for the same.

For those who need React-Native client code for this, see below:

import * as React from 'react';
import * as AppleAuthentication from 'expo-apple-authentication';
import { signInWithApple } from '../api';

const AppleAuthenticationButton = () => (
  <AppleAuthentication.AppleAuthenticationButton
    buttonType={AppleAuthentication.AppleAuthenticationButtonType.SIGN_IN}
    buttonStyle={AppleAuthentication.AppleAuthenticationButtonStyle.WHITE}
    cornerRadius={5}
    style={{ width: 200, height: 44 }}
    onPress={async () => {
      try {
        const credential = await AppleAuthentication.signInAsync({
          requestedScopes: [
            AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
            AppleAuthentication.AppleAuthenticationScope.EMAIL,
          ],
        });

        console.log('signed in', credential);
        await signInWithApple(credential);
        // signed in
      } catch (e) {
        if (e.code === 'ERR_CANCELED') {
          console.log('cancelled');
          // handle that the user canceled the sign-in flow
        } else {
          console.log('apple authentication error', e);
          // handle other errors
        }
      }
    }}
  />
);

export default AppleAuthenticationButton;
export const signInWithApple = async (credentials) => {
  const {
    identityToken, user, email, authorizationCode, fullName,
  } = credentials;

  apiCall('users/sign_in', 'post', {
    method: 'apple',
    identityToken,
    user,
    email,
    authorizationCode,
    fullName,
  });
};

Also, as I noted in my comment below I found that there are 2 keys provided with the apple credentials and only one of them works. I have no idea why, but the following code works better than the code linked in the earlier response.

  def validate_apple_id

    name = params[:name]
    userIdentity = params[:user]
    jwt = params[:identityToken]

    begin
      header_segment = JSON.parse(Base64.decode64(jwt.split(".").first))
      alg = header_segment["alg"]

      apple_response = Net::HTTP.get(URI.parse(APPLE_PEM_URL))
      apple_certificate = JSON.parse(apple_response)
      token_data = nil

      apple_certificate["keys"].each do | key |
        keyHash = ActiveSupport::HashWithIndifferentAccess.new(key)
        jwk = JWT::JWK.import(keyHash)
        token_data ||= JWT.decode(jwt, jwk.public_key, true, {algorithm: alg})[0] rescue nil
      end

      if token_data&.has_key?("sub") && token_data.has_key?("email") && userIdentity == token_data["sub"]
        yield
      else
        # TODO: Render error to app
      end
    rescue StandardError => e
      # TODO: Render error to app
    end

  end
Related