apple_id 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +52 -3
- data/Rakefile +15 -2
- data/VERSION +1 -1
- data/apple_id.gemspec +6 -3
- data/lib/apple_id.rb +4 -1
- data/lib/apple_id/access_token.rb +10 -0
- data/lib/apple_id/client.rb +23 -5
- data/lib/apple_id/id_token.rb +45 -0
- metadata +58 -14
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: e000f6003a2baeecbb6683a9636b311cac3eec9ec20df988f79f0bbd50231135
         | 
| 4 | 
            +
              data.tar.gz: e4890bdb603981bcd9d4489915e607029490c221210816315f96dd3d6fccfd9d
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 1bb4b14f28e1906cf6517e2382422a5dd44c864479ea550bf2ad54a2758b166ab0464e0e04539f0e2f26c592daae932eba81d4f82f508b0aae2d45f956d1bcce
         | 
| 7 | 
            +
              data.tar.gz: 406ae5c6dca6868bfac746b83b3422b1a92876e6f6d75ced236f2b653b88e8d6469ca5544b33afae387d8bca78aa2237608e1557022daff8d3f334667c8e1663
         | 
    
        data/README.md
    CHANGED
    
    | @@ -1,8 +1,10 @@ | |
| 1 1 | 
             
            # AppleID
         | 
| 2 2 |  | 
| 3 | 
            -
             | 
| 3 | 
            +
            "Sign-in with Apple" is an implementation of OpenID Connect with small custom features.
         | 
| 4 4 |  | 
| 5 | 
            -
             | 
