Simple Ways to Protect an API: HTTP Basic Authentication and HTTP Token Authentication
The backend for theScore mobile apps consists of many different web services (Core Sports, Personalization, Push Alerts, etc.). Each service provides its own RESTful API. Some of these services are internal. As such, they only have a very limited number of consumers. Having a limited number of consumers for an API lets you keep the authentication really simple for that API.
For the purposes of this blog post, suppose that we have two services. One that is responsible
for detecting alerts (i.e. new goal is scored), and another that is responsible for actually
sending those alerts to mobile devices. Let's call these Detector
and Sender
respectively.
Also, suppose that Sender
is a web service, and Detector
is a background processing system.
Further, whenever Detector
detects an alert, it uses the API provided by Sender
to deliver
the alerts.
The internals of the services described above would look like the following in Ruby/Rails:
# Detector Service
class GoalDetector
def perform(*args)
# Send the alert upon detection of new alert
# REF https://github.com/lostisland/faraday
client = Faraday.new('https://sender.abc.com')
client.post('/alerts', message: 'Spain Goal')
end
end
# Sender Service (https://sender.abc.com)
class AlertsController < ActionController::Base
# POST /alerts
def create
# Send an alert with message in params[:message]
end
end
All is good except that our Sender
API is public. Anyone who happens to stumble upon
that API can discover this great power to send push alerts to our users. So, we need
a way to protect the API from unauthorized access.
HTTP Basic Authentication
This is generally the go-to solution for this problem. Our example above would look like the following with Basic Authentication support:
# Detector Service
class GoalDetector
def perform(*args)
# Send the alert upon detection of new alert
# REF https://github.com/lostisland/faraday
client = Faraday.new('https://sender.abc.com') do |c|
c.basic_auth('username', 'password')
end
client.post('/alerts', message: 'Spain Goal')
end
end
# Sender Service (https://sender.abc.com)
class AlertsController < ActionController::Base
before_action :authenticate
# POST /alerts
def create
# Send an alert with message in params[:message]
end
private
def authenticate
authenticate_or_request_with_http_basic do |username, password|
username == 'username' && password == 'password'
end
end
end
# NOTE: In real applications, we will not have the
# authentication credentials lying around in code.
# We will store them in external configuration.
Now, the POST
request to https://sender.abc.com/alerts
would require that an
Authorization
header is present with the credentials encoded correctly in the value.
The header would look like:
# dXNlcm5hbWU6cGFzc3dvcmQ=\n is Base64-encoded value
# of the string username:password
Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=\n
If you don't have the credentials, you won't be able to generate the Authorization
header correctly. If you send the wrong value for the header in your request, you will
get 401 Unauthorized
response back. So, we've now effectively protected the Sender
service from unauthorized access.
HTTP Token Authentication
With Basic Authentication, you need to configure both username and password
in Sender
and Detector
services. But, if the password is really strong, then it
is a secure credential by itself. In other words, you can think of the password as a
secure token. In this case, the username becomes redundant. This is where Token
Authentication comes in.
Our code example would look like the following with Token Authentication support:
# Detector Service
class GoalDetector
def perform(*args)
# Send the alert upon detection of new alert
# REF https://github.com/lostisland/faraday
client = Faraday.new('https://sender.abc.com') do |c|
c.token_auth('WCZZYjnOQFUYfJIN2ShH1iD24UHo58A6TI')
end
client.post('/alerts', message: 'Spain Goal')
end
end
# Sender Service (https://sender.abc.com)
class AlertsController < ActionController::Base
before_action :authenticate
# POST /alerts
def create
# Send an alert with message in params[:message]
end
private
def authenticate
authenticate_or_request_with_http_token do |token, options|
token == 'WCZZYjnOQFUYfJIN2ShH1iD24UHo58A6TI'
end
end
end
# NOTE: In real applications, we will not have the
# authentication credentials lying around in code.
# We will store them in external configuration.
Now, the POST
request to https://sender.abc.com/alerts
would require that that an
Authorization
header is present with the correct token in the value. The header
would look like:
Authorization: Token token="WCZZYjnOQFUYfJIN2ShH1iD24UHo58A6TI"
If you send the wrong token in the Authorization
header, you will get
401 Unauthorized
response back. Again, we've protected the API from unauthorized
access.
Conclusion
Both HTTP Basic Authentication and HTTP Token Authentication offer really simple solutions to protect an API from unauthorized access. But, with Token Authentication, you will have one less thing to configure in your services and consumers. Because of that, I prefer using Token Authentication.
Having said that, unlike Basic Authentication which is an approved spec, Token Authentication is a draft spec:
- HTTP Authentication: Basic and Digest Access Authentication
- HTTP Authentication: Token Access Authentication
Regardless of being a draft spec, Token Authentication is well-supported and really easy-to-use. I don't see any practical reasons not to use it.
NOTE: Both Basic Authentication and Token Authentication are insecure unless used
over HTTPS
. All the code examples above use HTTPS
to make this explicit. We use
HTTPS
on all of our APIs at theScore.