How to Configure Sign in With Apple in Rails 8 without Omniauth
There are a couple of reasons you might want to setup "Sign in with Apple" in Rails 8 without using Omniauth.
- You don't want to use Omniauth
- You want to use the latest Apple authentication API and more importantly, the latest version of the nice looking button.
I just wanted the button.
But another reason you might want to do this is if you want to learn a bit more about the general OAuth flow and the weird quirks of the Apple API.
What you'll learn
- How to get "Sign in with Apple" working with Apple's JS Web configuration.
- How to use the apple_id gem to handle the important parts of the OAuth flow.
- How to validate the nonce during the OAuth flow, despite Apple breaking the spec a bit.
And most importantly, how to get this mostly right on the first try since Apple will not let you use localhost for the redirect URI. So you have to test it in production.
Assumptions
- You've got a Rails 8 app with a User model
- You have an Apple Developer account and already went through the steps to create an app ID, a service ID, and get the key.
- You have a controller that handles "sessions" for signing in and out of your app, but this guide does not cover Devise so you're on your own there.
- You have a user model with the following fields:
- email (string)
- name (string)
- oauth_uid (string)
- oauth_provider (string)
👀 If you haven't got your ids and keys from Apple yet, the nhosoya/omniauth-apple gem has a good guide.
Step 1: Configure your secrets and credentials
You can set these in your environment or using Rails credentials. I'll show Rails credentials below.
apple:
team_id: TEAMIDTEAM
client_id: com.example.auth
key_id: KEYIDKEYID
private_key: "-----BEGIN PRIVATE KEY-----\nPRIVATE KEY HERE\n-----END PRIVATE KEY-----\n"
team_id
is labeled as "App ID Prefix" when editing the App ID configuration.client_id
is the bundle ID of your app. I would recommend appendingauth
orclient
to the end of it to make it easier to remember what it's for.key_id
is the ID of the key you created and associated with your app.private_key
is the file you downloaded when creating the key. It has a name likeAuthKey_KEYIDKEYID.p8
⚠️ Important: Make sure you edit the private_key
entry to remove the newlines and replace them with a \n
character. You also need a trailing newline at the end of the string.
✅ After this step you'll have the secrets configured.
Step 2: Install the apple_id gem
Add gem "apple_id"
to your Gemfile. And run bundle install
.
The apple_id gem is a wrapper around the Apple ID API. It's a good starting point for building your own OAuth flow and it does the heavy lifting for us.
✅ After this step you'll have the gem installed too.
Step 3: Create a callback controller and route
You need a controller to handle the OAuth callback from Apple. For brevity, I will link to a documented example of this controller that you can copy and paste into your app.
⚠️ Important: Make sure you have a route to your callback that matches what you configured in the Apple Developer portal and that matches your redirect URI.
⚠️ Important: The controller example above uses cookies.signed[:user_id]
to indicate a Current.user
or current_user
or otherwise determine if the user is signed in. Your app might work differently.
# config/routes.rb
post "/auth/apple/callback", to: "apple_auth#callback", as: :apple_callback
In other OAuth implementations this is a get
request, but Apple requires a post
request. This is a quirk of Apple and there's a special cookie we're using to store the nonce that we'll see in Step 5.
✅ After this step you'll have a controller and route to handle the OAuth callback from Apple.
Step 4: Add the ability to find or create Users from Apple id_tokens
The appleid gem does all the OAuth work in the controller from Step 3. At the end you're left with an idtoken which has some information that you can use to find or create a User.
⚠️ Important: Apple only sends the user information once when the user first signs up. If you don't capture and save it then, you cannot get it again unless the user removes the connection to your app and tries again.
Again for brevity, here is a documented concern that can be included in your User model.
You'll include it in your User model like this:
class User < ApplicationRecord
include AppleAuthenticatable
end
✅ After this step you'll have a User model that can find or create a User from an Apple id_token and optional user information.
Step 5: Configure the nonce in your session view and controller
We want to configure our Apple OAuth request to use a nonce. This unique random value is sent to Apple when we initiate the OAuth request and then Apple returns it to us in the callback.
This allows us to verify that the request is coming from Apple and not a malicious actor.
However, because Apple sends a POST request, and Rails uses cookies for sessions that are set to lax
by default, we don't have access to the session
object in the apple auth controller.
So we need to store the nonce in a special cookie with different security settings so that we can access it in the apple auth controller during the callback and verify it against what's in the id_token.
This is something you can set this up as a before_action
or just call it in the controller action that's rendering the view with the button.
Here's an example of a method to use in a before_action
.
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
before_action :setup_apple_sign_in, only: [ :new, :create ]
# Your other methods here like new and create
private
def setup_apple_sign_in
cookies.encrypted[:apple_sign_in_nonce] = {
value: SecureRandom.hex(16),
expires: 5.minutes.from_now,
secure: true,
same_site: :none
}
end
end
✅ After this step you'll have a nonce stored in a special cookie that we can access in the apple auth controller during the callback.
Step 6: Add the Apple ID Button and required JS to your form
Apple describes how to add the button to a website in their Configuring your webpage with Sign in with Apple documentation.
Start by adding the button to your "Sign in" view (e.g. app/views/sessions/new.html.erb
), including the Apple JS and configuring the details of the OAuth request.
I think this is easiest to do using content_for
in your view so that the context is right next to the button and then you can just inject it into the head of your layout.
So, in your "Sign in" view (e.g. app/views/sessions/new.html.erb
) you'll have something like this:
<!-- button -->
<div id="appleid-signin" data-color="black" data-border="true" data-type="sign in"></div>
<!-- content_for to inject things into <head> -->
<% content_for :sign_in_with_apple do %>
<script type="text/javascript" src="https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js"></script>
<meta name="appleid-signin-client-id" content="<%= Rails.application.credentials.dig(:apple, :client_id) %>">
<meta name="appleid-signin-scope" content="email name">
<meta name="appleid-signin-redirect-uri" content="<%= apple_callback_url %>">
<meta name="appleid-signin-nonce" content="<%= cookies.encrypted[:apple_sign_in_nonce] %>">
<meta name="appleid-signin-use-popup" content="false">
<% end %>
- The
appleid-signin-client-id
is the client ID of your app. - The
appleid-signin-scope
is the scope of the OAuth request. - The
appleid-signin-redirect-uri
is the URL in your Rails app that will handle the OAuth response from apple. - The
appleid-signin-nonce
is a random string that is used to verify the request. - The
appleid-signin-use-popup
is a boolean that determines whether the popup will be used for the OAuth request.
⚠️ Important: You need to use a secure redirect URI. This means you can't use localhost. You have to use a real domain.
⚠️ Important: If you set the value of appleid-signin-use-popup
to true
, Apple WILL NOT send a POST request to your redirect URI. It will instead assume you are trying to use the Javascript API and are listening for DOM events.
This represents the default button style which is black with white logo and text. You can learn more about customzing the button in the Displaying Sign in with Apple buttons on the web documentation.
Then, in your layout (e.g. app/views/layouts/application.html.erb
) you'll yield the content_for to inject the details of the OAuth request into the <head>
of your layout.
<head>
<%= yield :sign_in_with_apple %>
</head>
✅ After this step you'll see the button rendering on your sign in page.
Step 7: Check everything twice
Since you can only test this in production* using a real domain with a certification, it's good to double check everything that can trip you up before you deploy.
- Note: You could setup another App, Service ID, and Key along with some sort of tunnel (Apple doesn't allow free ngrok domains in redirect uris) to test locally, but it's a pain.
- Check that you have the right credentials in your secrets.
- Make sure you configured your redirect URI in your Apple Developer portal.
- it should be the same as the
appleid-signin-redirect-uri
in your view. - it should be the same as the
apple_callback_url
in your routes. - it should look something like
https://example.com/auth/apple/callback
.
- it should be the same as the
- Make sure the user you are trying to create or find has the right fields.
- Also make sure that it would pass validation if you tried to save it with the oauthprovider and oauthuid set.
If you do end up having to deploy a few times, no problem. And if you need to test the "new user" flow, you can go to your Apple account and remove the app from the list of "Sign in with Apple" apps.
Step 8: Deploy and test it out
Assuming you're configured correctly and you copied and pasted the code from the previous steps, you should be good to go.
When you try to sign in with Apple, the flow is something like:
- Click the "Sign in with Apple" button.
- You see a popup asking you which email to use and to invoke Touch ID (if you have it set up).
- The popup goes away and you will find yourself on the redirect URI.
- If all goes well, you'll be redirected back to your application's root_url with a "signed in" user.