Today I explored different approaches to modelling API tokens. In most of our projects, we create a secure string and then hash it using a SHA256 algorithm.
The thing is, when I look at that code, it’s kind of scary. There are loads of security-related terms mixed with secrets, algorithms and randomness. The “shape” of the code gives off this aura of “don’t touch any of this, it’s really hard and secure” - which makes it intimidating to work with or modify.
Rails provides several built-in methods for handling tokens, but none of them quite hit the sweet spot on their own. Here’s what I found while investigating alternatives.
The usual suspects
Rails has has_secure_token which creates a plain text unique(ish) token. In our applications we treat API tokens like passwords, so we usually store the hashed version and display the unhashed (raw) version only once to a User. The Rails method doesn’t quite get us all the way as there’s no way to store it other than in plain text.
There’s also generate_token_for which is really powerful - it doesn’t need any data to be stored. The downside is that it’s easy to request a new token for the same record and therefore difficult to know how many tokens are in use or have been generated.
A more elegant approach
The approach I was relatively happy with was to use Rails has_secure_password with a :token field - has_secure_password :token.
This requires the bcrypt gem and a token_digest column added to the model. Paired with an after_initialize callback to set the token if there is no token_digest, we have a secure token setup. The token value will only be available one time and the token_digest is completely secure.
# models/authentication_token.rb
# == Schema Information
#
# Table name: authentication_tokens
#
# id :int not null, primary key
# token_digest :string not null
#
class AuthenticationToken < ApplicationRecord
has_secure_password :token, reset_token: false
after_initialize -> {
unless token_digest
self.token = SecureRandom.base58(36)
end
}
end
# generate a token
auth_token = AuthenticationToken.create
auth_token.id #=> 1234
auth_token.token #=> f229029b48146c5
Making tokens findable
Lookup is the next issue. It would be impractical to lookup a previously stored token in this manner. I built an access_token method which would simply join the record’s id with the token and a . to separate them.
Ok so why do that? Well when an API request containing this access token needs to be authenticated, we can use more helpers from Rails. We can get the access token from the Authorization header using authenticate_or_request_with_http_token mixed with authenticate_by, provided by the has_secure_password module, to identify and authenticate the access token.
# models/authentication_token.rb
# == Schema Information
#
# Table name: authentication_tokens
#
# id :int not null, primary key
# token_digest :string not null
#
class AuthenticationToken < ApplicationRecord
has_secure_password :token, reset_token: false
after_initialize -> {
unless token_digest
self.token = SecureRandom.base58(36)
end
}
def access_token
return unless id && token
[id, token].join(".")
end
end
# generate a token
auth_token = AuthenticationToken.create
auth_token.id #=> 1234
auth_token.token #=> f229029b48146c5
auth_token.access_token #=> 1234.f229029b48146c5
Now in the controller we can authenticate requests by splitting the access token and using authenticate_by:
# controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_action :authenticate
def authenticate
authenticate_or_request_with_http_token do |access_token, _options|
id, token = access_token.split(".", 2)
AuthenticationToken.authenticate_by(id:, token:)
end
end
end
And here’s how you’d use it in a curl request:
curl -X GET --location "http://127.0.0.1:1025/api/" \
-H "Authorization: Bearer 1234.f229029b48146c5" \
-H "Content-Type: application/json"
An alternative using Token parameters
While digging into the authenticate_or_request_with_http_token method, I found that if a Token Authorization header is used, parameters can be passed to the controller. In this case we could avoid the combined id and token approach and pass them separately.
I’m not sure if this is the intended use for these options, but it works:
# models/authentication_token.rb
# == Schema Information
#
# Table name: authentication_tokens
#
# id :int not null, primary key
# token_digest :string not null
#
class AuthenticationToken < ApplicationRecord
has_secure_password :token, reset_token: false
after_initialize -> {
unless token_digest
self.token = SecureRandom.base58(36)
end
}
end
# generate a token
auth_token = AuthenticationToken.create
auth_token.id #=> 1234
auth_token.token #=> f229029b48146c5
The controller would handle it like this:
# controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_action :authenticate
def authenticate
authenticate_or_request_with_http_token do |token, options|
id = options.fetch("id")
AuthenticationToken.authenticate_by(id:, token:)
end
end
end
And the curl request would look like this:
curl -X GET --location "http://127.0.0.1:1025/api/" \
-H "Authorization: Token token=\"f229029b48146c5\", id=\"1234\"" \
-H "Content-Type: application/json"
Both of these approaches feel more elegant than the SHA256 approach. No scary-looking security code scattered everywhere. No secret keys being generated in one place and hashed in another. Just Rails methods doing what they’re designed to do - has_secure_password handles the security, authenticate_by handles the lookup, and authenticate_or_request_with_http_token handles the request authentication.
The code is cleaner, easier to understand, and doesn’t give off that “don’t touch this” vibe. You can look at it and immediately understand what’s happening without needing to trace through hashing algorithms and secret management.