Skip to content

Instantly share code, notes, and snippets.

@leesmith
Forked from eliotsykes/api_controller.rb
Created August 14, 2020 20:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save leesmith/6549146a04b3623e3097973a9a6c537a to your computer and use it in GitHub Desktop.
Save leesmith/6549146a04b3623e3097973a9a6c537a 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment