Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Apple Sign-In #1502

Open
wants to merge 1 commit into
base: the-future
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,5 @@ AOZORA_MONGO_URL=mongodb://localhost/aozora
ANIME_PLANET_PROXY_HOST=anime-planet.com

APPLE_VERIFICATION_URL=https://sandbox.itunes.apple.com/verifyReceipt

APPLE_CLIENT_ID=some_client_id
4 changes: 4 additions & 0 deletions app/graphql/types/account.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ def profile
null: true,
description: 'Twitter account linked to the account'

field :apple_id, String,
null: true,
description: 'AppleID linked to the account'

field :sfw_filter, Boolean,
null: true,
description: 'Whether Not Safe For Work content is accessible'
Expand Down
1 change: 1 addition & 0 deletions app/graphql/types/enum/external_identity_provider.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
class Types::Enum::ExternalIdentityProvider < Types::Enum::Base
value 'FACEBOOK', 'Facebook identity', value: :facebook
value 'APPLE', 'Apple Id identity', value: :apple
end
1 change: 1 addition & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ class User < ApplicationRecord
validates :gender, length: { maximum: 20 }
validates :password, length: { maximum: 72 }, if: :registered?
validates :facebook_id, uniqueness: true, allow_nil: true
validates :apple_id, uniqueness: true, allow_nil: true

scope :active, -> { where(deleted_at: nil) }
scope :by_slug, ->(*slugs) { where(slug: slugs&.flatten) }
Expand Down
4 changes: 2 additions & 2 deletions app/policies/user_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ def visible_attributes(all)
all
else
all - %i[email password confirmed previous_email language time_zone country share_to_global
title_language_preference sfw_filter rating_system theme facebook_id has_password
ao_pro]
title_language_preference sfw_filter rating_system theme facebook_id apple_id
has_password ao_pro]
end
end

Expand Down
3 changes: 3 additions & 0 deletions config/initializers/doorkeeper.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true

require 'authorization/assertion/facebook'
require 'authorization/assertion/apple'
require 'authorization/password'

Doorkeeper.configure do
Expand All @@ -24,6 +25,8 @@
case params[:provider]
when 'facebook'
Authorization::Assertion::Facebook.new(params[:assertion]).user!
when 'apple'
Authorization::Assertion::Apple.new(params[:id_token], params[:user]).user!
end
end
# Restrict access to the web interface for adding oauth applications
Expand Down
6 changes: 6 additions & 0 deletions db/migrate/20240401005041_add_apple_id_to_user.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class AddAppleIdToUser < ActiveRecord::Migration[6.1]
def change
add_column :users, :apple_id, :string, limit: 255
add_index :users, :apple_id, unique: true
end
end
4 changes: 3 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 2023_10_28_222209) do
ActiveRecord::Schema.define(version: 2024_04_01_005041) do

# These are extensions that must be enabled in order to support this database
enable_extension "citext"
Expand Down Expand Up @@ -1563,9 +1563,11 @@
t.jsonb "avatar_data"
t.jsonb "cover_image_data"
t.integer "sfw_filter_preference", default: 0, null: false
t.string "apple_id", limit: 255
t.index "lower((email)::text)", name: "users_lower_idx"
t.index "lower((name)::text), id", name: "index_users_on_lower_name_and_id", comment: "Unknown who is querying this, but it is painfully slow without this index"
t.index ["ao_id"], name: "index_users_on_ao_id", unique: true
t.index ["apple_id"], name: "index_users_on_apple_id", unique: true
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["facebook_id"], name: "index_users_on_facebook_id", unique: true
t.index ["slug"], name: "index_users_on_slug", unique: true
Expand Down
70 changes: 70 additions & 0 deletions lib/authorization/assertion/apple.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# frozen_string_literal: true

module Authorization
module Assertion
class Apple
APPLE_KEYS = 'https://appleid.apple.com/auth/keys'

def initialize(jwt, user_id)
@jwt = jwt
@user_id = user_id
end

def user!
return nil unless validate!
@user ||= User.where(apple_id:).first
shomykohai marked this conversation as resolved.
Show resolved Hide resolved
end

private

def data
@data ||= decode_jwt
end

def apple_id
data[:sub]
end

def email
data[:email]
end

def validate!
validate_aud && validate_exp && validate_sub && validate_iat && validate_iss
end

def validate_sub
apple_id == @user_id
end

def validate_iat
data[:iat].to_i <= Time.now.to_i
end

def validate_exp
data[:exp].to_i > Time.now.to_i
end

def validate_aud
data[:aud] == ENV['APPLE_CLIENT_ID']
end

def validate_iss
data[:iss] == 'https://appleid.apple.com'
end

def decode_jwt
return {} unless @jwt
jwt_header = JSON.parse(Base64.decode64(@jwt.split('.').first))

response = Net::HTTP.get(URI.parse(APPLE_KEYS))
apple_jwks = JSON.parse(response)
matching_key = apple_jwks['keys'].select { |key| key['kid'] == jwt_header['kid'] }

jwk = JWT::JWK.create_from(matching_key.first)
@decoded_jwt = JWT.decode(@jwt, jwk.public_key, true, algorithm: matching_key.first['alg'])
@decoded_jwt.first.deep_symbolize_keys
end
end
end
end
45 changes: 45 additions & 0 deletions spec/lib/authorization/assertion/apple_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# frozen_string_literal: true

require 'rails_helper'
require 'authorization/assertion/apple'

RSpec.describe Authorization::Assertion::Apple do
shomykohai marked this conversation as resolved.
Show resolved Hide resolved
let(:private_key) { OpenSSL::PKey::RSA.generate(2048) }
let(:apple_auth) { Authorization::Assertion::Apple.new(signed_jwt, user_id) }
shomykohai marked this conversation as resolved.
Show resolved Hide resolved
let(:jwk) { JWT::JWK.new(private_key) }
let(:jwt) do
{
iss: 'https://appleid.apple.com',
aud: ENV['APPLE_CLIENT_ID'],
iat: Time.now.to_i,
exp: Time.now.to_i + 10.minutes,
sub: 'xxx.yyy.zzz',
email: '[email protected]',
is_private_email: 'true'
}
end
let(:exported_private_key) { JWT::JWK::RSA.new(private_key).export.merge({ alg: 'RS256' }) }
let(:apple_jwks) { [exported_private_key] }
let(:signed_jwt) { JWT.encode(jwt, private_key, 'RS256', kid: jwk.kid) }
let(:user_id) { 'xxx.yyy.zzz' }

before do
stub_request(:get, 'https://appleid.apple.com/auth/keys').to_return(
body: {
keys: apple_jwks
}.to_json,
status: 200,
headers: { 'Content-Type': 'application/json' }
)
end

describe '#user!' do # rubocop:disable RSpec/MultipleMemoizedHelpers
subject(:test_subject) { apple_auth.user! }

let!(:user) { create(:user, apple_id: 'xxx.yyy.zzz') }

it 'returns the user' do
expect(test_subject).to eq(user)
end
end
end