Skip to content

Instantly share code, notes, and snippets.

@eliotsykes
Last active May 10, 2022 03:59
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save eliotsykes/f4d772deffa9a27fb91ab3f5d1f137ba to your computer and use it in GitHub Desktop.
Save eliotsykes/f4d772deffa9a27fb91ab3f5d1f137ba to your computer and use it in GitHub Desktop.
Token Authentication in Rails API Controller and Request Spec
# File: app/controllers/api/api_controller.rb
class Api::ApiController < ActionController::Base
# Consider subclassing ActionController::API instead of Base, see
# http://api.rubyonrails.org/classes/ActionController/API.html
protect_from_forgery with: :null_session
before_action :authenticate
def self.disable_turbolinks_cookies
skip_before_action :set_request_method_cookie
end
disable_turbolinks_cookies
private
def authenticate
# See http://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Token.html
authenticate_or_request_with_http_token do |token, _options|
# Perform token comparison, taking special care to
# avoid timing attacks and length leaks!
# See: https://thisdata.com/blog/timing-attacks-against-string-comparison/
# Bad! Vulnerable to timing attacks:
# token == api_authentication_token # AVOID!!
# Not great, mitigate timing attacks but leaks length information:
# ActiveSupport::SecurityUtils.secure_compare(token, api_authentication_token) # AVOID!!
# OK, mitigate timing attacks and length leaks:
token_digest = ::Digest::SHA256.hexdigest(token)
api_auth_token_digest = ::Digest::SHA256.hexdigest(api_authentication_token)
ActiveSupport::SecurityUtils.secure_compare(token_digest, api_auth_token_digest)
# ActiveSupport::SecurityUtils.variable_size_secure_compare(token, api_authentication_token) # Alternative
end
end
def api_authentication_token
# Encryption option: https://github.com/rocketjob/symmetric-encryption
raise 'TODO: write your code here to lookup and decrypt an API token that is stored encrypted'
# Temporary option:
ENV.fetch('API_AUTHENTICATION_TOKEN') { raise 'Missing API token!' }
# If you are looking up a token per account or per user, do not use the submitted token
# as the lookup key as this is vulnerable to timing attacks.
# BAD: User.find_by_token(submitted_token).token # AVOID!!!
# Instead, use an alternative identifier that is not the token. E.g. one of
# email address, username, or some other value that is *not* the api secret token.
# OK: User.find_by_email(email).token
# Also OK: User.find_by_username(username).token
end
end
# File: spec/api/widgets_api_spec.rb OR spec/requests/widgets_api_spec.rb
require 'rails_helper'
describe 'Widgets API' do
describe 'POST /api/v1/widgets' do
it 'creates widget' do
# See http://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Token.html
# Will generate header like:
# Authorization: Token token=<TOKEN_HERE>
# but these would also work:
# Authorization: Token <TOKEN_HERE>
# Authorization: Bearer <TOKEN_HERE>
# Authorization: Bearer token=<TOKEN_HERE>, foo=bar
encoded_credentials =
ActionController::HttpAuthentication::Token.encode_credentials('YOUR_API_TEST_TOKEN')
headers = { 'Authorization' => encoded_credentials }
params = { name: 'Foo', color: 'red' }
expect do
post '/api/v1/widgets', params: params, headers: headers, as: :json
end.to change { Widget.exists?(params) }.from(false).to(true)
expect(response).to have_http_status :created
expect(response.content_type).to eq 'application/json'
# If you need to get the created widget:
# widget = Widget.last
# expect(widget.confirmed).to eq false
end
end
end
@bhh
Copy link

bhh commented Jun 9, 2019

hi i just found your nice snippet and i have a question for the second example. (trying to undestand timing attacks better)

i looked up the secure_compare stuff
https://api.rubyonrails.org/classes/ActiveSupport/SecurityUtils.html

and they already use a hexdigest for the parameters. so would the second example also be "OK"? because no length leaks out in the comparisson?

additionally considered this page
https://3v4l.org/8tjrs
isnt it still possible to get the length of the whole process if the hashing takes place in the same process with the actual comparisson?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment