Skip to content

Instantly share code, notes, and snippets.

@clayton
Last active April 16, 2025 20:43
Show Gist options
  • Save clayton/c1cbba8e4aa7c32a67506599aefdf4b5 to your computer and use it in GitHub Desktop.
Save clayton/c1cbba8e4aa7c32a67506599aefdf4b5 to your computer and use it in GitHub Desktop.
Apple Auth Callback controller
class AppleAuthController < ApplicationController
# Skip CSRF for Apple callback and initialize client only when needed
before_action :apple_client, only: [ :callback ]
skip_before_action :verify_authenticity_token, only: [ :callback ]
def callback
# Validate parameters before proceeding
return unprocessable_entity unless valid_code?
return unprocessable_entity unless valid_id_token?
@apple_client.authorization_code = params[:code]
begin
# Exchange authorization code for access token
token_response = @apple_client.access_token!
rescue AppleID::Client::Error => e
# Log authentication errors to Rails logger
Rails.logger.error("Apple authentication error: #{e.message}")
return unauthorized
end
# Verify ID token received through back channel (server-to-server)
id_token_back_channel = token_response.id_token
id_token_back_channel.verify!(
client: @apple_client,
access_token: token_response.access_token
)
# Verify ID token received from front channel (browser)
id_token_front_channel = AppleID::IdToken.decode(params[:id_token])
id_token_front_channel.verify!(
client: @apple_client,
code: params[:code]
)
id_token = token_response.id_token
# Verify nonce to prevent replay attacks
unless valid_nonce?(id_token)
return unauthorized
end
# Find or create user using Apple's unique identifier
# 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_or_create_from_apple(id_token, parsed_user_params)
params[:nonce] = nil
sign_in_and_redirect
end
private
# Initialize Apple OAuth client with credentials
def apple_client
@apple_client ||= AppleID::Client.new(
identifier: Rails.application.credentials.apple[:client_id],
team_id: Rails.application.credentials.apple[:team_id],
key_id: Rails.application.credentials.apple[:key_id],
private_key: OpenSSL::PKey::EC.new(Rails.application.credentials.apple[:private_key]),
redirect_uri: apple_callback_url
)
end
# Set user cookie and redirect to home page
def sign_in_and_redirect
cookies.signed[:user_id] = @user.id
redirect_to root_url
end
# Handle authentication failures
def unauthorized
flash[:alert] = "Unauthorized"
redirect_to new_session_path
end
# Handle invalid request parameters
def unprocessable_entity
flash[:alert] = "Sorry, there was something wrong with the request. Please try signing in again."
redirect_to new_session_path
end
# Verify authorization code is present
def valid_code?
params[:code].present?
end
# Verify ID token is present
def valid_id_token?
params[:id_token].present?
end
# Verify nonce matches to prevent CSRF/replay attacks
def valid_nonce?(id_token)
return unless id_token.nonce_supported == true || id_token.nonce_supported == "true"
# First, read the nonce value
stored_nonce = cookies.encrypted[:apple_sign_in_nonce]
# Then delete it
cookies.delete(:apple_sign_in_nonce)
# Compare with the nonce from the id_token
id_token.nonce == stored_nonce
end
# Parse user data from Apple's response
def parsed_user_params
return {} unless params[:user].present?
JSON.parse(params[:user])
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment