Skip to content

Instantly share code, notes, and snippets.

@davideluque
Last active February 13, 2024 15:18
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 7 You must be signed in to fork a gist
  • Save davideluque/5a277c8ea8c31b48e35cb9d0c4ddef3e to your computer and use it in GitHub Desktop.
Save davideluque/5a277c8ea8c31b48e35cb9d0c4ddef3e to your computer and use it in GitHub Desktop.
Sign in with Apple in Ruby on Rails using apple_id gem.

Sign in with Apple Implementation for Ruby on Rails

Note: Before diving into this implementation, consider that there are more popular and potentially easier-to-use solutions for implementing Sign in with Apple in Ruby on Rails, such as Omniauth. If you prefer a more out-of-the-box approach, explore those alternatives.

Overview

This implementation of the Sign in with Apple service in Ruby on Rails is suitable for APIs, eliminating the need for views.

Features

This implementation handles the following tasks:

  1. Identity Token Verification:

    • Verifies the user's identity token with Apple servers.
    • Ensures the token is not expired and has not been tampered with or replayed to the app.
  2. User Authentication:

    • Logs in the user.
    • Registers the user.
    • Connects the user's Apple account to an existing account.

Parameters

Make use of the following parameters in the implementation:

  • code: Apple's authorization code after sign-in. Example: c49a75458b1e74b9f8e866f5a93b1689a.0.nrtuy. ...
  • id_token: Apple's identity token after sign-in. Example: eyJraWQiOiJBSURPUEsxIiwiYWxnIjoiUlMyNT ...

Handling Errors

The code snippet below demonstrates how errors from Apple's servers are handled:

begin
  token_response = @client.access_token!
rescue AppleID::Client::Error => e
  # The variable "e" contains the error message from Apple.
  return unauthorized
end

This snippet rescues from an ErrorResponse received from Apple, typically due to an invalid value in the code parameter.

Possible Causes of Error

This error may occur under the following circumstances:

  • The code parameter is invalid.
  • Changes in Sign in with Apple’s configurations (identifier, private key, team, key id, redirect URI, etc.).
  • Mismatch between the backend's configuration making the request to Apple servers (this implementation) and the configuration used in the frontend to display the Sign-in page.

License

This gist is licensed under the MIT License.

# Enviroment is managed with gem 'figaro'
# THESE ARE NOT THE REAL ONES, USE YOUR VALUES.
APPLE_CLIENT_ID: "com.myapp.client"
APPLE_TEAM_ID: "DX4RM9AL52"
APPLE_KEY: "51KDRS24J5"
APPLE_PRIVATE_KEY: "-----BEGIN PRIVATE KEY-----\nZj0DAQehRANCAARxcsMPCg29tjBgsO8K8cp3mJIoSu\n+HPFYiW1jNaa+MvTHxMIGTAgEAmBMGByqGSM49AgEgCCqGSM49AwEHBHkwdwIBAQQKj7Hb+b++gCgYIKoZIN\nxPJ3EEpVqz4/rH/ExZSKwaIZ/nCtkvtPUS7Y7IHaBVB94OHNzppD3UE\npYRfzHK+\n-----END PRIVATE KEY-----\n"
APPLE_REDIRECT_URI: "https://api.myapp.com/auth/apple"
gem 'apple_id'

MIT License

Copyright (c) 2023 davideluque

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Rails.application.routes.draw do
post 'auth/apple' => 'sessions#apple_callback'
end
# frozen_string_literal: true
class SessionsController < ApplicationController
before_action :apple_client, only: [:apple_callback]
def apple_callback
return unprocessable_entity unless params[:code].present? && params[:identity_token].present?
@apple_client.authorization_code = params[:code]
begin
token_response = @apple_client.access_token!
rescue AppleID::Client::Error => e
# puts e # gives useful messages from apple on failure
return unauthorized
end
id_token_back_channel = token_response.id_token
id_token_back_channel.verify!(
client: @apple_client,
access_token: token_response.access_token
)
id_token_front_channel = AppleID::IdToken.decode(params[:identity_token])
id_token_front_channel.verify!(
client: @apple_client,
code: params[:code]
)
id_token = token_response.id_token
# id_token.sub, a.k.a apple_uid is unique per user, no matter how many times
#  you perform the same request.
@user = User.find_by(apple_uid: id_token.sub)
return sign_in_and_return if @user.present?
@user = User.find_by_email(id_token.email)
if @user.present?
# To prevent an account takeover vulnerability, show the user a message and
# make them sign in with their password to link their account to apple.
# The user has to prove the ownership of the account by signin in.
unprocessable_entity
else
@user = User.register_user_from_apple(id_token.sub, id_token.email)
created
end
end
private
def created
render status: 201, json: {
status: 'success',
data: @user
}
end
def apple_client
@apple_client ||= AppleID::Client.new(
identifier: ENV['APPLE_CLIENT_ID'],
team_id: ENV['APPLE_TEAM_ID'],
key_id: ENV['APPLE_KEY'],
private_key: OpenSSL::PKey::EC.new(ENV['APPLE_PRIVATE_KEY']),
redirect_uri: ENV['APPLE_REDIRECT_URI']
)
end
def sign_in_and_return
sign_in(@user, store: true, bypass: false) # Devise method
render status: 200, json: {
status: 'success',
data: @user
}
end
def unauthorized
render status: 401, json: {
status: 'error'
}
end
def unprocessable_entity
render status: 422, json: {
status: 'error'
}
end
end
# User model. It is a devise_token_auth user model.
class User < ApplicationRecord
def self.register_user_from_apple(email, uid)
User.create do |user|
user.apple_uid = uid
user.email = email
user.provider = :apple # devise_token_auth attribute, but you can add it yourself.
user.uid = email # devise_token_auth attribute
end
end
end
@devifr
Copy link