| 5 | 
            +
            This gem handles such custom features.
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            Basically, this gem is based on my [OpenID Connect gem](https://github.com/nov/openid_connect) and [OAuth2 gem](https://github.com/nov/rack-oauth2), so the usage is almost same with them.
         | 
| 6 8 |  | 
| 7 9 | 
             
            ## Installation
         | 
| 8 10 |  | 
| @@ -22,7 +24,54 @@ Or install it yourself as: | |
| 22 24 |  | 
| 23 25 | 
             
            ## Usage
         | 
| 24 26 |  | 
| 25 | 
            -
             | 
| 27 | 
            +
            There is [a sample rails app](https://github.com/nov/signin-with-apple) running at [signin-with-apple.herokuapp.com](https://signin-with-apple.herokuapp.com).
         | 
| 28 | 
            +
             | 
| 29 | 
            +
            If you run script in your terminal only, do like below.
         | 
| 30 | 
            +
             | 
| 31 | 
            +
            ```ruby
         | 
| 32 | 
            +
            require 'apple_id'
         | 
| 33 | 
            +
             | 
| 34 | 
            +
            # NOTE: in debugging mode, you can see all HTTPS request & response in the log.
         | 
| 35 | 
            +
            # AppleID.debug!
         | 
| 36 | 
            +
             | 
| 37 | 
            +
            pem = <<-PEM
         | 
| 38 | 
            +
            -----BEGIN PRIVATE KEY-----
         | 
| 39 | 
            +
              :
         | 
| 40 | 
            +
              :
         | 
| 41 | 
            +
            -----END PRIVATE KEY-----
         | 
| 42 | 
            +
            PEM
         | 
| 43 | 
            +
            private_key = OpenSSL::PKey::EC.new pem
         | 
| 44 | 
            +
             | 
| 45 | 
            +
            client = AppleID::Client.new(
         | 
| 46 | 
            +
              identifier: '<YOUR-CLIENT-ID>',
         | 
| 47 | 
            +
              team_id: '<YOUR-TEAM-ID>',
         | 
| 48 | 
            +
              key_id: '<YOUR-KEY-ID>',
         | 
| 49 | 
            +
              private_key: private_key,
         | 
| 50 | 
            +
              redirect_uri: '<YOUR-REDIRECT-URI>'
         | 
| 51 | 
            +
            )
         | 
| 52 | 
            +
             | 
| 53 | 
            +
            authorization_uri = client.authorization_uri(scope: [:email, :name])
         | 
| 54 | 
            +
            puts authorization_uri
         | 
| 55 | 
            +
            `open "#{authorization_uri}"`
         | 
| 56 | 
            +
             | 
| 57 | 
            +
            print 'code: ' and STDOUT.flush
         | 
| 58 | 
            +
            code = gets.chop
         | 
| 59 | 
            +
             | 
| 60 | 
            +
            client.authorization_code = code
         | 
| 61 | 
            +
            response = client.access_token!
         | 
| 62 | 
            +
             | 
| 63 | 
            +
            response.id_token.verify!(
         | 
| 64 | 
            +
              client,
         | 
| 65 | 
            +
              access_token: response.access_token,
         | 
| 66 | 
            +
             | 
| 67 | 
            +
              # NOTE:
         | 
| 68 | 
            +
              #  When verifying signature, one http request to Apple's JWKs are required.
         | 
| 69 | 
            +
              #  You can skip ID Token signature verification when you got the token directly from the token endpoint in TLS channel.
         | 
| 70 | 
            +
              verify_signature: false
         | 
| 71 | 
            +
            )
         | 
| 72 | 
            +
            puts response.id_token.sub # => OpenID Connect Subject Identifier (= Apple User ID)
         | 
| 73 | 
            +
            puts response.id_token.original_jwt.pretty_generate
         | 
| 74 | 
            +
            ```
         | 
| 26 75 |  | 
| 27 76 | 
             
            ## Development
         | 
| 28 77 |  | 
    
        data/Rakefile
    CHANGED
    
    | @@ -1,6 +1,19 @@ | |
| 1 | 
            -
            require  | 
| 2 | 
            -
             | 
| 1 | 
            +
            require 'bundler'
         | 
| 2 | 
            +
            Bundler::GemHelper.install_tasks
         | 
| 3 3 |  | 
| 4 | 
            +
            require 'rspec/core/rake_task'
         | 
| 4 5 | 
             
            RSpec::Core::RakeTask.new(:spec)
         | 
| 5 6 |  | 
| 7 | 
            +
            namespace :coverage do
         | 
| 8 | 
            +
              desc 'Open coverage report'
         | 
| 9 | 
            +
              task :report do
         | 
| 10 | 
            +
                require 'simplecov'
         | 
| 11 | 
            +
                `open "#{File.join SimpleCov.coverage_path, 'index.html'}"`
         | 
| 12 | 
            +
              end
         | 
| 13 | 
            +
            end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            task :spec do
         | 
| 16 | 
            +
              Rake::Task[:'coverage:report'].invoke unless ENV['TRAVIS_RUBY_VERSION']
         | 
| 17 | 
            +
            end
         | 
| 18 | 
            +
             | 
| 6 19 | 
             
            task :default => :spec
         | 
    
        data/VERSION
    CHANGED
    
    | @@ -1 +1 @@ | |
| 1 | 
            -
            0.0 | 
| 1 | 
            +
            0.1.0
         | 
    
        data/apple_id.gemspec
    CHANGED
    
    | @@ -20,7 +20,10 @@ Gem::Specification.new do |spec| | |
| 20 20 |  | 
| 21 21 | 
             
              spec.add_runtime_dependency 'rack-oauth2', '~> 1.9.3'
         | 
| 22 22 | 
             
              spec.add_runtime_dependency 'openid_connect', '~> 1.0'
         | 
| 23 | 
            -
              spec.add_development_dependency 'bundler' | 
| 24 | 
            -
              spec.add_development_dependency 'rake' | 
| 25 | 
            -
              spec.add_development_dependency 'rspec' | 
| 23 | 
            +
              spec.add_development_dependency 'bundler'
         | 
| 24 | 
            +
              spec.add_development_dependency 'rake'
         | 
| 25 | 
            +
              spec.add_development_dependency 'rspec'
         | 
| 26 | 
            +
              spec.add_development_dependency 'rspec-its'
         | 
| 27 | 
            +
              spec.add_development_dependency 'webmock'
         | 
| 28 | 
            +
              spec.add_development_dependency 'simplecov'
         | 
| 26 29 | 
             
            end
         | 
    
        data/lib/apple_id.rb
    CHANGED
    
    | @@ -2,6 +2,7 @@ require 'openid_connect' | |
| 2 2 |  | 
| 3 3 | 
             
            module AppleID
         | 
| 4 4 | 
             
              ISSUER = 'https://appleid.apple.com'
         | 
| 5 | 
            +
              JWKS_URI = 'https://appleid.apple.com/auth/keys'
         | 
| 5 6 |  | 
| 6 7 | 
             
              VERSION = ::File.read(
         | 
| 7 8 | 
             
                ::File.join(::File.dirname(__FILE__), '../VERSION')
         | 
| @@ -50,4 +51,6 @@ module AppleID | |
| 50 51 | 
             
              self.debugging = false
         | 
| 51 52 | 
             
            end
         | 
| 52 53 |  | 
| 53 | 
            -
            require 'apple_id/client'
         | 
| 54 | 
            +
            require 'apple_id/client'
         | 
| 55 | 
            +
            require 'apple_id/access_token'
         | 
| 56 | 
            +
            require 'apple_id/id_token'
         | 
| @@ -0,0 +1,10 @@ | |
| 1 | 
            +
            module AppleID
         | 
| 2 | 
            +
              class AccessToken < OpenIDConnect::AccessToken
         | 
| 3 | 
            +
                undef_required_attributes :client
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                def initialize(access_token, attributes = {})
         | 
| 6 | 
            +
                  super attributes.merge(access_token: access_token)
         | 
| 7 | 
            +
                  self.id_token = IdToken.decode(id_token) if id_token.present?
         | 
| 8 | 
            +
                end
         | 
| 9 | 
            +
              end
         | 
| 10 | 
            +
            end
         | 
    
        data/lib/apple_id/client.rb
    CHANGED
    
    | @@ -1,12 +1,13 @@ | |
| 1 1 | 
             
            module AppleID
         | 
| 2 2 | 
             
              class Client < OpenIDConnect::Client
         | 
| 3 | 
            +
                class Error < Rack::OAuth2::Client::Error; end
         | 
| 4 | 
            +
             | 
| 3 5 | 
             
                attr_required :team_id, :key_id, :private_key
         | 
| 4 6 |  | 
| 5 7 | 
             
                def initialize(attributes)
         | 
| 6 8 | 
             
                  attributes_with_default = {
         | 
| 7 | 
            -
                     | 
| 8 | 
            -
                     | 
| 9 | 
            -
                    token_endpoint: '/auth/token'
         | 
| 9 | 
            +
                    authorization_endpoint: File.join(ISSUER, '/auth/authorize'),
         | 
| 10 | 
            +
                    token_endpoint: File.join(ISSUER, '/auth/token')
         | 
| 10 11 | 
             
                  }.merge(attributes)
         | 
| 11 12 | 
             
                  super attributes_with_default
         | 
| 12 13 | 
             
                end
         | 
| @@ -21,7 +22,7 @@ module AppleID | |
| 21 22 | 
             
                def client_secret_jwt
         | 
| 22 23 | 
             
                  jwt = JSON::JWT.new(
         | 
| 23 24 | 
             
                    iss: team_id,
         | 
| 24 | 
            -
                    aud:  | 
| 25 | 
            +
                    aud: ISSUER,
         | 
| 25 26 | 
             
                    sub: identifier,
         | 
| 26 27 | 
             
                    iat: Time.now,
         | 
| 27 28 | 
             
                    exp: 1.minutes.from_now
         | 
| @@ -29,5 +30,22 @@ module AppleID | |
| 29 30 | 
             
                  jwt.kid = key_id
         | 
| 30 31 | 
             
                  jwt.sign(private_key)
         | 
| 31 32 | 
             
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                def setup_required_scope(scopes)
         | 
| 35 | 
            +
                  # NOTE:
         | 
| 36 | 
            +
                  #  openid_connect gem add `openid` scope automatically.
         | 
| 37 | 
            +
                  #  However, it's not required for Sign-in with Apple.
         | 
| 38 | 
            +
                  scopes
         | 
| 39 | 
            +
                end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                def handle_success_response(response)
         | 
| 42 | 
            +
                  token_hash = JSON.parse(response.body).with_indifferent_access
         | 
| 43 | 
            +
                  AccessToken.new token_hash.delete(:access_token), token_hash
         | 
| 44 | 
            +
                end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                def handle_error_response(response)
         | 
| 47 | 
            +
                  error = JSON.parse(response.body).with_indifferent_access
         | 
| 48 | 
            +
                  raise Error.new(response.status, error)
         | 
| 49 | 
            +
                end
         | 
| 32 50 | 
             
              end
         | 
| 33 | 
            -
            end
         | 
| 51 | 
            +
            end
         | 
| @@ -0,0 +1,45 @@ | |
| 1 | 
            +
            module AppleID
         | 
| 2 | 
            +
              class IdToken < OpenIDConnect::ResponseObject::IdToken
         | 
| 3 | 
            +
                class VerificationFailed < StandardError; end
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                alias_method :original_jwt, :raw_attributes
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                def verify!(expected_client, access_token: nil, code: nil, verify_signature: true)
         | 
| 8 | 
            +
                  verify_signature! if verify_signature
         | 
| 9 | 
            +
                  verify_claims! expected_client, access_token, code
         | 
| 10 | 
            +
                  self
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                class << self
         | 
| 14 | 
            +
                  def decode(jwt_string)
         | 
| 15 | 
            +
                    super jwt_string, :skip_verification
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                private
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def jwks
         | 
| 22 | 
            +
                  @jwks ||= JSON.parse(
         | 
| 23 | 
            +
                    OpenIDConnect.http_client.get_content(JWKS_URI)
         | 
| 24 | 
            +
                  ).with_indifferent_access
         | 
| 25 | 
            +
                  JSON::JWK::Set.new @jwks[:keys]
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                def verify_signature!
         | 
| 29 | 
            +
                  original_jwt.verify! jwks
         | 
| 30 | 
            +
                rescue
         | 
| 31 | 
            +
                  raise VerificationFailed, 'Signature Verification Failed'
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                def verify_claims!(expected_client, access_token, code)
         | 
| 35 | 
            +
                  # TODO: verify at_hash & c_hash
         | 
| 36 | 
            +
                  unless (
         | 
| 37 | 
            +
                    iss == ISSUER &&
         | 
| 38 | 
            +
                    aud == expected_client.identifier &&
         | 
| 39 | 
            +
                    Time.now.to_i.between?(iat, exp)
         | 
| 40 | 
            +
                  )
         | 
| 41 | 
            +
                    raise VerificationFailed, 'Claims Verification Failed'
         | 
| 42 | 
            +
                  end
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
              end
         | 
| 45 | 
            +
            end
         | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: apple_id
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0.0 | 
| 4 | 
            +
              version: 0.1.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - nov
         | 
| 8 8 | 
             
            autorequire: 
         | 
| 9 9 | 
             
            bindir: exe
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2019-06- | 
| 11 | 
            +
            date: 2019-06-05 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: rack-oauth2
         | 
| @@ -42,44 +42,86 @@ dependencies: | |
| 42 42 | 
             
              name: bundler
         | 
| 43 43 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| 44 44 | 
             
                requirements:
         | 
| 45 | 
            -
                - - " | 
| 45 | 
            +
                - - ">="
         | 
| 46 46 | 
             
                  - !ruby/object:Gem::Version
         | 
| 47 | 
            -
                    version: ' | 
| 47 | 
            +
                    version: '0'
         | 
| 48 48 | 
             
              type: :development
         | 
| 49 49 | 
             
              prerelease: false
         | 
| 50 50 | 
             
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 51 51 | 
             
                requirements:
         | 
| 52 | 
            -
                - - " | 
| 52 | 
            +
                - - ">="
         | 
| 53 53 | 
             
                  - !ruby/object:Gem::Version
         | 
| 54 | 
            -
                    version: ' | 
| 54 | 
            +
                    version: '0'
         | 
| 55 55 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 56 56 | 
             
              name: rake
         | 
| 57 57 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| 58 58 | 
             
                requirements:
         | 
| 59 | 
            -
                - - " | 
| 59 | 
            +
                - - ">="
         | 
| 60 60 | 
             
                  - !ruby/object:Gem::Version
         | 
| 61 | 
            -
                    version: ' | 
| 61 | 
            +
                    version: '0'
         | 
| 62 62 | 
             
              type: :development
         | 
| 63 63 | 
             
              prerelease: false
         | 
| 64 64 | 
             
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 65 65 | 
             
                requirements:
         | 
| 66 | 
            -
                - - " | 
| 66 | 
            +
                - - ">="
         | 
| 67 67 | 
             
                  - !ruby/object:Gem::Version
         | 
| 68 | 
            -
                    version: ' | 
| 68 | 
            +
                    version: '0'
         | 
| 69 69 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 70 70 | 
             
              name: rspec
         | 
| 71 71 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| 72 72 | 
             
                requirements:
         | 
| 73 | 
            -
                - - " | 
| 73 | 
            +
                - - ">="
         | 
| 74 74 | 
             
                  - !ruby/object:Gem::Version
         | 
| 75 | 
            -
                    version: ' | 
| 75 | 
            +
                    version: '0'
         | 
| 76 76 | 
             
              type: :development
         | 
| 77 77 | 
             
              prerelease: false
         | 
| 78 78 | 
             
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 79 79 | 
             
                requirements:
         | 
| 80 | 
            -
                - - " | 
| 80 | 
            +
                - - ">="
         | 
| 81 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 82 | 
            +
                    version: '0'
         | 
| 83 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 84 | 
            +
              name: rspec-its
         | 
| 85 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 86 | 
            +
                requirements:
         | 
| 87 | 
            +
                - - ">="
         | 
| 88 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 89 | 
            +
                    version: '0'
         | 
| 90 | 
            +
              type: :development
         | 
| 91 | 
            +
              prerelease: false
         | 
| 92 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 93 | 
            +
                requirements:
         | 
| 94 | 
            +
                - - ">="
         | 
| 95 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 96 | 
            +
                    version: '0'
         | 
| 97 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 98 | 
            +
              name: webmock
         | 
| 99 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 100 | 
            +
                requirements:
         | 
| 101 | 
            +
                - - ">="
         | 
| 102 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 103 | 
            +
                    version: '0'
         | 
| 104 | 
            +
              type: :development
         | 
| 105 | 
            +
              prerelease: false
         | 
| 106 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 107 | 
            +
                requirements:
         | 
| 108 | 
            +
                - - ">="
         | 
| 109 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 110 | 
            +
                    version: '0'
         | 
| 111 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 112 | 
            +
              name: simplecov
         | 
| 113 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 114 | 
            +
                requirements:
         | 
| 115 | 
            +
                - - ">="
         | 
| 116 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 117 | 
            +
                    version: '0'
         | 
| 118 | 
            +
              type: :development
         | 
| 119 | 
            +
              prerelease: false
         | 
| 120 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 121 | 
            +
                requirements:
         | 
| 122 | 
            +
                - - ">="
         | 
| 81 123 | 
             
                  - !ruby/object:Gem::Version
         | 
| 82 | 
            -
                    version: ' | 
| 124 | 
            +
                    version: '0'
         | 
| 83 125 | 
             
            description: Sign-in with Apple backend library in Ruby.
         | 
| 84 126 | 
             
            email:
         | 
| 85 127 | 
             
            - nov@matake.jp
         | 
| @@ -100,7 +142,9 @@ files: | |
| 100 142 | 
             
            - bin/console
         | 
| 101 143 | 
             
            - bin/setup
         | 
| 102 144 | 
             
            - lib/apple_id.rb
         | 
| 145 | 
            +
            - lib/apple_id/access_token.rb
         | 
| 103 146 | 
             
            - lib/apple_id/client.rb
         | 
| 147 | 
            +
            - lib/apple_id/id_token.rb
         | 
| 104 148 | 
             
            homepage: https://github.com/nov/apple_id
         | 
| 105 149 | 
             
            licenses:
         | 
| 106 150 | 
             
            - MIT
         |