Skip to content

Instantly share code, notes, and snippets.

@Yorickov
Forked from davideluque/!README.MD
Created June 15, 2023 13:09
Show Gist options
  • Save Yorickov/4574aa4fa87a651909d70c82a6e0dc3c to your computer and use it in GitHub Desktop.
Save Yorickov/4574aa4fa87a651909d70c82a6e0dc3c 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)
unless @user.present
@user = User.register_user_from_apple(id_token.sub, id_token.email)
return created
else
# to prevent an account takeover vulnerability, show the user a message and make them sign in
# to link their account to apple. the user has to prove the ownership of the account by signin in
return unprocessable_entity
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