apple_sign_in 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 612f3cbaf5a755093433a7ca8c3814c87de9100074a217be935ac37655bee813
4
+ data.tar.gz: 13bcf7886feddc8ddbbb479e0f1112dd00744c8300dc5256f08d34b29a2accc2
5
+ SHA512:
6
+ metadata.gz: 50a886815d10532a2a9d68cf9d843f36072137982369f44b048506144817c6dea46c6b3a3572c40021f07d699103fc7a6f64a5a290067a30a8a3744a8c93fa66
7
+ data.tar.gz: 035db6c66a46299f654c2cedaa8b5827951778a5f93139eb0dc97d21e429e6204e463ab4dccbdb678f326662028e88323818137fc93cd4f6829510b9148346a6
@@ -0,0 +1,13 @@
1
+ module AppleSignIn
2
+ require "dry/types"
3
+ require "dry/auto_inject"
4
+
5
+ require 'apple_sign_in/config/container'
6
+ require 'apple_sign_in/config/auto_inject'
7
+
8
+ require 'apple_sign_in/api_caller'
9
+ require 'apple_sign_in/client_secret_generator'
10
+ require 'apple_sign_in/identity_token_verifier'
11
+ require 'apple_sign_in/refresh_token_retriever'
12
+ require 'apple_sign_in/error'
13
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppleSignIn
4
+ class ApiCaller
5
+ include AutoInject[
6
+ :httparty,
7
+ :apple_base_url
8
+ ]
9
+
10
+ def post(url, body)
11
+ httparty.post("#{apple_base_url}#{url}", body: body)
12
+ end
13
+
14
+ def get(url, body = {})
15
+ httparty.get("#{apple_base_url}#{url}", body: body)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppleSignIn
4
+ class ClientSecretGenerator
5
+ include AutoInject[
6
+ :apple_team_id,
7
+ :apple_base_url,
8
+ :apple_key_id,
9
+ :apple_private_key
10
+ ]
11
+
12
+ def generate(identity_token)
13
+ apple_client_id = extract_apple_client_id(identity_token)
14
+ claims = create_claims(apple_client_id)
15
+ jwt(claims)
16
+ end
17
+
18
+ private
19
+
20
+ def jwt(claims)
21
+ jwt = JSON::JWT.new(claims)
22
+ jwt.kid = apple_key_id
23
+ jws = jwt.sign(private_key, :ES256)
24
+ jws.to_s
25
+ end
26
+
27
+ def create_claims(apple_client_id)
28
+ {
29
+ "iss" => apple_team_id,
30
+ "iat" => Time.now.to_i,
31
+ "exp" => 5.months.from_now.to_i,
32
+ "aud" => apple_base_url,
33
+ "sub" => apple_client_id
34
+ }
35
+ end
36
+
37
+ def private_key
38
+ OpenSSL::PKey::EC.new apple_private_key
39
+ end
40
+
41
+ def headers
42
+ {
43
+ "kid" => apple_key_id
44
+ }
45
+ end
46
+
47
+ def extract_apple_client_id(identity_token)
48
+ token_payload = JSON::JWT.decode(identity_token, :skip_verification)
49
+ token_payload["aud"]
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,3 @@
1
+ module AppleSignIn
2
+ AutoInject = Dry::AutoInject(Container)
3
+ end
@@ -0,0 +1,25 @@
1
+ module AppleSignIn
2
+ Container = Dry::Container.new
3
+
4
+ APPLE_BASE_URL = "https://appleid.apple.com".freeze
5
+
6
+ PROVIDER_NAMES = %w(apple).freeze
7
+
8
+ # Constants
9
+ Container.register(:provider_names, -> { PROVIDER_NAMES })
10
+ Container.register(:apple_base_url, -> { APPLE_BASE_URL })
11
+ Container.register(:apple_team_id, -> { ENV["APPLE_TEAM_ID"] })
12
+ Container.register(:apple_key_id, -> { ENV["APPLE_KEY_ID"] })
13
+ Container.register(:apple_private_key, -> { ENV("APPLE_PRIVATE_KEY") })
14
+ Container.register(:apple_redirect_uri, -> { ENV["APPLE_REDIRECT_URI"] })
15
+ Container.register(:apple_client_ids, -> { ENV["APPLE_CLIENT_IDS"].split(",") })
16
+
17
+ # 3rd Party
18
+ Container.register(:httparty, -> { HTTParty })
19
+
20
+ # Actions
21
+ Container.register(:apple_client_secret_generator, -> { AppleSignIn::ClientSecretGenerator.new })
22
+ Container.register(:apple_api_caller, -> { AppleSignIn::ApiCaller.new })
23
+ Container.register(:apple_identity_token_verifier, -> { AppleSignIn::IdentityTokenVerifier.new })
24
+ Container.register(:apple_refresh_token_retriever, -> { AppleSignIn::RefreshTokenRetriever.new })
25
+ end
@@ -0,0 +1,7 @@
1
+ module AppleSignIn
2
+ class Error < StandardError
3
+ class InvalidProviderToken < AppleSignIn::Error; end
4
+ class InvalidCredentials < AppleSignIn::Error; end
5
+ class InvalidRefreshToken < AppleSignIn::Error; end
6
+ end
7
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppleSignIn
4
+ class IdentityTokenVerifier
5
+ include AutoInject[
6
+ :apple_base_url,
7
+ :apple_api_caller,
8
+ :apple_client_ids
9
+ ]
10
+
11
+ def valid?(identity_token)
12
+ decoded_token = JSON::JWT.decode(identity_token, :skip_verification)
13
+
14
+ valid_claims?(decoded_token) &&
15
+ valid_headers?(decoded_token.header) &&
16
+ valid_signature?(identity_token)
17
+ end
18
+
19
+ private
20
+
21
+ def valid_claims?(claims)
22
+ valid_issuer?(claims) &&
23
+ valid_audience?(claims) &&
24
+ valid_time?(claims) &&
25
+ valid_expiry_time?(claims)
26
+ end
27
+
28
+ def valid_issuer?(claims)
29
+ claims["iss"].include?(apple_base_url.to_s)
30
+ end
31
+
32
+ def valid_audience?(claims)
33
+ apple_client_ids.include?(claims["aud"])
34
+ end
35
+
36
+ def valid_time?(claims)
37
+ claims["iat"].between?(30.seconds.ago.to_i, Time.now.to_i)
38
+ end
39
+
40
+ def valid_expiry_time?(claims)
41
+ claims["exp"] > Time.now.to_i
42
+ end
43
+
44
+ def valid_headers?(headers)
45
+ headers["alg"] == "RS256"
46
+ end
47
+
48
+ def valid_signature?(identity_token)
49
+ jwt = JSON::JWT.decode(identity_token, :skip_verification)
50
+ kid = jwt.header["kid"]
51
+ key = select_public_key(kid)
52
+ jwt.verify!(key)
53
+ end
54
+
55
+ def apple_public_keys
56
+ response = apple_api_caller.get("/auth/keys")
57
+ JSON.parse(response.body)["keys"]
58
+ end
59
+
60
+ def select_public_key(kid)
61
+ jwk_set = JSON::JWK::Set.new(apple_public_keys)
62
+ appropriate_key = jwk_set.select { |key| key["kid"] == kid }.first
63
+ appropriate_key.to_key
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppleSignIn
4
+ class RefreshTokenRetriever
5
+ include AutoInject[
6
+ :apple_api_caller,
7
+ :apple_client_secret_generator,
8
+ :apple_redirect_uri
9
+ ]
10
+
11
+ def call(authorization_data)
12
+ body = build_body(authorization_data)
13
+ response = apple_api_caller.post("/auth/token", body)
14
+ parse(response)
15
+ end
16
+
17
+ private
18
+
19
+ def parse(response)
20
+ return JSON.parse(response.body) if response.code == 200
21
+
22
+ handle_error(response)
23
+ end
24
+
25
+ def handle_error(response)
26
+ Rollbar.error(response)
27
+ return raise(AppleSignIn::Error, response.body.to_s) if response.code == 400
28
+
29
+ raise(AppleSignIn::Error, response.code.to_s)
30
+ end
31
+
32
+ def build_body(authorization_data)
33
+ {
34
+ "client_id" => extract_apple_client_id(authorization_data["identity_token"]),
35
+ "client_secret" => secret(authorization_data["identity_token"]),
36
+ "grant_type" => "authorization_code",
37
+ "code" => authorization_data["authorization_code"],
38
+ "redirect_uri" => apple_redirect_uri
39
+ }
40
+ end
41
+
42
+ def extract_apple_client_id(identity_token)
43
+ token_payload = JSON::JWT.decode(identity_token, :skip_verification)
44
+ token_payload["aud"]
45
+ end
46
+
47
+ def secret(identity_token)
48
+ apple_client_secret_generator.generate(identity_token)
49
+ end
50
+ end
51
+ end
metadata ADDED
@@ -0,0 +1,210 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: apple_sign_in
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Gui Heurich
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-06-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: dry-container
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: dry-auto_inject
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: dry-types
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: httparty
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rollbar
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: json-jwt
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
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: vcr
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: rspec
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
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rake
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: timecop
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: webmock
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ description: Validate identity token claims, headers, and signatures.Exchange authorization
168
+ codes for refresh tokens.
169
+ email: guilherme.heurich@protonmail.com
170
+ executables: []
171
+ extensions: []
172
+ extra_rdoc_files: []
173
+ files:
174
+ - lib/apple_sign_in.rb
175
+ - lib/apple_sign_in/api_caller.rb
176
+ - lib/apple_sign_in/client_secret_generator.rb
177
+ - lib/apple_sign_in/config/auto_inject.rb
178
+ - lib/apple_sign_in/config/container.rb
179
+ - lib/apple_sign_in/error.rb
180
+ - lib/apple_sign_in/identity_token_verifier.rb
181
+ - lib/apple_sign_in/refresh_token_retriever.rb
182
+ homepage: https://github.com/GuiHeurich/apple_sign_in
183
+ licenses:
184
+ - MIT
185
+ metadata:
186
+ bug_tracker_uri: https://github.com/GuiHeurich/apple_sign_in/issues
187
+ changelog_uri: https://github.com/GuiHeurich/apple_sign_in/blob/master/CHANGELOG.md
188
+ documentation_uri: https://www.rubydoc.info/gems/apple_sign_in
189
+ homepage_uri: https://github.com/GuiHeurich/apple_sign_in
190
+ source_code_uri: https://github.com/GuiHeurich/apple_sign_in
191
+ post_install_message:
192
+ rdoc_options: []
193
+ require_paths:
194
+ - lib
195
+ required_ruby_version: !ruby/object:Gem::Requirement
196
+ requirements:
197
+ - - ">="
198
+ - !ruby/object:Gem::Version
199
+ version: '0'
200
+ required_rubygems_version: !ruby/object:Gem::Requirement
201
+ requirements:
202
+ - - ">="
203
+ - !ruby/object:Gem::Version
204
+ version: '0'
205
+ requirements: []
206
+ rubygems_version: 3.0.6
207
+ signing_key:
208
+ specification_version: 4
209
+ summary: Backend functionality for Sign in with Apple
210
+ test_files: []