omniauth-globalid 0.0.7 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e427c1de19685187e54935aa993e66ebbcd09e5bed685531151038b7cc478efd
4
- data.tar.gz: b34dd08d02ce163537ad58a40ca0257eddc1ff69e4604cc2731623e0854db77f
3
+ metadata.gz: cb02c58ca0c780feb42b7518bf4fe51b297a296dee418dbb13f9b2287f8d0d29
4
+ data.tar.gz: 76eee540664df39fd0d64b596c99b20fd82a0c7aeb1f5865c65f1d3bb30a7c89
5
5
  SHA512:
6
- metadata.gz: 0ae5b3594525c68653dc67c34de627ef7070575967e7e263e9ff67e98935d6ab87c08297ab5a35056476d3bf075ba79844f275822e02fad8074c6fac3ec999a4
7
- data.tar.gz: ecfe81f85c6789b66c424ab2d3b9e87179e9d744c9b92c7c1849eccdc03c4cad15f41ff3cd6226fbe3af929112e721d6f41ce438fa55fd68b7f7f9d5db469ac7
6
+ metadata.gz: ad5836f727bdfe6a9a50971c4e06b98a50af2a788bfaf6204de0f1ab2908e3a2fcfa5c2ba3fc25dbad2ea87c9993bf9adc91c8c27a47ea109e769ca7da1aac63
7
+ data.tar.gz: ab52c6695f2a1a68234e02775a0332053c6e70ec42515735a04273593cc4a99777d98b3639cde18afca769bbde80a9e49a734cefa0f9fa8fff08bb8520126c97
data/.editorconfig ADDED
@@ -0,0 +1,11 @@
1
+ # EditorConfig is awesome: https://EditorConfig.org
2
+
3
+ # top-most EditorConfig file
4
+ root = true
5
+
6
+ [*]
7
+ end_of_line = lf
8
+ indent_size = 2
9
+ indent_style = space
10
+ insert_final_newline = true
11
+ trim_trailing_whitespace = true
data/.gitignore CHANGED
@@ -4,4 +4,9 @@ Gemfile.lock
4
4
  pkg/*
5
5
  coverage
6
6
 
7
- info.markdown
7
+ info.markdown
8
+
9
+ .env
10
+
11
+ # Ignore ruby versioning stuff
12
+ .ruby-*
data/.rubocop.yml ADDED
@@ -0,0 +1,28 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.5
3
+
4
+ Layout:
5
+ Enabled: false
6
+
7
+ Metrics/LineLength:
8
+ Enabled: false
9
+
10
+ Metrics/MethodLength:
11
+ Enabled: false
12
+
13
+ Metrics/ClassLength:
14
+ Enabled: false
15
+
16
+ Metrics/ModuleLength:
17
+ Exclude:
18
+ - "**/*_spec.rb"
19
+
20
+ Metrics/BlockLength:
21
+ Exclude:
22
+ - "**/*_spec.rb"
23
+
24
+ Metrics/ParameterLists:
25
+ CountKeywordArgs: false
26
+
27
+ Style:
28
+ Enabled: false
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source "https://rubygems.org"
2
+
3
+ group :test do
4
+ gem "rack-test"
5
+ gem "webmock"
6
+ gem "rspec", "~> 3.5.0"
7
+ gem "rubocop", "~> 0.67", require: false
8
+ gem "guard" # Autorunning tests
9
+ gem "guard-rspec", require: false
10
+ gem "guard-rubocop", require: false
11
+ gem "dotenv" # So that we can load the env variables for the specs
12
+ end
13
+
14
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,7 @@
1
+ group :red_green_refactor, halt_on_fail: true do
2
+ guard :rspec, cmd: "rspec" do
3
+ watch(%r|^spec/(.*)\/(.*)_spec\.rb|)
4
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
5
+ watch(%r|^spec/spec_helper\.rb|) { "spec" }
6
+ end
7
+ end
data/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2019, Seth Herr
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
data/README.md CHANGED
@@ -1,5 +1,387 @@
1
- # OmniAuth GlobaliD
1
+ # OmniAuth globaliD
2
2
 
3
- This gem contains the GlobaliD strategy for OmniAuth.
3
+ This gem contains the GlobaliD strategy for OmniAuth, and includes functionality for accessing PII that authenticated users share with you.
4
4
 
