Exactly!" said Deep Thought. "So once you do know what the question actually is, you'll know what the answer means.
― Douglas Adams, The Hitchhiker's Guide to the Galaxy
Heimdallr is a JWT authorization gem strictly designed for Rails 5 GraphQL API projects.
While there are a handful of other projects that provide authorization and/or JWT support, none of them fit my specific needs:
- No built-in GUI.
- Scope based permissions.
- Revocable & refreshable tokens.
- Support for both HMAC & RSA encryption.
- And (most importantly) support for the amazing GraphQL gem.
Please keep in mind that this project is very much a work-in-progress, and it might not even work for some users. Feel free suggest changes and improvements!
WARNING: Heimdallr only supports PostgreSQL 9.4 and higher! While it would be fairly trivial to support other RDBMS, I currently only use PostgreSQL in my office.
- Installing / Getting Started
- What is a Heimdallr Application?
- Configuration
- Internationalization (I18n)
- GraphQL Types
- GraphQL Mutations
- Services
- Development
Heimdallr is cryptographically signed. To be sure the gem you install has not been tampered with, add the Heimdallr public key (if you have not already) as a trusted certificate:
gem cert --add <(curl -Ls https://raw.githubusercontent.com/nater540/heimdallr/master/certs/heimdallr.pem)
- Put this in your Gemfile
gem 'heimdallr'
- Run the installation generator
rails g heimdallr:install
This will install the Heimdallr initializer into config/initializers/heimdallr.rb
.
- Run the application migration generator
rails g heimdallr:application APPLICATION_MODEL_NAME
Important: If you name your token class anything other than token
, you will need to update the association inside the generated application model!
- Run the token migration generator (Should be done after the application generator so the table name can be found)
rails g heimdallr:token TOKEN_MODEL_NAME
- Include the
Heimdallr::Authenticable
module in yourApplicationController
class ApplicationController < ActionController::API
include Heimdallr::Authenticable
end
- Add
before_action :heimdallr_authorize!
inside your GraphQL controller
class GraphqlController < ApplicationController
before_action :heimdallr_authorize!
end
Admittedly "Application" is not the best term that I could have used, but I digress...
Simply put, an application is a class that may issue, renew & revoke JWT tokens with specific permissions.
For example, you have two separate applications that both access a shared API:
- A client-facing website that can list all users, but it cannot create or delete anything.
- An admin portal that can create, read, update & delete users.
For (hopefully) obvious security reasons, the client-facing website application should not be permitted to issue tokens with the create:users
or obliterate:users
scopes.
This is the default initializer that will be generated:
Heimdallr.configure do |config|
# The default JWT algorithm to use
config.default_algorithm = 'HS512'
# Token validation period (Default: 30 minutes)
config.expiration_time = -> { 30.minutes.from_now.utc }
# The JWT expiration leeway
config.expiration_leeway = 30.seconds
# The master encryption key
config.secret_key = 'RANDOMLY-GENERATED-STRING'
# The default scopes to include for requests without a token (Optional)
config.default_scopes = %w[view]
end
You can set the default JWT algorithm that will be used with the default_algorithm
configuration option.
When you use HMAC for cryptographic signing, each application will have a unique encrypted secret value generated upon creation.
Available Algorithms:
- HS256 - HMAC using SHA-256 hash algorithm.
- HS384 - HMAC using SHA-384 hash algorithm.
- HS512 - HMAC using SHA-512 hash algorithm.
You may retrieve the secret value from the application by doing the following:
application.secret
When you use RSA for cryptographic signing, each application will have a unique encrypted certificate generated upon creation.
Available Algorithms:
- RS256 - RSA using SHA-256 hash algorithm.
- RS384 - RSA using SHA-384 hash algorithm.
- RS512 - RSA using SHA-512 hash algorithm.
You may retrieve the certificate object from the application by doing the following:
application.rsa
You can set the default JWT expiration time by setting a proc to the expiration_time
configuration option.
config.expiration_time = -> { 30.minutes.from_now.utc }
Please keep in mind that all times must be in UTC!
The expiration leeway configuration option is used to account for clock skew.
This is the master secret key used for encryption of application secrets & certificates.
-= DANGER, WILL ROBINSON! =-
Although a secret key is generated when you run the installation generator, it is strictly done to speed up integration time. The secret key value should NEVER be stored under source control!
Instead, use an environment variable like so:
config.secret_key = ENV.fetch(:heimdallr_key)
If you provide an array of default scopes, requests that do not have an Authorization
header will have a new token created automatically.
However, if you do not provide any default scopes requests that do not have an Authorization
header will be rejected with the following error:
{
"errors": [
{
"status": "401",
"source": {
"pointer": "/request/headers/authorization"
},
"title": "Unauthorized",
"detail": "Missing Authorization header."
}
]
}
Note: You must provide a default scope if you plan to use the built-in GraphQL mutations for issuing tokens!
Heimdallr supports I18n using the Rails Internationalization (I18n) API. See config/locales/en.yml
for further information.
Heimdallr includes a few handy GraphQL types that can be installed into your project by running the following generator:
rails g heimdallr:types
This type provides an enum with the following values:
Name | Description |
---|---|
HS256 |
HMAC using SHA-256 hash algorithm. |
HS384 |
HMAC using SHA-384 hash algorithm. |
HS512 |
HMAC using SHA-512 hash algorithm. |
RS256 |
RSA using SHA-256 hash algorithm. |
RS384 |
RSA using SHA-384 hash algorithm. |
RS512 |
RSA using SHA-512 hash algorithm. |
This type provides an enum with (currently) a single value of SECRET
. It is used by the a few GraphQL mutations.
This type is used to expose JWT applications via the API and is entirely optional (However, it is used for mutations). It provides the following fields:
Name | Type | Description |
---|---|---|
id |
UuidType |
A UUID-4 value that is set automatically by the database upon creation. |
name |
String |
Provided when creating a new application, should be a human friendly value. |
ip |
String |
Token issue requests must come from this IP address, or they will be refused (Optional) |
key |
String |
A randomly generated string that must be provided when issuing new tokens. |
scopes |
Array |
An array of scopes that this application is authorized to issue tokens for. |
This type is used to expose JWT tokens via the API, it provides the following fields:
Name | Type | Description |
---|---|---|
id |
UuidType |
A UUID-4 value that is set automatically by the database upon creation. |
ip |
String |
The IP address this token was issued to. |
scopes |
Array |
An array of scopes that this token is granted to use. |
application |
ApplicationType |
The application that issued this token. |
jwt |
String |
The encoded JWT token string. |
createdAt |
DateTime |
An ISO-8601 encoded UTC date string representing when this token was issued. |
expiresAt |
DateTime |
An ISO-8601 encoded UTC date string representing when this token will expire. |
revokedAt |
DateTime |
An ISO-8601 encoded UTC date string representing when this was revoked. |
notBefore |
DateTime |
An ISO-8601 encoded UTC date string representing when this token becomes active. |
An ISO-8601 encoded UTC date string.
A universally unique identifier (UUID) is a 128-bit number used to identify information in computer systems.
Heimdallr includes a number of GraphQL mutations that can be installed into your project by running the following generator:
rails g heimdallr:mutations
This mutation lets you make new JWT applications via a GraphQL request, it has the following input fields:
Name | Type | Required | Description |
---|---|---|---|
name |
String |
Yes | The application name. |
scopes |
Array |
Yes | An array of scopes that this application will be authorized to issue tokens for. |
algorithm |
AlgorithmEnum |
Yes | The algorithm to use for cryptographic signing tokens. |
Example
{
createApplication(input: {
name: "Unicorns & Rainbows",
scopes: ["unicorn:create", "unicorn:update", "unicorn:hug", "unicorn:ride", "rainbow:create", "rainbow:obliterate"],
algorithm: RS256
}) {
application {
id
name
key
}
}
}
This mutation lets you issue JWT tokens via a GraphQL request, it has the following input fields:
Name | Type | Required | Description |
---|---|---|---|
application |
ApplicationInput |
Yes | An input type that has two fields, id (the application UUID) and key (the application key) |
audience |
String |
No | Optional audience string value. |
subject |
String |
No | Optional subject string value. |
scopes |
Array |
Yes | An array of scopes that this token should be issued. (The application must be authorized to issue these scopes!) |
Example
{
createToken(input: {
application: {
id: "6cc8b666-aa57-4933-8899-0205c9eeeb7c",
key: "7c5b4002adb254df96c8a40fe98863f39f2a32324ac26bcd5de27a5dc4e76a22ec9616cd7f074d56e64f4d589e2b82e31c5f6995a454a5ec3d387a6342520234"
},
scopes: ["unicorn:hug", "unicorn:ride", "rainbow:create"],
subject: "Supercalifragilisticexpialidocious"
}) {
token {
jwt
}
}
}
Heimdallr provides a few helper "services" to assist with the creation & management of applications and tokens.
This service allows you to quickly create a new JWT application.
Example
application = Heimdallr::CreateApplication.new(
name: 'My Little Pony',
scopes: %w[unicorn:create unicorn:update unicorn:hug unicorn:ride],
algorithm: 'RS256',
ip: request.remote_ip
).call
This service allows you to quickly create a new JWT token.
Example
# Create a new token, but do not encode it into a JWT string
token = Heimdallr::CreateToken.new(
application: application,
scopes: 'unicorn:ride',
expires_at: 1.hour.from_now,
subject: 'Supercalifragilisticexpialidocious'
).call(encode: false)
This service is required to properly decode a JWT encoded string. Although its' primary purpose
is for the Authenticable
controller mixin, you are free to use it inside your application as well.
Example
token = Heimdallr::DecodeToken.new('JWT-ENCODED-STRING-GOES-HERE', leeway: 30.seconds).call
The following actions are performed when decoding a JWT token:
- The
iss
(Application ID) &jti
(Token ID) claims are fetched. - A cache hit is done to avoid a database query to see if this token was used recently.
- The signature is verified to ensure that the token was not tampered with.
- The
exp
(Expiration) claim is fetched and used to ensure that the token is still valid. - The
nbf
(Not Before) claim is checked to see if this token is valid yet (Optional)
Critical Issues
This service will raise a Heimdallr::TokenError
exception if any of the following occur:
- The
iss
orjti
claims do not exist. - The token does not exist in the cache or database.
- The token is malformed (Missing header, payload or signature)
- The
exp
ornbf
claims do not match what is stored in the database (Sanity checks)
Recoverable Issues
Even if an exception was not raised, you must still check to ensure that the token has no errors:
if token.token_errors?
# TODO: Do something spectacular with the errors!
render json: { errors: [*token.token_errors] }, status: 420
end
Errors can be any of the following:
- The token was revoked (Message:
This token has been revoked. Please acquire a new token and try your request again.
) - The token is expired (Message:
The provided JWT is expired. Please acquire a new token and try your request again.
) - The
nbf
claim is in the future (Message:The provided JWT is not valid yet and cannot be used.
)
Note: The reason that exceptions are not raised for these events is so expired / revoked tokens can be accessed by an administrator or displayed on a UI without a convoluted amount of error handling
To run the local engine server:
bundle install
bundle exec rails server
Run all the rspec unit tests by doing the following:
Note: You must have a PostgreSQL server running locally before running this command!
bin/rails db:create db:migrate RAILS_ENV=test
bundle exec rspec
Make sure you have doctoc installed:
npm install -g doctoc
Run the doctoc command in the project directory:
doctoc README.md --github
Ensure you have the yard gem installed:
gem install yard
Update the documentation by running the yard command in the project directory:
yard