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 +4 -4
- data/.editorconfig +11 -0
- data/.gitignore +6 -1
- data/.rubocop.yml +28 -0
- data/Gemfile +14 -0
- data/Guardfile +7 -0
- data/LICENSE +15 -0
- data/README.md +385 -3
- data/Rakefile +8 -0
- data/lib/omniauth-globalid.rb +3 -2
- data/lib/omniauth/globalid/vault.rb +81 -0
- data/lib/{omniauth-globalid → omniauth/globalid}/version.rb +1 -1
- data/lib/omniauth/strategies/globalid.rb +54 -5
- data/omniauth-globalid.gemspec +9 -8
- metadata +45 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cb02c58ca0c780feb42b7518bf4fe51b297a296dee418dbb13f9b2287f8d0d29
|
4
|
+
data.tar.gz: 76eee540664df39fd0d64b596c99b20fd82a0c7aeb1f5865c65f1d3bb30a7c89
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ad5836f727bdfe6a9a50971c4e06b98a50af2a788bfaf6204de0f1ab2908e3a2fcfa5c2ba3fc25dbad2ea87c9993bf9adc91c8c27a47ea109e769ca7da1aac63
|
7
|
+
data.tar.gz: ab52c6695f2a1a68234e02775a0332053c6e70ec42515735a04273593cc4a99777d98b3639cde18afca769bbde80a9e49a734cefa0f9fa8fff08bb8520126c97
|
data/.editorconfig
ADDED
data/.gitignore
CHANGED
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
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
|
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
|
-
|
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
data/lib/omniauth-globalid.rb
CHANGED
@@ -1,2 +1,3 @@
|
|
1
|
-
require "omniauth
|
2
|
-
require
|
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
|
@@ -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
|
-
|
22
|
-
|
23
|
-
|
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
|
-
|
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)
|
data/omniauth-globalid.gemspec
CHANGED
@@ -1,24 +1,25 @@
|
|
1
|
-
#
|
1
|
+
# coding: utf-8
|
2
2
|
$:.push File.expand_path("../lib", __FILE__)
|
3
|
-
require "omniauth
|
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/
|
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 = "
|
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(">=
|
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.
|
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
|
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-
|
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.
|
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: '
|
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
|
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/
|
116
|
+
homepage: https://gitlab.com/globalid/opensource/omniauth-globalid
|
82
117
|
licenses:
|
83
|
-
-
|
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:
|
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
|
-
|
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
|