5
- This is almost certainly broken.
5
+ ## Installation
6
+
7
+ Install this gem by adding it to your gemfile,
8
+
9
+ ```ruby
10
+ gem "omniauth-globalid"
11
+ ```
12
+
13
+ Then `bundle install`
14
+
15
+ `Omniauth::Strategies::Globalid` is a rack middleware for authenticating with globaliD. It supports OAuth2 authentication and openID Connect.
16
+
17
+ If you're adding this to a Rails app using [devise](https://github.com/plataformatec/devise) for authentication, add this to your `config/initializers/devise.rb`:
18
+
19
+ ```ruby
20
+ config.omniauth :globalid, ENV["GLOBALID_CLIENT_ID"], ENV["GLOBALID_CLIENT_SECRET"],
21
+ ```
22
+
23
+ Here's an example not using devise, adding this to the middleware to a Rails app in `config/initializers/omniauth.rb`:
24
+
25
+ ```ruby
26
+ Rails.application.config.middleware.use OmniAuth::Builder do
27
+ provider :globalid, ENV["GLOBALID_CLIENT_ID"], ENV["GLOBALID_CLIENT_SECRET"]
28
+ end
29
+ ```
30
+
31
+ Options you can pass in the initialization (none are required):
32
+
33
+ | Parameter | Description |
34
+ | --------- | ----------- |
35
+ | `acrc_id` | _Verification Requirements_, e.g. a requirement that the user has a valid government id |
36
+ | `scope` | Must be `openid` if passing an `acrc_id` that specifies [PII sharing](#access-pii-from-the-vault) |
37
+ | `private_key` | Private key given to globaliD. Required for [PII sharing](#access-pii-from-the-vault) |
38
+ | `private_key_pass` | Password for `private_key` specified |
39
+
40
+ Here is what a configuration for a setup that uses PII sharing looks like:
41
+
42
+ ```ruby
43
+ provider :globalid,
44
+ ENV["GLOBALID_CLIENT_ID"],
45
+ ENV["GLOBALID_CLIENT_SECRET"],
46
+ acrc_id: ENV["ACRC_ID"],
47
+ scope: "openid",
48
+ private_key: ENV["GLOBALID_PRIVATE_KEY"],
49
+ private_key_pass: ENV["GLOBALID_PRIVATE_KEY_PASS"]
50
+ ```
51
+
52
+ If you're curious about what those options mean, or how to use them, read [globaliD's documentation](https://developer.global.id/external/documentation/index.html) or the [walkthroughs in this readme](#globalid-authentication-walkthroughs).
53
+
54
+
55
+ ## Local development
56
+
57
+ Run the tests with `rake`
58
+
59
+ Use `bundle exec guard` to watch the files for changes and rerun tests
60
+
61
+ ## globaliD Authentication Walkthroughs
62
+
63
+ You can just install this gem which manages all this for you 🙂 - but if you want to understand how all this works, there are walkthroughs for these topics:
64
+
65
+ - [Setup](#setup) (required for all the walkthroughs)
66
+ - [OAuth2 Authorization Code Request Flow](#oauth2-authorization-code-request-flow)
67
+ - [OpenID Connect Flow](#openid-connect-flow)
68
+ - [Refresh access tokens Flow](#refresh-access-tokens-flow)
69
+ - [Access Personally Identifiable Information from the Vault](#access-pii-from-the-vault)
70
+
71
+ ### Setup
72
+
73
+ To be able to run these walkthroughs you first have to install the necessary gems:
74
+
75
+ ```bash
76
+ gem install "dotenv"
77
+ gem install "jwt"
78
+ gem install "faraday"
79
+ ```
80
+
81
+ And set up a `.env` file that has values for these keys: `GLOBALID_CLIENT_ID`, `GLOBALID_CLIENT_SECRET`, `ACRC_ID`, `REDIRECT_URL`
82
+
83
+ If you're going to execute the walkthrough commands in [IRB](https://en.wikipedia.org/wiki/Interactive_Ruby_Shell), enter irb and run these commands:
84
+
85
+ ```ruby
86
+ require "dotenv/load" # Load the .env file
87
+ require "jwt" # Load the gem for decoding JWTs
88
+ require "faraday" # http request library
89
+ token_url = "https://api.globalid.net/v1/auth/token" # token_url is used multiple times, so store in a variable
90
+ ```
91
+
92
+ ---
93
+
94
+ ### OAuth2 _Authorization Code_ Request Flow
95
+
96
+ **The server side authorization flow for "Sign in with GlobaliD"**
97
+
98
+ #### 1. Create a URL that renders the globaliD sign in page
99
+
100
+ This URL includes these parameters
101
+
102
+ | Parameter | Required? | Description |
103
+ | --------- | :-------: | ----------- |
104
+ | `client_id` | ✔ | Defines which app is making this authentication request. Setup in [globaliD's developer panel](https://developer.global.id) |
105
+ | `scope` | ✔ | Defines what you are asking permission to do for the user. In the basic setup, this is `public` |
106
+ | `redirect_uri` | ✔ | Where to send user after authentication, must match the app defined by the `client_id` |
107
+ | `response_type` | ✔ | Needs to be `code`, because we're doing the _authorization code_ flow |
108
+ | `state` | ✔ | Security parameter to [prevent request forgery](https://stackoverflow.com/questions/26132066/what-is-the-purpose-of-the-state-parameter-in-oauth-authorization-request) |
109
+ | `acrc_id` | | _Verification Requirements_, e.g. a requirement that the user has a valid government id |
110
+
111
+ This will create the URL in IRB:
112
+
113
+ ```ruby
114
+ # These are the parameters for the globaliD authorization URL:
115
+ authorization_params = { client_id: ENV["GLOBALID_CLIENT_ID"], acrc_id: ENV["ACRC_ID"], redirect_uri: ENV["REDIRECT_URL"], grant_type: "authorization_code", nonce: "something-random", response_type: "code", scope: "public" }
116
+ # Which you use to compose the globaliD authorization URL -
117
+ authorization_url = "https://auth.global.id?" + URI.encode_www_form(authorization_params)
118
+ ```
119
+
120
+ #### 2. Send user to the generated globaliD url
121
+
122
+ After the user authenticates, they are redirected to the `redirect_url` with these parameters:
123
+
124
+ | Parameter | Description |
125
+ | --------- | ----------- |
126
+ | `grant_type` | The type of OAuth flow - we're doing the `authorization_code` |
127
+ | `code` | The authorization code, which we will use to get an access token |
128
+ | `state` | Security parameter to [prevent request forgery](https://stackoverflow.com/questions/26132066/what-is-the-purpose-of-the-state-parameter-in-oauth-authorization-request) |
129
+
130
+ ... If you're following along in IRB, you can open the authorization url in your browser with:
131
+
132
+ ```ruby
133
+ system "open '#{authorization_url}'"
134
+ ```
135
+
136
+ #### 3. Receive the request that comes to the `redirect_url`
137
+
138
+ The URL will look like:
139
+
140
+ ```
141
+ https://global.id/fake_redirect_auth/?grant_type=authorization_code&code=7b35a90b8f904aae9db676660e33784e
142
+ ```
143
+
144
+ The `code` parameter's value from the above URL is `7b35a90b8f904aae9db676660e33784e`. Assign this to `code` in IRB:
145
+
146
+ ```ruby
147
+ code = "7b35a90b8f904aae9db676660e33784e" # use YOUR code, not the sample ;)
148
+ ```
149
+
150
+ _If you have a functioning Omniauth installation this will happen in your controller_
151
+
152
+
153
+ #### 4. Make a request to globaliD's token URL to get the `access_token`
154
+
155
+
156
+ ```ruby
157
+ authorization_code_token_params = { client_id: ENV["GLOBALID_CLIENT_ID"], client_secret: ENV["GLOBALID_CLIENT_SECRET"], grant_type: "authorization_code", code: code, redirect_uri: ENV["REDIRECT_URL"] }
158
+ token_response = Faraday.new(url: token_url).post do |req|
159
+ req.headers["Content-Type"] = "application/x-www-form-urlencoded"
160
+ req.body = URI.encode_www_form(authorization_code_token_params)
161
+ end
162
+ ```
163
+
164
+ #### 5. Parse the JSON from _token_response_ above
165
+
166
+ The `token_response.body` for the above request will be JSON that looks something like this:
167
+
168
+ ```js
169
+ {
170
+ access_token: "eyJhb....", // This will be a big string, truncated here for legibility
171
+ token_type: "bearer",
172
+ expires_in: 7200,
173
+ refresh_token: "4806a8863c62a6509046638d80c37d16",
174
+ id_token: null
175
+ }
176
+ ```
177
+
178
+ You'll need to store the `access_token`, `refresh_token` and `scope` to be able to make authenticated requests to globaliD's API.
179
+
180
+ Storing the expiration (current time + `expires_in` seconds) is a good idea - it's when you'll need to use the refresh token to get a new access_token.
181
+
182
+ With this `access_token` you can make authenticated requests to globaliD's APIs
183
+
184
+ #### Notes
185
+
186
+ - [Check out an interactive demonstration of OAuth2 authorization code requests](https://www.oauth.com/playground/authorization-code.html)
187
+ - _Libraries help make this much easier_ for example, the [OAuth2 Gem](https://github.com/oauth-xx/oauth2)
188
+
189
+ ---
190
+
191
+ ### OpenID Connect Flow
192
+
193
+ _OpenID Connect is just an expansion of the OAuth2 request flow ([documented above](#oauth2-authorization-code-request-flow))_
194
+
195
+ The differences are:
196
+
197
+ - You _must_ use the scope `openid` and include an acrc_id parameter when creating the URL for the user to authenticate ([step 1](#1-create-a-url-that-renders-the-globalid-sign-in-page))
198
+ - The _access token response_ is different and requires additional parsing ([step 5](#5-parse-the-json-from-access-token-response-above))
199
+
200
+ So, for the OpenID Connect flow follow steps 1 through 4 for the OAuth2 _Authorization Code_ request flow (making sure to use the `openid` scope and include a acrc_id), and replace step 5 with this:
201
+
202
+ #### 5. Parse the JSON from the access token response and decode the JSON Web Token
203
+
204
+ The `token_response` from the OpenID Connect response will look something like this:
205
+
206
+ ```js
207
+ {
208
+ access_token: "eyJhbGciOiJSUzI1", // truncated for legibility
209
+ token_type: "bearer",
210
+ expires_in: 7200,
211
+ refresh_token: "0ec3c111cb18f4a48db2d8246b8cb5eb",
212
+ id_token: "eyJhbGciOiJSUzI1NiIsIn" // starts same as access token but is longer, truncated for legibility
213
+ }
214
+ ```
215
+
216
+ The `id_token` parameter from this response is a [JSON Web Token](https://jwt.io/introduction/) (JWT).
217
+
218
+ We only need the `id_token` value from this response, which we get and decode like this:
219
+
220
+ ```ruby
221
+ id_token = JSON.parse(token_response.body)["id_token"]
222
+ # NOTE: The JWT signature from globaliD's API is broken right now because of an issue with the location of the public key
223
+ # So for now, we skip verifying the signature, and parse the JWT with this:
224
+ decoded_token = JWT.decode(id_token, nil, false).first
225
+ ```
226
+
227
+ The decoded JWT looks like this:
228
+
229
+ ```js
230
+ {
231
+ "sub": "ef141f5d-2a9f-429d-999f-8bbec78a733a", // the globaliD UUID for the user who authenticated
232
+ "iss": "https://globalid.net",
233
+ "nonce": "af78x76zv87xv78v",
234
+ "iat": "1570659177025",
235
+ "exp": "1570745577026",
236
+ "idp.globalid.net/claims/null": {},
237
+ "idp.globalid.net/claims/dd24263d-079b-4779-9776-167fe6e03ab8": {
238
+ "bf4cd542-216f-4377-bc46-7601eca09048": [
239
+ "WQexnTFKt1E...." // Base64 encoded, encrypted claims token
240
+ ]
241
+ }
242
+ }
243
+ ```
244
+
245
+ You can use the access_token from this response to make authenticated requests to globaliD's APIs.
246
+
247
+ The claims key-value pairs from this JWT include the data necessary to [access PII from the Vault](#access-pii-from-the-vault).
248
+
249
+ #### Notes
250
+
251
+ - Using the visual decoder and debugger at [jwt.io](https://jwt.io) can be helpful for understanding how globaliD's OpenID Connect response works (and understanding JWTs in general)
252
+
253
+
254
+ ---
255
+
256
+ ### Refresh access tokens Flow
257
+
258
+ **This is how to refresh an `access_token` once its expiration passes**
259
+
260
+ _You can determine if you need to refresh the access token if the expiration time has passed, or if you get a 401 status response with a JSON body that includes the key-value pair: `"message": "The bearer token has expired"`._
261
+
262
+ #### 1. Make a POST request to the token url with the `refresh_token`, `client_id` and `client_secret`
263
+
264
+ ```ruby
265
+ refresh_token = "70165f183e7efee2b298302bcaea5276" # Replace with the actual code you have
266
+ refresh_params = { client_id: ENV["GLOBALID_CLIENT_ID"], redirect_uri: ENV["REDIRECT_URL"], grant_type: "refresh_token", refresh_token: refresh_token }
267
+ response = Faraday.new(url: token_url).post do |req|
268
+ req.headers["Content-Type"] = "application/x-www-form-urlencoded"
269
+ req.body = URI.encode_www_form(refresh_params)
270
+ end
271
+ ```
272
+
273
+ #### 2.Parse the JSON in the `response.body`, the access token is the `access_token` value
274
+
275
+ You can use this access token until it expires - at which point you'll need to request a new access token, using the refresh token you were given in the original request (just follow the refresh access token flow again).
276
+
277
+ ---
278
+
279
+ ### Access PII from the Vault
280
+
281
+ To be able to make an ACRC request with PII You also _must_ have given globaliD an encryption key.
282
+
283
+ The key must be in the `.pem` format, (you'll be required to enter a password) - generate it with:
284
+
285
+ ```shell
286
+ openssl genrsa -des3 -out key_for_globaliD.key 4096
287
+ openssl rsa -in "key_for_globaliD.key" -pubout > "public_key_for_globaliD.pub"
288
+ ```
289
+
290
+ Once you have those files:
291
+
292
+ - Send the public key (`public_key_for_globaliD.pub` generated from the above snippet) to [devsupport@global.id](mailto:devsupport@global.id)
293
+ - Add the private key (`key_for_globaliD.key`) to your `.env` file under as `GLOBALID_PRIVATE_KEY` (wrap the key in quotes and replace the new lines with `\n`)
294
+ - Add the password to the `.env` file as `GLOBALID_PRIVATE_KEY_PASS`
295
+
296
+ To be able to access a user's PII, you'll have to follow the [OpenID Connect Flow above](#openid-connect-flow) - you _must_ have included an `acrc_id` that authorizes PII sharing when following that flow.
297
+
298
+ Then, follow these steps (all the code is at the end)
299
+
300
+ #### 6. Make _another_ token request to get an `access_token`,
301
+
302
+ Make this token request using the `client_credentials` grant. This doesn't require a user's access token, so you can do it at any point you want.
303
+
304
+ #### 7. Extract the `claims` from the JWT
305
+
306
+ The JWT key that is `idp.globalid.net/claims/#{acrc_id}` holds the claims values
307
+
308
+ #### 8. Decode and decrypt the claims
309
+
310
+ Base64 decode and decrypt the claims (with the private key and private key pass sent to globaliD) each of the `claims` - to get the `private_data_tokens`
311
+
312
+ #### 9. Get the _vault responses_
313
+
314
+ Make a request to the globaliD vault with the `private_data_tokens` in the body of the request, and parse the response JSON
315
+
316
+ #### 10. Decode and decrypt the vault responses
317
+
318
+ Base64 decode and decrypt (with the private key and private key pass sent to globaliD) each of the _vault responses_ `encrypted_data_password` - to get the `decrypted_data_password`
319
+
320
+ #### 11. Split the Vault data into the Initialization Vector and the Encrypted data
321
+
322
+ The first 32 bytes are the Initialization Vector (iv). The following bytes is the actual encrypted data that you want to decrypt.
323
+
324
+ #### 12. Decrypt the AES encrypted data
325
+
326
+ The `decrypted_data_password` and the `iv` need to be Hex encoded and applied to an _AES 256 CBC_ Cipher to decrypt the PII.
327
+
328
+ #### This is code you can paste into IRB
329
+
330
+ _This covers steps 6 through 12, provided you followed the OpenID Connect flow in IRB_
331
+
332
+ ```ruby
333
+ # Load the private key with Ruby's encryption library
334
+ private_key = OpenSSL::PKey::RSA.new(ENV["GLOBALID_PRIVATE_KEY"], ENV["GLOBALID_PRIVATE_KEY_PASS"])
335
+
336
+ # We only care about the *encrypted_claims_token* part of the decoded_token - but keys are dynamically named
337
+ # The decoded_token has a key-value pair with a key that is `idp.globalid.net/claims/#{acrc_id}`
338
+ # The value from this key value pair will itselv be key-value pairs, with values of the encrypted_data_tokens for the vault
339
+ # Parsing this is a pain, we find matching keys, grab values for those keys, and then grab the values of those values :/
340
+ claims_key_values = decoded_token.select { |k, v| k.match?(ENV["ACRC_ID"]) }
341
+ encrypted_data_tokens = claims_key_values.values.map(&:values).flatten
342
+ # Get the tokens to make requests to the vault, which is how you access the PII, by decrypting the encrypted_data_tokens
343
+ decrypted_tokens = encrypted_data_tokens.map do |claim_token|
344
+ # The claim_tokens are base64 encoded
345
+ private_key.private_decrypt(Base64.decode64(claim_token), OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
346
+ end
347
+ # At this point you need to get an access token to make requests to the vault.
348
+ # Get this access token by making a request to the token_url using the `client_credentials` grant_type
349
+ # NOTE: the client_credentials grant_type doesn't require a user token
350
+ client_credentials_token_params = { client_id: ENV["GLOBALID_CLIENT_ID"], client_secret: ENV["GLOBALID_CLIENT_SECRET"], grant_type: "client_credentials", redirect_uri: ENV["REDIRECT_URL"] }
351
+ client_credentials_response = openid_response = Faraday.new(url: token_url).post do |req|
352
+ req.headers["Content-Type"] = "application/x-www-form-urlencoded"
353
+ req.body = URI.encode_www_form(client_credentials_token_params)
354
+ end
355
+ # client_credentials_response body is JSON that looks like this: { access_token: "...", token_type: "bearer", expires_in: 7200, refresh_token: nil }
356
+ access_token = JSON.parse(client_credentials_response.body)["access_token"] # this is the only part of the client_credentials_response we use
357
+
358
+ # We now have all the data we need to be able to make requests to the vault!
359
+ # Make a request to the vault, using the access token, with the decrypted tokens:
360
+ vault_response = Faraday.new(url: "https://api.global.id/v1/vault/get-encrypted-data").post do |req|
361
+ req.headers["Authorization"] = "Bearer #{access_token}"
362
+ req.headers["Content-Type"] = "application/json"
363
+ req.body = { private_data_tokens: decrypted_tokens }.to_json
364
+ end
365
+
366
+ # The vault_response body potentially has multiple responses, so we need to decrypt each of them:
367
+ pii_key_values = JSON.parse(vault_response.body).map do |vault_data|
368
+ # Decrypt the password for the vault data
369
+ decrypted_data_password = private_key.private_decrypt(Base64.decode64(vault_data["encrypted_data_password"]), OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
370
+ # The Initialization Vector is the first 32 bytes of the encrypted data
371
+ iv = vault_data["encrypted_data"][0, 32]
372
+ # The actual encrypted data is everything after the first 32 bytes
373
+ encrypted_data = vault_data["encrypted_data"][32, vault_data["encrypted_data"].length]
374
+ # Create a cipher that can decrypt the data that was encrypted in the vault
375
+ cipher = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
376
+ cipher.decrypt # Tell the cipher instance that we are going to decrypt with it
377
+ # The password and the IV are hex encoded
378
+ cipher.key = Array(decrypted_data_password).pack("H*") # Encode the password in hex (base16)
379
+ cipher.iv = Array(iv).pack("H*") # the initialization vector (iv) is first 32 chars of the encoded_data, hex encoded
380
+ # Decode the base64 encoded data, and decrypt it!
381
+ decrypted_pii = cipher.update(Base64.decode64(encrypted_data)) + cipher.final
382
+ JSON.parse(decrypted_pii)
383
+ end
384
+
385
+ # And there you have it! Simple ;)
386
+ pp pii_key_values
387
+ ```
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ desc "Run specs"
5
+ RSpec::Core::RakeTask.new
6
+
7
+ desc "Default: run specs."
8
+ task default: :spec
@@ -1,2 +1,3 @@
1
- require "omniauth-globalid/version"
2
- require 'omniauth/strategies/globalid'
1
+ require "omniauth/globalid/version"
2
+ require "omniauth/globalid/vault"
3
+ require "omniauth/strategies/globalid"
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAuth
4
+ module Globalid
5
+ class Vault
6
+ def initialize(openid_token: nil, token_url: nil, client_id: nil, client_secret: nil, redirect_uri: nil)
7
+ @openid_token = openid_token
8
+ # TODO: Figure out a cleaner way to implement this!
9
+ @token_url = token_url || "https://api.globalid.net/v1/auth/token"
10
+ @client_id = client_id || ENV["GLOBALID_CLIENT_ID"]
11
+ @client_secret = client_secret || ENV["GLOBALID_CLIENT_SECRET"]
12
+ @redirect_uri = redirect_uri || ENV["GLOBALID_REDIRECT_URL"]
13
+ if ENV["GLOBALID_PRIVATE_KEY"]&.length
14
+ @private_key = OpenSSL::PKey::RSA.new(ENV["GLOBALID_PRIVATE_KEY"], ENV["GLOBALID_PRIVATE_KEY_PASS"])
15
+ end
16
+ end
17
+
18
+ attr_accessor :openid_token, :private_key
19
+
20
+ def decrypted_pii
21
+ vault_response.map do |vault_data|
22
+ # Decrypt the password for the vault data
23
+ decrypted_data_password = private_key.private_decrypt(Base64.decode64(vault_data["encrypted_data_password"]), OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
24
+ # The Initialization Vector is the first 32 bytes of the encrypted data
25
+ iv = vault_data["encrypted_data"][0, 32]
26
+ # The actual encrypted data is everything after the first 32 bytes
27
+ encrypted_data = vault_data["encrypted_data"][32, vault_data["encrypted_data"].length]
28
+ # Create a cipher that can decrypt the data that was encrypted in the vault
29
+ cipher = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
30
+ cipher.decrypt # Tell the cipher instance that we are going to decrypt with it
31
+ # The password and the IV are hex encoded
32
+ cipher.key = Array(decrypted_data_password).pack("H*") # Encode the password in hex (base16)
33
+ cipher.iv = Array(iv).pack("H*") # the initialization vector (iv) is first 32 chars of the encoded_data, hex encoded
34
+ # Decode the base64 encoded data, and decrypt it!
35
+ decrypted_pii = cipher.update(Base64.decode64(encrypted_data)) + cipher.final
36
+ JSON.parse(decrypted_pii)
37
+ end
38
+ end
39
+
40
+ def encrypted_data_tokens
41
+ # Parsing this is a paid because the keys are dynamic
42
+ # And we need the inside of the nested structure :(
43
+ @openid_token.select { |k, v| k.match?("/claims/") }
44
+ .reject { |k, v| k.match?("/claims/null") }
45
+ .values.map(&:values).flatten
46
+ end
47
+
48
+ def decrypted_tokens
49
+ # Get the tokens to make requests to the vault, which is how you access the PII, by decrypting the encrypted_data_tokens
50
+ encrypted_data_tokens.map do |claim_token|
51
+ # The claim_tokens are base64 encoded
52
+ private_key.private_decrypt(Base64.decode64(claim_token), OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
53
+ end
54
+ end
55
+
56
+ def client_credentials_access_token
57
+ # TODO: figure out how to configure these without having to specify via environmental variables
58
+ client_credentials_token_params = {
59
+ client_id: @client_id,
60
+ client_secret: @client_secret,
61
+ redirect_uri: @redirect_uri,
62
+ grant_type: "client_credentials",
63
+ }
64
+ client_credentials_response = Faraday.new(url: @token_url).post do |req|
65
+ req.headers["Content-Type"] = "application/x-www-form-urlencoded"
66
+ req.body = URI.encode_www_form(client_credentials_token_params)
67
+ end
68
+ JSON.parse(client_credentials_response.body)["access_token"] # this is the only part of the client_credentials_response we use
69
+ end
70
+
71
+ def vault_response
72
+ result = Faraday.new(url: "https://api.global.id/v1/vault/get-encrypted-data").post do |req|
73
+ req.headers["Authorization"] = "Bearer #{client_credentials_access_token}"
74
+ req.headers["Content-Type"] = "application/json"
75
+ req.body = { private_data_tokens: decrypted_tokens }.to_json
76
+ end
77
+ JSON.parse(result.body)
78
+ end
79
+ end
80
+ end
81
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OmniAuth
4
4
  module Globalid
5
- VERSION = "0.0.7"
5
+ VERSION = "0.1.0"
6
6
  end
7
7
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "omniauth-oauth2"
4
+ require "jwt"
4
5
 
5
6
  module OmniAuth
6
7
  module Strategies
@@ -12,15 +13,29 @@ module OmniAuth
12
13
  authorize_url: "/",
13
14
  token_url: "https://api.globalid.net/v1/auth/token"
14
15
 
16
+ def self.parse_jwt(id_token)
17
+ JWT.decode(id_token, nil, false).first
18
+ end
19
+
15
20
  # https://github.com/omniauth/omniauth-oauth2/issues/81
16
21
  def callback_url
17
22
  full_host + script_name + callback_path
18
23
  end
19
24
 
20
25
  def authorize_params
21
- return super unless acrc_id_provided?
22
-
23
- super.merge(acrc_id: request.params["acrc_id"])
26
+ auth_params = super # Get the OAuth2 omniauth params
27
+ # Add the acrc_id if configured
28
+ auth_params[:acrc_id] = options[:acrc_id] if options[:acrc_id]
29
+ # If we are getting pii sharing, we need to have the openid scope
30
+ if pii_sharing?
31
+ auth_params[:scope] = "openid"
32
+ end
33
+ # If we are in the openid scope, we need a nonce
34
+ if options[:scope]&.match?("openid")
35
+ auth_params[:nonce] ||= SecureRandom.hex(24)
36
+ end
37
+ return auth_params unless acrc_id_in_request?
38
+ auth_params.merge(acrc_id: request.params["acrc_id"] || request.params[:acrc_id])
24
39
  end
25
40
 
26
41
  uid { raw_info["gid_uuid"] }
@@ -32,7 +47,8 @@ module OmniAuth
32
47
  description: raw_info["description"],
33
48
  image: raw_info["display_image_url"],
34
49
  location: location(raw_info),
35
- }
50
+ }.merge(id_token: openid_token)
51
+ .merge(decrypted_pii: decrypted_pii)
36
52
  end
37
53
 
38
54
  def raw_info
@@ -42,6 +58,22 @@ module OmniAuth
42
58
  @raw_info = JSON.parse(result.body)
43
59
  end
44
60
 
61
+ def openid_token
62
+ return @openid_token if defined?(@openid_token)
63
+ id_token = access_token["id_token"]
64
+ if !id_token
65
+ @openid_token = {}
66
+ else
67
+ @openid_token = self.class.parse_jwt(id_token)
68
+ end
69
+ @openid_token
70
+ end
71
+
72
+ def decrypted_pii
73
+ return {} unless @openid_token.keys.any? && options[:decrypt_pii_on_login]
74
+ @decrypted_pii ||= vault.decrypted_pii
75
+ end
76
+
45
77
  private
46
78
 
47
79
  def api_connection
@@ -52,8 +84,25 @@ module OmniAuth
52
84
  end
53
85
  end
54
86
 
87
+ def vault
88
+ OmniAuth::Globalid::Vault.new(openid_token: openid_token,
89
+ token_url: options[:token_url],
90
+ client_id: options[:client_id],
91
+ client_secret: options[:client_secret],
92
+ redirect_uri: options[:redirect_uri])
93
+ end
94
+
95
+ def acrc_id_in_request?
96
+ request.params.key?("acrc_id") || request.params[:acrc_id]
97
+ end
98
+
55
99
  def acrc_id_provided?
56
- request.params.key?("acrc_id")
100
+ options[:acrc_id] || acrc_id_in_request?
101
+ end
102
+
103
+ def pii_sharing?
104
+ # TODO: make this actually check if we need PII sharing. For now, just assuming
105
+ acrc_id_provided? && options.key?("private_key")
57
106
  end
58
107
 
59
108
  def location(raw_info)
@@ -1,24 +1,25 @@
1
- # -*- encoding: utf-8 -*-
1
+ # coding: utf-8
2
2
  $:.push File.expand_path("../lib", __FILE__)
3
- require "omniauth-globalid/version"
3
+ require "omniauth/globalid/version"
4
4
 
5
5
  Gem::Specification.new do |s|
6
6
  s.name = "omniauth-globalid"
7
7
  s.version = OmniAuth::Globalid::VERSION
8
8
  s.authors = ["Seth Herr"]
9
- s.homepage = "https://gitlab.com/sethherr/omniauth-globalid"
9
+ s.homepage = "https://gitlab.com/globalid/opensource/omniauth-globalid"
10
10
  s.description = %q{OmniAuth strategy for GlobaliD}
11
11
  s.summary = s.description
12
- s.license = "MIT"
12
+ s.license = "ISC"
13
13
 
14
- s.files = `git ls-files`.split("\n")
15
- # s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") # Hopefully someday
14
+ s.files = `git ls-files`.split("\n").reject { |f| f.start_with?("spec/") }
16
15
  s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
17
16
  s.require_paths = ["lib"]
18
- s.required_ruby_version = Gem::Requirement.new(">= 1.9.3")
17
+ s.required_ruby_version = Gem::Requirement.new(">= 2.2")
19
18
 
20
19
  s.add_runtime_dependency "omniauth", "~> 1.2"
21
20
  s.add_runtime_dependency "omniauth-oauth2", "~> 1.1"
21
+ s.add_runtime_dependency "jwt", "~> 2.2.1"
22
22
  s.add_dependency "rack"
23
- s.add_development_dependency "bundler", "~> 1.0"
23
+ s.add_development_dependency "bundler", "~> 1.14"
24
+ s.add_development_dependency "rake", "~> 12.0"
24
25
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omniauth-globalid
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.7
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Seth Herr
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-09-30 00:00:00.000000000 Z
11
+ date: 2019-11-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: omniauth
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: jwt
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 2.2.1
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 2.2.1
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: rack
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -58,29 +72,50 @@ dependencies:
58
72
  requirements:
59
73
  - - "~>"
60
74
  - !ruby/object:Gem::Version
61
- version: '1.0'
75
+ version: '1.14'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.14'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '12.0'
62
90
  type: :development
63
91
  prerelease: false
64
92
  version_requirements: !ruby/object:Gem::Requirement
65
93
  requirements:
66
94
  - - "~>"
67
95
  - !ruby/object:Gem::Version
68
- version: '1.0'
96
+ version: '12.0'
69
97
  description: OmniAuth strategy for GlobaliD
70
98
  email:
71
99
  executables: []
72
100
  extensions: []
73
101
  extra_rdoc_files: []
74
102
  files:
103
+ - ".editorconfig"
75
104
  - ".gitignore"
105
+ - ".rubocop.yml"
106
+ - Gemfile
107
+ - Guardfile
108
+ - LICENSE
76
109
  - README.md
110
+ - Rakefile
77
111
  - lib/omniauth-globalid.rb
78
- - lib/omniauth-globalid/version.rb
112
+ - lib/omniauth/globalid/vault.rb
113
+ - lib/omniauth/globalid/version.rb
79
114
  - lib/omniauth/strategies/globalid.rb
80
115
  - omniauth-globalid.gemspec
81
- homepage: https://gitlab.com/sethherr/omniauth-globalid
116
+ homepage: https://gitlab.com/globalid/opensource/omniauth-globalid
82
117
  licenses:
83
- - MIT
118
+ - ISC
84
119
  metadata: {}
85
120
  post_install_message:
86
121
  rdoc_options: []
@@ -90,14 +125,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
90
125
  requirements:
91
126
  - - ">="
92
127
  - !ruby/object:Gem::Version
93
- version: 1.9.3
128
+ version: '2.2'
94
129
  required_rubygems_version: !ruby/object:Gem::Requirement
95
130
  requirements:
96
131
  - - ">="
97
132
  - !ruby/object:Gem::Version
98
133
  version: '0'
99
134
  requirements: []
100
- rubygems_version: 3.0.3
135
+ rubyforge_project:
136
+ rubygems_version: 2.7.9
101
137
  signing_key:
102
138
  specification_version: 4
103
139
  summary: OmniAuth strategy for GlobaliD