devifr commented Jan 2, 2021

i get error OpenSSL::PKey::ECError (invalid curve name)

@naku-i386
Copy link

Is it necessary to validate if code parameter is valid? I have the JWT id_token and i verify it using apple key from https://appleid.apple.com/auth/keys

@davideluque
Copy link
Author

davideluque commented May 19, 2021

Is it necessary to validate if code parameter is valid? I have the JWT id_token and i verify it using apple key from https://appleid.apple.com/auth/keys

I think it is. I believe it's the method .access_token! the one that checks the parameter.

I had a case where the code param was invalid. That is why I added the begin rescue end block.

@e-serranor
Copy link

e-serranor commented Dec 20, 2022

Hi @davideluque, thanks for sharing this.

I tried this solution and I'm getting an error since the apple_uid column is not present for User. Is this column generated by a migration or a command from a gem? Or do I have to manually add it (fairly simple)? Just in case I missed a previous step.

ERROR: column users.apple_uid does not exist

@davideluque
Copy link
Author

Hi @davideluque, thanks for sharing this.

I tried this solution and I'm getting an error since the apple_uid column is not present for User. Is this column generated by a migration or a command from a gem? Or do I have to manually add it (fairly simple)? Just in case I missed a previous step.

ERROR: column users.apple_uid does not exist

You have to manually add it yourself

@deHelden
Copy link

i get error OpenSSL::PKey::ECError (invalid curve name)

I've had the same problem. the issue for me was that I didn't provide APPLE_PRIVATE_KEY

@davideluque it would be awesome to edit application.yml APPLE_PEM to APPLE_PRIVATE_KEY as it's used at setup_apple_client

@harrisreynolds
Copy link

I'm stuck getting a weird invalid_client error:

12:07:12 web.1  | #<AppleID::Client:0x00000001100a4518 @identifier="app.prayerteam.web", @team_id="FL726TVN3W.app.prayerteam", @key_id="YG33TJYKQ7", @private_key=#<OpenSSL::PKey::EC:0x00000001100a46f8 oid=id-ecPublicKey>, @secret=nil, @certificate=nil, @redirect_uri="https://exactly-holy-sunbird.ngrok-free.app/users/auth/apple/callback", @scheme=nil, @host=nil, @port=nil, @authorization_endpoint="https://appleid.apple.com/auth/authorize", @token_endpoint="https://appleid.apple.com/auth/token", @revocation_endpoint="https://appleid.apple.com/auth/revoke", @userinfo_endpoint="/userinfo", @expires_in=nil, @grant=#<Rack::OAuth2::Client::Grant::AuthorizationCode:0x000000011009eac8 @code="c46359e7ee0fc4df6a370c6a794cc7a9a.0.mryut.Qt7BFiM6rbza0iK5SEG4TA", @redirect_uri="https://exactly-holy-sunbird.ngrok-free.app/users/auth/apple/callback">>
12:07:13 web.1  | Completed 500 Internal Server Error in 465ms (ActiveRecord: 6.0ms | Allocations: 14572)
12:07:13 web.1  |
12:07:13 web.1  |
12:07:13 web.1  |
12:07:13 web.1  | AppleID::Client::Error (invalid_client):
12:07:13 web.1  |
12:07:13 web.1  | app/controllers/apple_signin_controller.rb:41:in `apple_callback'

I've included in the snipped above my @client variable.

Any ideas here? This is my second run at implementing this and have been stuck for hours trying to get basic Apple Signin to work.

@davideluque
Copy link
Author

I'm stuck getting a weird invalid_client error:

12:07:12 web.1  | #<AppleID::Client:0x00000001100a4518 @identifier="app.prayerteam.web", @team_id="FL726TVN3W.app.prayerteam", @key_id="YG33TJYKQ7", @private_key=#<OpenSSL::PKey::EC:0x00000001100a46f8 oid=id-ecPublicKey>, @secret=nil, @certificate=nil, @redirect_uri="https://exactly-holy-sunbird.ngrok-free.app/users/auth/apple/callback", @scheme=nil, @host=nil, @port=nil, @authorization_endpoint="https://appleid.apple.com/auth/authorize", @token_endpoint="https://appleid.apple.com/auth/token", @revocation_endpoint="https://appleid.apple.com/auth/revoke", @userinfo_endpoint="/userinfo", @expires_in=nil, @grant=#<Rack::OAuth2::Client::Grant::AuthorizationCode:0x000000011009eac8 @code="c46359e7ee0fc4df6a370c6a794cc7a9a.0.mryut.Qt7BFiM6rbza0iK5SEG4TA", @redirect_uri="https://exactly-holy-sunbird.ngrok-free.app/users/auth/apple/callback">>
12:07:13 web.1  | Completed 500 Internal Server Error in 465ms (ActiveRecord: 6.0ms | Allocations: 14572)
12:07:13 web.1  |
12:07:13 web.1  |
12:07:13 web.1  |
12:07:13 web.1  | AppleID::Client::Error (invalid_client):
12:07:13 web.1  |
12:07:13 web.1  | app/controllers/apple_signin_controller.rb:41:in `apple_callback'

I've included in the snipped above my @client variable.

Any ideas here? This is my second run at implementing this and have been stuck for hours trying to get basic Apple Signin to work.

What gem version are you using? Maybe you've got the latest one, and they've changed how the client receives the variables.

@davideluque
Copy link
Author

i get error OpenSSL::PKey::ECError (invalid curve name)

I've had the same problem. the issue for me was that I didn't provide APPLE_PRIVATE_KEY

@davideluque it would be awesome to edit application.yml APPLE_PEM to APPLE_PRIVATE_KEY as it's used at setup_apple_client

My bad thanks for the comment. I have updated the code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment