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 +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 +85 -0
- data/lib/omniauth/globalid/version.rb +7 -0
- data/lib/omniauth/strategies/globalid.rb +70 -3
- data/omniauth-globalid.gemspec +9 -8
- metadata +49 -13
- data/lib/omniauth-globalid/version.rb +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ee817323908f8a4bda10bb5e71c86bc5e7900d6d63eb71772de5c3c700ce1714
|
4
|
+
data.tar.gz: c0318012c3dace7a58f3b607d4c3de8ce1e0ab18abf505031986fc02a82d7968
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7af4b3659f7b945c60f1816a0514763eda8df43467a7075b4984dae03c017ea55f133346d8e3537a739ac223ac8cda2925d788e85100e720616b7e6efb1366cd
|
7
|
+
data.tar.gz: 4d39d5a172625f1d8446a8f9ff2a3646e76da0c0a35a84451e9990ed0fa99e29e212133490c8f92610187437a3f801905acbb67b6075fa3538e03289c6de38b7
|
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
|
+
`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
|
-
|
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
|
+
```
|
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,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
|
@@ -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"
|
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
|
-
|
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
|
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"
|
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.
|
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:
|
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.
|
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
|
-
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
|
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
|
-
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:
|
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
|
-
|
101
|
-
|
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: []
|