# Adding 'Sign in with Apple' to your Ruby on Rails 7.1 App: A Step-by-Step Guide

I added [omniauth-apple](https://github.com/nhosoya/omniauth-apple) to one of my side projects to get '[Sign in with Apple](https://support.apple.com/en-us/102609)' authentication. I hit a couple of issues that took me a while to fix, so I want to share how I solved them, hoping it saves others some time.

### Getting Started

I'm working with [Rails 7.1](https://edgeguides.rubyonrails.org/7_1_release_notes.html) and [authentication-zero](https://github.com/lazaronixon/authentication-zero). My `Gemfile` already has [omniauth](https://github.com/omniauth/omniauth) and [omniauth-rails\_csrf\_protection](https://github.com/cookpad/omniauth-rails_csrf_protection) because I passed the `--omniauthable` flag to the `authentication-zero` generator:

```bash
rails generate authentication --omniauthable
```

Next, I added `omniauth-apple` to my `Gemfile`:

```ruby
gem "omniauth-apple"
```

Configuring 'Sign in with Apple' requires several steps on the Apple side of the house that I won't go into. However, the short version is you will need an [Apple Developer](https://developer.apple.com/programs/) account, and you'll need to add some configuration to it. The `omniauth-apple` gem outlines how to do that in its [README](https://github.com/nhosoya/omniauth-apple#configuring-sign-in-with-apple).

Next, I added the Apple configuration to my [Custom Credentials](https://guides.rubyonrails.org/security.html#custom-credentials):

```yaml
apple:
  bundle_id: com.example
  app_id_prefix: <redacted>
  key_id: <redacted>
  private_key: |
    -----BEGIN PRIVATE KEY-----
    <redacted>
    -----END PRIVATE KEY-----
```

Then, I added the apple provider to `config/initializers/omniauth.rb`:

```ruby
Rails.application.config.middleware.use OmniAuth::Builder do
  provider :apple,
    Rails.application.credentials.apple[:bundle_id],
    "",
    {
      scope: "email name",
      team_id: Rails.application.credentials.apple[:app_id_prefix],
      key_id: Rails.application.credentials.apple[:key_id],
      pem: Rails.application.credentials.apple[:private_key]
    }
end
```

Finally, you must add a button to 'Sign in with Apple' on your login page. I'm using [tailwindcss](https://tailwindcss.com/) via [tailwindcss-rails](https://github.com/rails/tailwindcss-rails) to style the button:

```erb
<%= button_to "/auth/apple", "data-turbo" => false, class: "text-white bg-[#050708] hover:bg-[#050708]/90 focus:ring-4 focus:ring-[#050708]/50 font-medium rounded-lg text-sm px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-[#050708]/50 dark:hover:bg-[#050708]/30 mr-2 mb-2" do %>
  <svg class="mr-2 -ml-1 w-5 h-5" aria-hidden="true" focusable="false" data-prefix="fab" data-icon="apple" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path fill="currentColor" d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z"></path></svg>
  <span class="text-sm font-semibold leading-6">Sign in with Apple</span>
<% end %>
```

If you want to test this locally, you'll need to use `https`. There are multiple options for that, but I use [ngrok](https://ngrok.com/). Once you have an `https` URL pointing at your local Rails server, you'll need to add a couple of things to your `config/environments/development.rb`:

```ruby
config.force_ssl = true
config.hosts << "yourexternalurl.com" # If you're using something like ngrok
```

While we're at it, you might want to turn on `force_ssl` in your `config/environments/production.rb` for your deployed app:

```ruby
config.assume_ssl = true # You may need this depending on your prodution deployment
config.force_ssl = true
```

**Note: Don't forget to update your domains and redirect\_uris in your 'Sign in with Apple' configuration within your Apple Developer account (see the** [**README**](https://github.com/nhosoya/omniauth-apple#steps)**)**

The `force_ssl` configuration is crucial because it ensures that your Rails session cookie is set with the `Secure` attribute. We're going to set `SameSite` to `None` later to fix an error, [which only works with a secure cookie](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#none).

That's a fair bit of work! You are probably excited to try this out, but you'll likely encounter an error.

### **CSRF detected 😱**

If you click on your 'Sign in with Apple' button, everything should work fine until you're redirected back. If you're testing locally, you'll probably see this:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699580951011/682e8f1e-3484-44e1-871c-142353347e16.png align="center")

This took me quite a while to wrap my head around, and I'll explain what's going on, but if you want the fix and don't care about the explanation, add this to your `config/application.rb` (credit to [jmonteiro](https://github.com/nhosoya/omniauth-apple/issues/54#issuecomment-1334754066))

```ruby
config.action_dispatch.cookies_same_site_protection =
  lambda do |request|
    request.path.starts_with?("/auth/apple") ? :none : :lax
  end
```

### Why does this happen?

There are [multiple](https://www.bscotch.net/post/sign-in-with-apple-implementation-hurdles) [discussions](https://github.com/nhosoya/omniauth-apple/issues/54) [about](https://github.com/nhosoya/omniauth-apple/issues/64) what's happening here and even a [couple](https://github.com/nhosoya/omniauth-apple/pull/95) of [PRs](https://github.com/nhosoya/omniauth-apple/pull/107) against `omniauth-apple` that have been closed by the maintainer. A lot is going on, but I'll try to summarize it in a Rails context and provide links for more information along the way:

* [Rails uses a cookie to store sessions by default.](https://api.rubyonrails.org/classes/ActionDispatch/Session/CookieStore.html)
    
* Modern versions of Rails set the `SameSite` attribute to `Lax` on the session cookie (see the [PR](https://github.com/rails/rails/pull/28297) here for more explanation). This means the cookie is sent along to cross-site requests (like our 'Sign in with Apple' link), but it only works with "safe" HTTP methods like `GET`.
    
* Apple [somewhat breaks OAuth protocol](https://www.bscotch.net/post/sign-in-with-apple-implementation-hurdles) by redirecting back with a `POST` instead of a `GET`.
    
* Part of a secure OAuth handshake is generating a `state`, storing it, and then sending it along as a parameter to the identity provider (in this case, Apple). When the user is redirected back, the identity provider sends that `state` back as a parameter for the server to compare to its stored `state`. If it doesn't match, we have a [CSRF](https://owasp.org/www-community/attacks/csrf) problem (see more [here](https://auth0.com/docs/secure/attack-protection/state-parameters)).
    
* [omniauth-oauth2 stores the state](https://github.com/omniauth/omniauth-oauth2/blob/master/lib/omniauth/strategies/oauth2.rb#L75) in the [rack](https://github.com/rack/rack) session, which, in Rails, means storing it in a cookie (see the first bullet).
    
* Because of the `POST` redirect back from Apple and the `Lax` `SameSite` cookie setting, the `state` parameter never gets back to our Rails app, and `omniauth-oauth2` correctly raises `csrf_detected`.
    

Now, back to the fix (see above). In our case, we just set `SameSite` to `None`, but only in the case where we're hitting the omniauth `/auth/apple` route.

Is this secure? Well, you'll have to make that determination for yourself, but let me explain why I'm ok with it:

* We're selectively setting `SameSite` to `None` in the case where we're making a request to 'Sign in with Apple' (via `omniauth`). That means, on all our other routes, the session cookie is set with `SameSite` to `Lax`.
    
* We already have another form of CSRF protection built into OAuth with the state parameter.
    
* Rails [cookie sessions are encrypted](https://guides.rubyonrails.org/v7.1.0/security.html#session-storage), meaning if there was a man-in-the-middle attack, the attacker couldn't read the `state`, which means they couldn't send it back in the redirect and `omniauth-oauth2` would raise `csrf_detected`.
    

I hope that helps! If you feel like I got something wrong or you have a question, please comment or shoot me an email at [**mattlins@hey.com**](mailto:mattlins@hey.com)
