Skip to content

Instantly share code, notes, and snippets.

@LucasArruda
Forked from davideluque/!README.MD
Created April 18, 2022 14:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save LucasArruda/6882fe151e9465dd6fedb95904d19499 to your computer and use it in GitHub Desktop.
Save LucasArruda/6882fe151e9465dd6fedb95904d19499 to your computer and use it in GitHub Desktop.
Sign in with Apple in Ruby on Rails using apple_id gem.

Implementation of the Sign in with Apple service in Ruby on Rails. This implementation is convenient for Ruby on Rails APIs as it does not use views.

This implementation does:

  • Verify the user's identity token with apple servers to confirm that the token is not expired and ensure it has not been tampered with or replayed to the app.
  • Log in the user, register the user or connect the user's apple account to the user's existing account.

Parameters

  • code: Apple's authorizationCode after sign in. Example: c49a75458b1e74b9f8e866f5a93b1689a.0.nrtuy. ...
  • id_token: Apple's identityToken after sign in. Example: eyJraWQiOiJBSURPUEsxIiwiYWxnIjoiUlMyNT ...

Error response from Apple

The following block:

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

Rescues from an ErrorResponse received from Apple, due to an invalid value in the code parameter.

This error can occur when the :code parameter is invalid, because of a change in the Sign in with Apple’s configurations (identifier, private key, team, key id, redirect URI, etc) or a mismatch between the backend's configuration that makes the request to apple servers (this implementation) and the configuration used in the frontend to show the Sign in page.

# 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_PEM: "-----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'
Rails.application.routes.draw do
post 'auth/apple' => 'sessions#apple_callback'
end
class SessionsController < ApplicationController
before_action :setup_apple_client, only: [:apple_callback]
def apple_callback
return unprocessable_entity unless params[:code].present? && params[:identity_token].present?
@client.authorization_code = params[:code]
begin
token_response = @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: @client
access_token: token_response.access_token,
)
id_token_front_channel = AppleID::IdToken.decode(params[:identity_token])
id_token_front_channel.verify!(
client: @client,
code: params[:code],
)
id_token = token_response.id_token
# You may want to change the "find_by" method to a less time consuming method.
# 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
# Enable apple login to existing user
@user.update_column(:apple_uid, id_token.sub)
return sign_in_and_return
else
@user = User.register_user_from_apple(id_token.sub, id_token.email)
return created
end
end
private
def created
render status: 201, json: {
status: "success",
data: @user
}
end
def setup_apple_client
@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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment