omniauth-globalid 0.0.6 → 0.1.5

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: 83940bd4e20de0706280b37adb6fbf87f1d7ae4b5a72609f6e6799ae5588d309
4
- data.tar.gz: 22717588305d36395cf352e8185a51bde0ecbed1625d64277e39d829ad64566d
3
+ metadata.gz: ee817323908f8a4bda10bb5e71c86bc5e7900d6d63eb71772de5c3c700ce1714
4
+ data.tar.gz: c0318012c3dace7a58f3b607d4c3de8ce1e0ab18abf505031986fc02a82d7968
5
5
  SHA512:
6
- metadata.gz: 962d6041ad635b47b11b9a2fd83c9d4fcf594d9c8ea3122d4bb066bb98b6fd466782c236995f61457e3e54fc7460b5f76eb43fa831856b07896bb4033d05c981
7
- data.tar.gz: e3eee64ba5384822570dfb1995afe387e50e8287d471728d390d6bcfbdf54082d34183d1ca04c6e31b6e5da7ae574bd67332c75aff1c359f41643828f72ff71e
6
+ metadata.gz: 7af4b3659f7b945c60f1816a0514763eda8df43467a7075b4984dae03c017ea55f133346d8e3537a739ac223ac8cda2925d788e85100e720616b7e6efb1366cd
7
+ data.tar.gz: 4d39d5a172625f1d8446a8f9ff2a3646e76da0c0a35a84451e9990ed0fa99e29e212133490c8f92610187437a3f801905acbb67b6075fa3538e03289c6de38b7
@@ -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-*
@@ -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
@@ -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
+ `omniauth-globalid` is a rack middleware for authenticating with globaliD. It supports OAuth2 authentication and openID Connect. 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
+ If you're adding this to a Rails app using [devise with OmniAuth](https://github.com/plataformatec/devise/wiki/OmniAuth:-Overview), add this to your `config/initializers/devise.rb`:
16
+
17
+ ```ruby
18
+ config.omniauth :globalid, ENV["GLOBALID_CLIENT_ID"], ENV["GLOBALID_CLIENT_SECRET"],
19
+ ```
20
+
21
+ Otherwise, you'll probably want to add this to the middleware of the Rails app in `config/initializers/omniauth.rb`:
22
+
23
+ ```ruby
24
+ Rails.application.config.middleware.use OmniAuth::Builder do
25
+ provider :globalid, ENV["GLOBALID_CLIENT_ID"], ENV["GLOBALID_CLIENT_SECRET"]
26
+ end
27
+ ```
28
+
29
+ Options you can pass in the initialization (none are required):
30
+
31
+ | Parameter | Description |
32
+ | --------- | ----------- |
33
+ | `acrc_id` | _Verification Requirements_, e.g. a requirement that the user has a valid government id |
34
+ | `scope` | Must be `openid` if passing an `acrc_id` that specifies [PII sharing](#access-pii-from-the-vault) |
35
+ | `private_key` | Private key given to globaliD. Required for [PII sharing](#access-pii-from-the-vault) |
36
+ | `private_key_pass` | Password for `private_key` specified |
37
+ | `decrypt_pii_on_login` | Decrypt PII on login, passing it through the authentication hash |
38
+
39
+ Here is what a configuration for a setup that uses PII sharing looks like:
40
+
41
+ ```ruby
42
+ provider :globalid,
43
+ ENV["GLOBALID_CLIENT_ID"],
44
+ ENV["GLOBALID_CLIENT_SECRET"],
45
+ acrc_id: ENV["ACRC_ID"],
46
+ scope: "openid",
47
+ private_key: ENV["GLOBALID_PRIVATE_KEY"],
48
+ private_key_pass: ENV["GLOBALID_PRIVATE_KEY_PASS"],
49
+ decrypt_pii_on_login: true
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
+ client_credentials_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"], acrc_id: ENV["ACRC_ID"] }
158
+ token_response = Faraday.new(url: token_url).post do |req|
159
+ req.headers["Content-Type"] = "application/json"
160
+ req.body = client_credentials_token_params.to_json
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/json"
269
+ req.body = refresh_params.to_json
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
+ ```
@@ -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,85 @@
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,
7
+ redirect_uri: nil, private_key: nil, private_key_pass: nil, acrc_id: nil)
8
+ @openid_token = openid_token
9
+ # TODO: Figure out a cleaner way to implement this!
10
+ @token_url = token_url || "https://api.global.id/v1/auth/token"
11
+ @client_id = client_id || ENV["GLOBALID_CLIENT_ID"]
12
+ @client_secret = client_secret || ENV["GLOBALID_CLIENT_SECRET"]
13
+ @redirect_uri = redirect_uri || ENV["GLOBALID_REDIRECT_URL"]
14
+ @acrc_id = acrc_id || ENV["ACRC_ID"]
15
+ # Clean up the private key in case environmental variables were extra escaped
16
+ private_key ||= ENV["GLOBALID_PRIVATE_KEY"].gsub("\\n", "\n").gsub("\"", "")
17
+ private_key_pass ||= ENV["GLOBALID_PRIVATE_KEY_PASS"]
18
+ @private_key = OpenSSL::PKey::RSA.new(private_key, private_key_pass)
19
+ end
20
+
21
+ attr_accessor :openid_token, :private_key
22
+
23
+ def decrypted_pii
24
+ vault_response.map do |vault_data|
25
+ # Decrypt the password for the vault data
26
+ decrypted_data_password = private_key.private_decrypt(Base64.decode64(vault_data["encrypted_data_password"]), OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
27
+ # The Initialization Vector is the first 32 bytes of the encrypted data
28
+ iv = vault_data["encrypted_data"][0, 32]
29
+ # The actual encrypted data is everything after the first 32 bytes
30
+ encrypted_data = vault_data["encrypted_data"][32, vault_data["encrypted_data"].length]
31
+ # Create a cipher that can decrypt the data that was encrypted in the vault
32
+ cipher = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
33
+ cipher.decrypt # Tell the cipher instance that we are going to decrypt with it
34
+ # The password and the IV are hex encoded
35
+ cipher.key = Array(decrypted_data_password).pack("H*") # Encode the password in hex (base16)
36
+ cipher.iv = Array(iv).pack("H*") # the initialization vector (iv) is first 32 chars of the encoded_data, hex encoded
37
+ # Decode the base64 encoded data, and decrypt it!
38
+ decrypted_pii = cipher.update(Base64.decode64(encrypted_data)) + cipher.final
39
+ JSON.parse(decrypted_pii)
40
+ end
41
+ end
42
+
43
+ def encrypted_data_tokens
44
+ # Parsing this is a paid because the keys are dynamic
45
+ # And we need the inside of the nested structure :(
46
+ @openid_token.select { |k, v| k.match?("/claims/") }
47
+ .reject { |k, v| k.match?("/claims/null") }
48
+ .values.map(&:values).flatten
49
+ end
50
+
51
+ def decrypted_tokens
52
+ # Get the tokens to make requests to the vault, which is how you access the PII, by decrypting the encrypted_data_tokens
53
+ encrypted_data_tokens.map do |claim_token|
54
+ # The claim_tokens are base64 encoded
55
+ private_key.private_decrypt(Base64.decode64(claim_token), OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
56
+ end
57
+ end
58
+
59
+ def client_credentials_access_token
60
+ # TODO: figure out how to configure these without having to specify via environmental variables
61
+ client_credentials_token_params = {
62
+ client_id: @client_id,
63
+ client_secret: @client_secret,
64
+ redirect_uri: @redirect_uri,
65
+ grant_type: "client_credentials",
66
+ acrc_id: @acrc_id
67
+ }
68
+ client_credentials_response = Faraday.new(url: @token_url).post do |req|
69
+ req.headers["Content-Type"] = "application/json"
70
+ req.body = client_credentials_token_params.to_json
71
+ end
72
+ JSON.parse(client_credentials_response.body)["access_token"] # this is the only part of the client_credentials_response we use
73
+ end
74
+
75
+ def vault_response
76
+ result = Faraday.new(url: "https://api.global.id/v1/vault/get-encrypted-data").post do |req|
77
+ req.headers["Authorization"] = "Bearer #{client_credentials_access_token}"
78
+ req.headers["Content-Type"] = "application/json"
79
+ req.body = { private_data_tokens: decrypted_tokens }.to_json
80
+ end
81
+ JSON.parse(result.body)
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAuth
4
+ module Globalid
5
+ VERSION = "0.1.5"
6
+ end
7
+ end
@@ -1,20 +1,43 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "omniauth-oauth2"
4
+ require "jwt"
2
5
 
3
6
  module OmniAuth
4
7
  module Strategies
5
8
  class Globalid < OmniAuth::Strategies::OAuth2
6
9
  option :name, "globalid"
7
- DEFAULT_SCOPE = "public".freeze
10
+ DEFAULT_SCOPE = "public"
8
11
 
9
12
  option :client_options, site: "https://auth.global.id",
10
13
  authorize_url: "/",
11
14
  token_url: "https://api.globalid.net/v1/auth/token"
12
15
 
16
+ def self.parse_jwt(id_token)
17
+ JWT.decode(id_token, nil, false).first
18
+ end
19
+
13
20
  # https://github.com/omniauth/omniauth-oauth2/issues/81
14
21
  def callback_url
15
22
  full_host + script_name + callback_path
16
23
  end
17
24
 
25
+ def authorize_params
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])
39
+ end
40
+
18
41
  uid { raw_info["gid_uuid"] }
19
42
 
20
43
  info do
@@ -24,15 +47,33 @@ module OmniAuth
24
47
  description: raw_info["description"],
25
48
  image: raw_info["display_image_url"],
26
49
  location: location(raw_info),
27
- }
50
+ }.merge(id_token: openid_token)
51
+ .merge(decrypted_pii: decrypted_pii)
28
52
  end
29
53
 
30
54
  def raw_info
31
55
  return @raw_info if defined?(@raw_info)
56
+
32
57
  result = api_connection.get("/v1/identities/me")
33
58
  @raw_info = JSON.parse(result.body)
34
59
  end
35
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
+
36
77
  private
37
78
 
38
79
  def api_connection
@@ -43,6 +84,30 @@ module OmniAuth
43
84
  end
44
85
  end
45
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
+ acrc_id: options[:acrc_id],
93
+ redirect_uri: options[:redirect_uri],
94
+ private_key: options[:private_key],
95
+ private_key_pass: options[:private_key_pass])
96
+ end
97
+
98
+ def acrc_id_in_request?
99
+ request.params.key?("acrc_id") || request.params[:acrc_id]
100
+ end
101
+
102
+ def acrc_id_provided?
103
+ options[:acrc_id] || acrc_id_in_request?
104
+ end
105
+
106
+ def pii_sharing?
107
+ # TODO: make this actually check if we need PII sharing. For now, just assuming
108
+ acrc_id_provided? && options.key?("private_key")
109
+ end
110
+
46
111
  def location(raw_info)
47
112
  location = [
48
113
  raw_info["metro_name"],
@@ -56,7 +121,9 @@ module OmniAuth
56
121
  end
57
122
 
58
123
  def nickname(raw_info)
59
- raw_info["gid_name"] if raw_info["gid_name_moderation_status"] == "accepted"
124
+ return if raw_info["gid_name_moderation_status"] != "accepted"
125
+
126
+ raw_info["gid_name"]
60
127
  end
61
128
  end
62
129
  end
@@ -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"
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.6
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Seth Herr
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-09-13 00:00:00.000000000 Z
11
+ date: 2020-11-03 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'
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'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: rack
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -58,31 +72,52 @@ 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
- email:
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
- post_install_message:
120
+ post_install_message:
86
121
  rdoc_options: []
87
122
  require_paths:
88
123
  - lib
@@ -90,15 +125,16 @@ 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
101
- signing_key:
135
+ rubyforge_project:
136
+ rubygems_version: 2.7.6.2
137
+ signing_key:
102
138
  specification_version: 4
103
139
  summary: OmniAuth strategy for GlobaliD
104
140
  test_files: []
@@ -1,5 +0,0 @@
1
- module OmniAuth
2
- module Globalid
3
- VERSION = "0.0.6"
4
- end
5
- end