How to implement Apple Login in a Phoenix app?

Hey everyone!
I’m currently implementing sign in with apple on my app. I’m already at the point where I got the JWT that I need to decode to validate the user and the key hash from apple (includes the kid, the alg etc…
How can I use the information from the key hash to decode the JWT correctly?
In Ruby it seems pretty straight forward with

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

But I’m having a hard time finding the equivalent in Elixir. I took a look at JOSE.JWK but I’m not sure which method I should use to generate the JWK and fetch the public key afterward
Thanks!

2 Likes

With Assent it is very easy too (in my mix file: {:assent, "~> 0.1.13"}). You just have to pass the apple authorization code (ASAuthorizationAppleIDCredential -> authorizationCode) and the apple_id is then verified. No need to manually pass the public key and algorithm. You don’t even need to pass the identityToken:

def check_apple_sign_in(_apple_jwt, apple_auth) do
    [
     client_id: "com.your_domain.your_app",
     team_id: "YOUR_ID",
     private_key_id: "KEY_ID",
     private_key_path: "/Users/you/development/Keys/AuthKey_2YZ4LV4PYI.p8",
     redirect_uri: nil
    ]
    |> Assent.Config.put(:session_params, %{})
    |> Assent.Strategy.Apple.callback(%{"code" => apple_auth})
    |> case do
      {:ok, %{user: user, token: token}} ->
        %{"sub" => user_id} = user
        Logger.info "apple sign in verification successful. user: #{Kernel.inspect(user)}; token: #{Kernel.inspect(token)}"
        {:ok, user_id} #the user_id you are interested in
      error ->
        Logger.info "apple sign in verification not successful: #{Kernel.inspect(error)}"
        {:error, :authentication}
    end
  end
4 Likes

I recently implemented Sign in with Apple in our app, and I used Joken to handle both token verification and client secret generation.

Here’s what I did to decode the JWT (Note: My example uses Joken v1.5)

token = Joken.token(id_token)
%{"alg" => alg, "kid" => key_id} = Joken.peek_header(token)

token
|> Joken.with_signer(Signer.rs(alg, public_key))
|> Joken.with_validation("aud", &(&1 == "<client_id>"))
|> Joken.with_validation("exp", &(&1 >= System.os_time(:second)))
|> Joken.with_validation("iss", &(&1 == "<issuer>"))
|> Joken.verify!()

public_key above is the matching key fetched from Apple’s public key endpoint.

Hope this helps - and feel free to reach out with additional questions

8 Likes

Were you able to solve your problem?

Hey, sorry for the late reply!
Yes, I was able to solve it but I followed a different approach, didn’t use Joken or Assent, just used JOSE directly.

    key_hash = Enum.find(parsed_body["keys"], fn key -> key["kid"] == kid end)

    jwk = JOSE.JWK.from_map(key_hash)

    case JOSE.JWT.verify_strict(jwk, [alg], token) do
        ...
    end
3 Likes

Just in case: please note that jose does not validate any claims of a jwt. It only verifies the signature - so you have to check all other things by yourself - especially exp and aud. Just don’t forget that!

Joken provides sane default behavior - which is useful. I did jwt handling both jose only and jose/joken and both are pretty straightforward.

Probably harder part is to parse a key in PEM format haha.

3 Likes

Hey @chulkilee, thanks for your feedback! Yes, I did indeed do the part of checking the uid, exp, aud, iss, etc!

I was a new person. What should I do after the validation is done?