TITLE: 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. 1. You don't want to use Omniauth 2. 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 1. You've got a Rails 8 app with a User model 2. You have an Apple Developer account and already went through the steps to create an app ID, a service ID, and get the key. 3. 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. 4. You have a user model with the following fields: 1. email (string) 2. name (string) 3. oauth_uid (string) 4. oauth_provider (string) 👀 If you haven't got your ids and keys from Apple yet, the [nhosoya/omniauth-apple](https://github.com/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. ```yaml 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" ``` 1. `team_id` is labeled as "App ID Prefix" when editing the App ID configuration. 2. `client_id` is the bundle ID of your app. I would recommend appending `auth` or `client` to the end of it to make it easier to remember what it's for. 3. `key_id` is the ID of the key you created and associated with your app. 4. `private_key` is the file you downloaded when creating the key. It has a name like `AuthKey_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](https://github.com/nov/apple_id) 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. [apple_auth_controller.rb](https://gist.github.com/clayton/c1cbba8e4aa7c32a67506599aefdf4b5) ⚠️ 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. ```ruby # 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 apple_id gem does all the OAuth work in the controller from Step 3. At the end you're left with an id_token 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. [apple_authenticatable.rb](https://gist.github.com/clayton/556f80441f58b35451346dce466ecf53) You'll include it in your User model like this: ```ruby 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`. ```ruby # 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](https://developer.apple.com/documentation/sign_in_with_apple/configuring-your-webpage-for-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: ```erb
<% content_for :sign_in_with_apple do %> <% end %> ``` 1. The `appleid-signin-client-id` is the client ID of your app. 2. The `appleid-signin-scope` is the scope of the OAuth request. 3. The `appleid-signin-redirect-uri` is the URL in your Rails app that will handle the OAuth response from apple. 4. The `appleid-signin-nonce` is a random string that is used to verify the request. 5. 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](https://developer.apple.com/documentation/sign_in_with_apple/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 `` of your layout. ```erb <%= yield :sign_in_with_apple %> ``` ✅ 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. 1. Check that you have the right credentials in your secrets. 2. Make sure you configured your redirect URI in your Apple Developer portal. 1. it should be the same as the `appleid-signin-redirect-uri` in your view. 2. it should be the same as the `apple_callback_url` in your routes. 3. it should look something like `https://example.com/auth/apple/callback`. 3. Make sure the user you are trying to create or find has the right fields. 1. Also make sure that it would pass validation if you tried to save it with the oauth_provider and oauth_uid 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: 1. Click the "Sign in with Apple" button. 2. You see a popup asking you which email to use and to invoke Touch ID (if you have it set up). 3. The popup goes away and you will find yourself on the redirect URI. 4. If all goes well, you'll be redirected back to your application's root_url with a "signed in" user. ## References and attributions - [Adding 'Sign in with Apple' to your Ruby on Rails 7.1 App: A Step-by-Step Guide](https://www.mattlins.com/adding-sign-in-with-apple-to-your-ruby-on-rails-71-app-a-step-by-step-guide) - [apple_id gem](https://github.com/nov/apple_id) - [omniauth-apple gem](https://github.com/nhosoya/omniauth-apple)