Even if it's pretty simple to roll out your own authentication system with Rails, I've decided (again) to use OmniAuth. This time I'm using it with Apple, as I'm thinking on creating a Turbo Native app for iOS as well.
OmniAuth is a simple gem to install and use. It makes OAuth integration with your app a breathe. But once OmniAuth is installed and configured you have to choose which strategies install and, as always, Apple doesn't make it easy.
I'm going to document the process because I might follow the same path in the future.
First thing is to install
OmniAuth and the
Apple strategy. I'm not going to explain this step because the official docs are really good.
In order to test Sign in with Apple you're going to need an SSL certificate. There are many ways to do this. Because Apple won't let me use localhost as a domain, I'm going to use lvh.me, that is a domain that points to 127.0.0.1
I found
this guide on how to test SSL in localhost. All the credits to
Decidim. I'm just copy/pasting and adapting their code to fit my needs.
Run this command to generate a key and certificate. I copied my files to
/config/ssl.
openssl req -x509 -out lvhme.crt -keyout lvhme.key \
-newkey rsa:2048 -nodes -sha256 \
-subj '/CN=lvh.me' -extensions EXT -config <( \
printf "[dn]\nCN=lvh.me\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:lvh.me\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth")
Then, because I use bin/dev, I updated the Procfile to run rails with the certificate:
web: bin/rails server -b "ssl://lvh.me:3443?key=config/ssl/lvhme.key&cert=config/ssl/lvhme.crt"
Edit /config/development.rb and add:
config.force_ssl = true
config.hosts << "lvh.me"
Start the server and the web will be accessible on https://lvh.me:3443. You'll see a warning because the certificate is self-signed, but it's ok. This is enough for Sign in with Apple to work.
Everything should work by now. But it doesn't. If you try to sign in, you'll be redirected to an Apple website, but when Apple redirects back to your site, Rails will complain.
First thing is about the redirect method. This route should work fine:
match "/auth/:provider/callback", to: "sessions#create", via: [ :get, :post ]
The second issue is related to CSRF. Usually disabling authenticity token verification in your controller/action is enough:
skip_forgery_protection only: [ :create ]
But with Apple this is not enough. It took me some internet digging to find a solution.
This Github comment from last year fixed it:
In the OmniAuth callback controller,
private
def verified_request?
action_name == 'apple' || super
end
And in /config/application.rb
config.action_dispatch.cookies_same_site_protection = lambda { |request|
request.path == '/users/auth/apple' ? :none : :lax
}
If everything went as expected, when Apple redirects to your Rails app, the signed user's info should be accessible from request.env["omniauth.auth"].
Keep in mind that Apple will only send user's name and email the first time the user signs in. After the first sign in, you'll only receive tokens and identifiers.
This is the content of request.env["omniauth.auth"] including name and email:
{
"provider": "apple",
"uid": "",
"info": {
"sub": "",
"email": "",
"first_name": "",
"last_name": "",
"name": "",
"email_verified": true,
"is_private_email": false
},
"credentials": {
"token": "",
"refresh_token": "",
"expires_at": 1725018440,
"expires": true
},
"extra": {
"raw_info": {
"id_info": {
"iss": "https://appleid.apple.com",
"aud": "",
"exp": 1725101240,
"iat": 1725014840,
"sub": "",
"nonce": "",
"at_hash": "",
"email": "",
"email_verified": true,
"auth_time": 1725014838,
"nonce_supported": true
},
"user_info": {
"name": {
"firstName": "",
"lastName": ""
},
"email": ""
},
"id_token": ""
}
}
}