Skip to content

Commit

Permalink
Merge pull request #29 from kdgm/validate-server-to-server-callbacks
Browse files Browse the repository at this point in the history
Handle server to server notifications
  • Loading branch information
TimoPeraza authored Mar 18, 2022
2 parents d1aa92d + b54df64 commit d71d10d
Show file tree
Hide file tree
Showing 11 changed files with 310 additions and 37 deletions.
53 changes: 49 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# AppleAuth

[![CI](https://api.travis-ci.org/rootstrap/apple_auth.svg?branch=master)](https://travis-ci.org/github/rootstrap/apple_auth)
[![CI](https://api.travis-ci.com/rootstrap/apple_auth.svg?branch=master)](https://travis-ci.com/github/rootstrap/apple_auth)
[![Maintainability](https://api.codeclimate.com/v1/badges/78453501221a76e3806e/maintainability)](https://codeclimate.com/github/rootstrap/apple_sign_in/maintainability)
[![Test Coverage](https://api.codeclimate.com/v1/badges/78453501221a76e3806e/test_coverage)](https://codeclimate.com/github/rootstrap/apple_sign_in/test_coverage)

Expand Down Expand Up @@ -56,13 +56,13 @@ end

We strongly recommend to use environment variables for these values.

Apple sign-in workflow:
### Apple sign-in workflow:

![alt text](https://docs-assets.developer.apple.com/published/360d59b776/rendered2x-1592224731.png)

For more information, check the [Apple oficial documentation](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api).

Validate JWT token and get user information:
### Validate JWT token and get user information:

```ruby
# with a valid JWT
Expand All @@ -79,14 +79,59 @@ AppleAuth::UserIdentity.new(user_id, invalid_jwt_token).validate!
>> AppleAuth::Conditions::JWTValidationError
```

Verify user identity and get access and refresh tokens:
### Verify user identity and get access and refresh tokens:

```ruby
code = 'cfb77c21ecd444390a2c214cd33decdfb.0.mr...'
AppleAuth::Token.new(code).authenticate!
>> { access_token: "a7058d...", expires_at: 1595894672, refresh_token: "r8f1ce..." }
```

### Handle server to server notifications

from the request parameter :payload

```ruby
# with a valid JWT
params[:payload] = "eyJraWQiOiJZ......"
AppleAuth::ServerIdentity.new(params[:payload]).validate!
>> {iss: "https://appleid.apple.com", exp: 1632224024, iat: 1632137624, jti: "yctpp1ZHaGCzaNB9PWB4DA",...}

# with an invalid JWT
params[:payload] = "asdasdasdasd......"
AppleAuth::ServerIdentity.new(params[:payload]).validate!
>> JWT::VerificationError: Signature verification raised
```

Implementation in a controller would look like this:

```ruby
class Hooks::AuthController < ApplicationController

skip_before_action :verify_authenticity_token

# https://developer.apple.com/documentation/sign_in_with_apple/processing_changes_for_sign_in_with_apple_accounts
# NOTE: The Apple documentation states the events attribute as an array but is in fact a stringified json object
def apple
# will raise an error when the signature is invalid
payload = AppleAuth::ServerIdentity.new(params[:payload]).validate!
event = JSON.parse(payload[:events]).symbolize_keys
uid = event["sub"]
user = User.find_by!(provider: 'apple', uid: uid)

case event[:type]
when "email-enabled", "email-disabled"
# Here we should update the user with the relay state
when "consent-revoked", "account-delete"
user.destroy!
else
throw event
end
render plain: "200 OK", status: :ok
end
end
```

## Using with Devise

If you are using devise_token_auth gem, run this generator.
Expand Down
3 changes: 3 additions & 0 deletions lib/apple_auth.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@
require 'apple_auth/helpers/conditions/iat_condition'
require 'apple_auth/helpers/conditions/iss_condition'
require 'apple_auth/helpers/jwt_conditions'
require 'apple_auth/helpers/jwt_decoder'
require 'apple_auth/helpers/jwt_server_conditions'

require 'apple_auth/server_identity'
require 'apple_auth/user_identity'
require 'apple_auth/token'

Expand Down
37 changes: 37 additions & 0 deletions lib/apple_auth/helpers/jwt_decoder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# frozen_string_literal: false

module AppleAuth
class JWTDecoder
APPLE_KEY_URL = 'https://appleid.apple.com/auth/keys'.freeze

attr_reader :jwt

def initialize(jwt)
@jwt = jwt
end

def call
decoded.first
end

private

def decoded
key_hash = apple_key_hash(jwt)
apple_jwk = JWT::JWK.import(key_hash)
JWT.decode(jwt, apple_jwk.public_key, true, algorithm: key_hash['alg'])
end

def apple_key_hash(jwt)
response = Net::HTTP.get(URI.parse(APPLE_KEY_URL))
certificate = JSON.parse(response)
matching_key = certificate['keys'].select { |key| key['kid'] == jwt_kid(jwt) }
ActiveSupport::HashWithIndifferentAccess.new(matching_key.first)
end

def jwt_kid(jwt)
header = JSON.parse(Base64.decode64(jwt.split('.').first))
header['kid']
end
end
end
34 changes: 34 additions & 0 deletions lib/apple_auth/helpers/jwt_server_conditions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: false

module AppleAuth
class JWTServerConditions
include Conditions

CONDITIONS = [
AudCondition,
IatCondition,
IssCondition
].freeze

attr_reader :decoded_jwt

def initialize(decoded_jwt)
@decoded_jwt = decoded_jwt
end

def validate!
JWT::ClaimsValidator.new(decoded_jwt).validate! && jwt_conditions_validate!
rescue JWT::InvalidPayload => e
raise JWTValidationError, e.message
end

private

def jwt_conditions_validate!
conditions_results = CONDITIONS.map do |condition|
condition.new(decoded_jwt).validate!
end
conditions_results.all? { |value| value == true }
end
end
end
19 changes: 19 additions & 0 deletions lib/apple_auth/server_identity.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

module AppleAuth
class ServerIdentity
attr_reader :jwt

def initialize(jwt)
@jwt = jwt
end

def validate!
token_data = JWTDecoder.new(jwt).call

JWTServerConditions.new(token_data).validate!

token_data.symbolize_keys
end
end
end
24 changes: 1 addition & 23 deletions lib/apple_auth/user_identity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

module AppleAuth
class UserIdentity
APPLE_KEY_URL = 'https://appleid.apple.com/auth/keys'

attr_reader :user_identity, :jwt

def initialize(user_identity, jwt)
Expand All @@ -12,31 +10,11 @@ def initialize(user_identity, jwt)
end

def validate!
token_data = decoded_jwt
token_data = JWTDecoder.new(jwt).call

JWTConditions.new(user_identity, token_data).validate!

token_data.symbolize_keys
end

private

def decoded_jwt
key_hash = apple_key_hash
apple_jwk = JWT::JWK.import(key_hash)
JWT.decode(jwt, apple_jwk.public_key, true, algorithm: key_hash['alg']).first
end

def apple_key_hash
response = Net::HTTP.get(URI.parse(APPLE_KEY_URL))
certificate = JSON.parse(response)
matching_key = certificate['keys'].select { |key| key['kid'] == jwt_kid }
ActiveSupport::HashWithIndifferentAccess.new(matching_key.first)
end

def jwt_kid
header = JSON.parse(Base64.decode64(jwt.split('.').first))
header['kid']
end
end
end
6 changes: 3 additions & 3 deletions spec/helpers/jwt_conditions_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
end
end

context 'when exp is not a integer' do
context 'when iat is not a integer' do
let(:jwt_iat) { Time.now }

it 'raises an exception' do
Expand All @@ -59,7 +59,7 @@
end
end

context 'when jwt iss is different to user_identity' do
context 'when jwt sub is different to user_identity' do
let(:jwt_sub) { '1234.5678.911' }

it 'raises an exception' do
Expand All @@ -69,7 +69,7 @@
end
end

context 'when jwt_aud is different to apple_client_id' do
context 'when jwt aud is different to apple_client_id' do
let(:jwt_aud) { 'net.apple_auth' }

it 'raises an exception' do
Expand Down
82 changes: 82 additions & 0 deletions spec/helpers/jwt_server_conditions_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe AppleAuth::JWTServerConditions do
let(:jwt_sub) { '820417.faa325acbc78e1be1668ba852d492d8a.0219' }
let(:jwt_iss) { 'https://appleid.apple.com' }
let(:jwt_aud) { 'com.apple_auth' }
let(:jwt_iat) { Time.now.to_i }
let(:jwt) do
{
iss: jwt_iss,
aud: jwt_aud,
iat: jwt_iat,
events: '{
"type": "email-enabled",
"sub": "820417.faa325acbc78e1be1668ba852d492d8a.0219",
"email": "[email protected]",
"is_private_email": "true",
"event_time": 1508184845
}'
}
end

let(:decoded_jwt) { ActiveSupport::HashWithIndifferentAccess.new(jwt) }

before do
AppleAuth.config.apple_client_id = 'com.apple_auth'
end

subject(:jwt_conditions_helper) { described_class.new(decoded_jwt) }

context '#valid?' do
context 'when decoded jwt attributes are valid' do
it 'returns true' do
expect(jwt_conditions_helper.validate!).to eq(true)
end
end

context 'when jwt has incorrect type attributes' do
context 'when iat is not a integer' do
let(:jwt_iat) { Time.now }

it 'raises an exception' do
expect { jwt_conditions_helper.validate! }.to raise_error(
AppleAuth::Conditions::JWTValidationError
)
end
end
end

context 'when jwt_aud is different to apple_client_id' do
let(:jwt_aud) { 'net.apple_auth' }

it 'raises an exception' do
expect { jwt_conditions_helper.validate! }.to raise_error(
AppleAuth::Conditions::JWTValidationError, 'jwt_aud is different to apple_client_id'
)
end
end

context 'when jwt_iss is different to apple_iss' do
let(:jwt_iss) { 'https://appleid.apple.net' }

it 'raises an exception' do
expect { jwt_conditions_helper.validate! }.to raise_error(
AppleAuth::Conditions::JWTValidationError, 'jwt_iss is different to apple_iss'
)
end
end

context 'when jwt_iat is greater than now' do
let(:jwt_iat) { (Time.now + 5.minutes).to_i }

it 'raises an exception' do
expect { jwt_conditions_helper.validate! }.to raise_error(
AppleAuth::Conditions::JWTValidationError, 'jwt_iat is greater than now'
)
end
end
end
end
Loading

0 comments on commit d71d10d

Please sign in to comment.