OmniAuth globaliD
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.
Installation
Install this gem by adding it to your gemfile,
gem "omniauth-globalid"
Then bundle install
If you're adding this to a Rails app using devise with OmniAuth, add this to your config/initializers/devise.rb
:
config.omniauth :globalid, ENV["GLOBALID_CLIENT_ID"], ENV["GLOBALID_CLIENT_SECRET"],
Otherwise, you'll probably want to add this to the middleware of the Rails app in config/initializers/omniauth.rb
:
Rails.application.config.middleware.use OmniAuth::Builder do
provider :globalid, ENV["GLOBALID_CLIENT_ID"], ENV["GLOBALID_CLIENT_SECRET"]
end
Options you can pass in the initialization (none are required):
Parameter | Description |
---|---|
acrc_id |
Verification Requirements, e.g. a requirement that the user has a valid government id |
scope |
Must be openid if passing an acrc_id that specifies PII sharing |
private_key |
Private key given to globaliD. Required for PII sharing |
private_key_pass |
Password for private_key specified |
decrypt_pii_on_login |
Decrypt PII on login, passing it through the authentication hash |
Here is what a configuration for a setup that uses PII sharing looks like:
provider :globalid,
ENV["GLOBALID_CLIENT_ID"],
ENV["GLOBALID_CLIENT_SECRET"],
acrc_id: ENV["ACRC_ID"],
scope: "openid",
private_key: ENV["GLOBALID_PRIVATE_KEY"],
private_key_pass: ENV["GLOBALID_PRIVATE_KEY_PASS"],
decrypt_pii_on_login: true
If you're curious about what those options mean, or how to use them, read globaliD's documentation or the walkthroughs in this readme.
Local development
Run the tests with rake
Use bundle exec guard
to watch the files for changes and rerun tests
globaliD Authentication Walkthroughs
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:
- Setup (required for all the walkthroughs)
- OAuth2 Authorization Code Request Flow
- OpenID Connect Flow
- Refresh access tokens Flow
- Access Personally Identifiable Information from the Vault
Setup
To be able to run these walkthroughs you first have to install the necessary gems:
gem install "dotenv"
gem install "jwt"
gem install "faraday"
And set up a .env
file that has values for these keys: GLOBALID_CLIENT_ID
, GLOBALID_CLIENT_SECRET
, ACRC_ID
, REDIRECT_URL
If you're going to execute the walkthrough commands in IRB, enter irb and run these commands:
require "dotenv/load" # Load the .env file
require "jwt" # Load the gem for decoding JWTs
require "faraday" # http request library
token_url = "https://api.globalid.net/v1/auth/token" # token_url is used multiple times, so store in a variable
OAuth2 Authorization Code Request Flow
The server side authorization flow for "Sign in with GlobaliD"
1. Create a URL that renders the globaliD sign in page
This URL includes these parameters
Parameter | Required? | Description |
---|---|---|
client_id |
✔ | Defines which app is making this authentication request. Setup in globaliD's developer panel |
scope |
✔ | Defines what you are asking permission to do for the user. In the basic setup, this is public |
redirect_uri |
✔ | Where to send user after authentication, must match the app defined by the client_id |
response_type |
✔ | Needs to be code , because we're doing the authorization code flow |
state |
✔ | Security parameter to prevent request forgery |
acrc_id |
Verification Requirements, e.g. a requirement that the user has a valid government id |
This will create the URL in IRB:
# These are the parameters for the globaliD authorization URL:
= { 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" }
# Which you use to compose the globaliD authorization URL -
= "https://auth.global.id?" + URI.encode_www_form()
2. Send user to the generated globaliD url
After the user authenticates, they are redirected to the redirect_url
with these parameters:
Parameter | Description |
---|---|
grant_type |
The type of OAuth flow - we're doing the authorization_code |
code |
The authorization code, which we will use to get an access token |
state |
Security parameter to prevent request forgery |
... If you're following along in IRB, you can open the authorization url in your browser with:
system "open '#{}'"
3. Receive the request that comes to the redirect_url
The URL will look like:
https://global.id/fake_redirect_auth/?grant_type=authorization_code&code=7b35a90b8f904aae9db676660e33784e
The code
parameter's value from the above URL is 7b35a90b8f904aae9db676660e33784e
. Assign this to code
in IRB:
code = "7b35a90b8f904aae9db676660e33784e" # use YOUR code, not the sample ;)
If you have a functioning Omniauth installation this will happen in your controller
4. Make a request to globaliD's token URL to get the access_token
= { client_id: ENV["GLOBALID_CLIENT_ID"], client_secret: ENV["GLOBALID_CLIENT_SECRET"], grant_type: "authorization_code", code: code, redirect_uri: ENV["REDIRECT_URL"] }
token_response = Faraday.new(url: token_url).post do |req|
req.headers["Content-Type"] = "application/x-www-form-urlencoded"
req.body = URI.encode_www_form()
end
5. Parse the JSON from token_response above
The token_response.body
for the above request will be JSON that looks something like this:
{
access_token: "eyJhb....", // This will be a big string, truncated here for legibility
token_type: "bearer",
expires_in: 7200,
refresh_token: "4806a8863c62a6509046638d80c37d16",
id_token: null
}
You'll need to store the access_token
, refresh_token
and scope
to be able to make authenticated requests to globaliD's API.
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.
With this access_token
you can make authenticated requests to globaliD's APIs
Notes
- Check out an interactive demonstration of OAuth2 authorization code requests
- Libraries help make this much easier for example, the OAuth2 Gem
OpenID Connect Flow
OpenID Connect is just an expansion of the OAuth2 request flow (documented above)
The differences are:
- You must use the scope
openid
and include an acrc_id parameter when creating the URL for the user to authenticate (step 1) - The access token response is different and requires additional parsing (step 5)
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:
5. Parse the JSON from the access token response and decode the JSON Web Token
The token_response
from the OpenID Connect response will look something like this:
{
access_token: "eyJhbGciOiJSUzI1", // truncated for legibility
token_type: "bearer",
expires_in: 7200,
refresh_token: "0ec3c111cb18f4a48db2d8246b8cb5eb",
id_token: "eyJhbGciOiJSUzI1NiIsIn" // starts same as access token but is longer, truncated for legibility
}
The id_token
parameter from this response is a JSON Web Token (JWT).
We only need the id_token
value from this response, which we get and decode like this:
id_token = JSON.parse(token_response.body)["id_token"]
# NOTE: The JWT signature from globaliD's API is broken right now because of an issue with the location of the public key
# So for now, we skip verifying the signature, and parse the JWT with this:
decoded_token = JWT.decode(id_token, nil, false).first
The decoded JWT looks like this:
{
"sub": "ef141f5d-2a9f-429d-999f-8bbec78a733a", // the globaliD UUID for the user who authenticated
"iss": "https://globalid.net",
"nonce": "af78x76zv87xv78v",
"iat": "1570659177025",
"exp": "1570745577026",
"idp.globalid.net/claims/null": {},
"idp.globalid.net/claims/dd24263d-079b-4779-9776-167fe6e03ab8": {
"bf4cd542-216f-4377-bc46-7601eca09048": [
"WQexnTFKt1E...." // Base64 encoded, encrypted claims token
]
}
}
You can use the access_token from this response to make authenticated requests to globaliD's APIs.
The claims key-value pairs from this JWT include the data necessary to access PII from the Vault.
Notes
- Using the visual decoder and debugger at jwt.io can be helpful for understanding how globaliD's OpenID Connect response works (and understanding JWTs in general)
Refresh access tokens Flow
This is how to refresh an access_token
once its expiration passes
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"
.
1. Make a POST request to the token url with the refresh_token
, client_id
and client_secret
refresh_token = "70165f183e7efee2b298302bcaea5276" # Replace with the actual code you have
refresh_params = { client_id: ENV["GLOBALID_CLIENT_ID"], redirect_uri: ENV["REDIRECT_URL"], grant_type: "refresh_token", refresh_token: refresh_token }
response = Faraday.new(url: token_url).post do |req|
req.headers["Content-Type"] = "application/x-www-form-urlencoded"
req.body = URI.encode_www_form(refresh_params)
end
2.Parse the JSON in the response.body
, the access token is the access_token
value
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).
Access PII from the Vault
To be able to make an ACRC request with PII You also must have given globaliD an encryption key.
The key must be in the .pem
format, (you'll be required to enter a password) - generate it with:
openssl genrsa -des3 -out key_for_globaliD.key 4096
openssl rsa -in "key_for_globaliD.key" -pubout > "public_key_for_globaliD.pub"
Once you have those files:
- Send the public key (
public_key_for_globaliD.pub
generated from the above snippet) to [email protected] - Add the private key (
key_for_globaliD.key
) to your.env
file under asGLOBALID_PRIVATE_KEY
(wrap the key in quotes and replace the new lines with\n
) - Add the password to the
.env
file asGLOBALID_PRIVATE_KEY_PASS
To be able to access a user's PII, you'll have to follow the OpenID Connect Flow above - you must have included an acrc_id
that authorizes PII sharing when following that flow.
Then, follow these steps (all the code is at the end)
6. Make another token request to get an access_token
,
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.
7. Extract the claims
from the JWT
The JWT key that is idp.globalid.net/claims/#{acrc_id}
holds the claims values
8. Decode and decrypt the claims
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
9. Get the vault responses
Make a request to the globaliD vault with the private_data_tokens
in the body of the request, and parse the response JSON
10. Decode and decrypt the vault responses
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
11. Split the Vault data into the Initialization Vector and the Encrypted data
The first 32 bytes are the Initialization Vector (iv). The following bytes is the actual encrypted data that you want to decrypt.
12. Decrypt the AES encrypted data
The decrypted_data_password
and the iv
need to be Hex encoded and applied to an AES 256 CBC Cipher to decrypt the PII.
This is code you can paste into IRB
This covers steps 6 through 12, provided you followed the OpenID Connect flow in IRB
# Load the private key with Ruby's encryption library
private_key = OpenSSL::PKey::RSA.new(ENV["GLOBALID_PRIVATE_KEY"], ENV["GLOBALID_PRIVATE_KEY_PASS"])
# We only care about the *encrypted_claims_token* part of the decoded_token - but keys are dynamically named
# The decoded_token has a key-value pair with a key that is `idp.globalid.net/claims/#{acrc_id}`
# The value from this key value pair will itselv be key-value pairs, with values of the encrypted_data_tokens for the vault
# Parsing this is a pain, we find matching keys, grab values for those keys, and then grab the values of those values :/
claims_key_values = decoded_token.select { |k, v| k.match?(ENV["ACRC_ID"]) }
encrypted_data_tokens = claims_key_values.values.map(&:values).flatten
# Get the tokens to make requests to the vault, which is how you access the PII, by decrypting the encrypted_data_tokens
decrypted_tokens = encrypted_data_tokens.map do |claim_token|
# The claim_tokens are base64 encoded
private_key.private_decrypt(Base64.decode64(claim_token), OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
end
# At this point you need to get an access token to make requests to the vault.
# Get this access token by making a request to the token_url using the `client_credentials` grant_type
# NOTE: the client_credentials grant_type doesn't require a user token
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"] }
client_credentials_response = openid_response = Faraday.new(url: token_url).post do |req|
req.headers["Content-Type"] = "application/x-www-form-urlencoded"
req.body = URI.encode_www_form(client_credentials_token_params)
end
# client_credentials_response body is JSON that looks like this: { access_token: "...", token_type: "bearer", expires_in: 7200, refresh_token: nil }
access_token = JSON.parse(client_credentials_response.body)["access_token"] # this is the only part of the client_credentials_response we use
# We now have all the data we need to be able to make requests to the vault!
# Make a request to the vault, using the access token, with the decrypted tokens:
vault_response = Faraday.new(url: "https://api.global.id/v1/vault/get-encrypted-data").post do |req|
req.headers["Authorization"] = "Bearer #{access_token}"
req.headers["Content-Type"] = "application/json"
req.body = { private_data_tokens: decrypted_tokens }.to_json
end
# The vault_response body potentially has multiple responses, so we need to decrypt each of them:
pii_key_values = JSON.parse(vault_response.body).map do |vault_data|
# Decrypt the password for the vault data
decrypted_data_password = private_key.private_decrypt(Base64.decode64(vault_data["encrypted_data_password"]), OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
# The Initialization Vector is the first 32 bytes of the encrypted data
iv = vault_data["encrypted_data"][0, 32]
# The actual encrypted data is everything after the first 32 bytes
encrypted_data = vault_data["encrypted_data"][32, vault_data["encrypted_data"].length]
# Create a cipher that can decrypt the data that was encrypted in the vault
cipher = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
cipher.decrypt # Tell the cipher instance that we are going to decrypt with it
# The password and the IV are hex encoded
cipher.key = Array(decrypted_data_password).pack("H*") # Encode the password in hex (base16)
cipher.iv = Array(iv).pack("H*") # the initialization vector (iv) is first 32 chars of the encoded_data, hex encoded
# Decode the base64 encoded data, and decrypt it!
decrypted_pii = cipher.update(Base64.decode64(encrypted_data)) + cipher.final
JSON.parse(decrypted_pii)
end
# And there you have it! Simple ;)
pp pii_key_values