Lock Down Your APIs: Simple Security Measures (Whitelisting, JSON Web Tokens, API Keys, OAuth 2.0, Basic Authentication, Hash-based Message Authentication Code)

Unsecured APIs are vulnerabilities. Learn effective ways to secure your REST APIs: Whitelisting, JSON Web Tokens, API Keys, OAuth 2.0, Basic Authentication and Hash-based Message Authentication Code for secure authentication.
Lock Down Your APIs:  Simple Security Measures (Whitelisting, JSON Web Tokens, API Keys, OAuth 2.0, Basic Authentication, Hash-based Message Authentication Code)

REST APIs https://vulehuan.com/en/blog/2024/7/rest-apis-simple-communication-for-powerful-apps-668a145df0915ca45912b906.html are like doors to your web application. Just as you lock your house door, you need to secure your API. Here are two simple ways to do it: Whitelisting Websites and Using Tokens.

Whitelisting Websites

Imagine you have a party, and you only want your friends to come. You make a list of their names and check it at the door. Whitelisting works the same way for APIs:

  • Make a list of allowed websites
  • When a request comes, check if it's from an allowed site
  • If yes, let it through. If no, block it
require 'sinatra'

ALLOWED_DOMAINS = ['client-1.com', 'client-2.net']

get '/api/orders' do
  origin = request.env['HTTP_ORIGIN']
  unless ALLOWED_DOMAINS.any? { |domain| origin&.include?(domain) }
    halt 403, "Access denied"
  end
  # Your code
end

request.env['HTTP_ORIGIN'] is not entirely secure on its own:

  • What is HTTP_ORIGIN? HTTP_ORIGIN is based on the Origin header sent by the client. This header is meant to indicate where the request originated from.
  • Can it be faked? Yes, it can be faked. Any HTTP header, including Origin, can be manipulated by the client. A malicious user could potentially set any value they want for this header.
  • Why is it still used? Despite its limitations, Origin is still useful in combination with other security measures, particularly for CORS (Cross-Origin Resource Sharing) policies.

How to improve security:

  • Don't rely solely on Origin for critical security decisions.
  • Use it in conjunction with other security measures like JWT authentication.
  • Implement proper CORS policies on your server.
  • Use HTTPS to prevent man-in-the-middle attacks that could modify headers.

Using Tokens

Tokens are like secret passwords for your API. Here's how they work:

  • When a user logs in, give them a special token
  • The user sends this token with every API request
  • Your API checks if the token is valid before responding

JSON Web Tokens (JWTs)

JWTs are a popular type of token. They're like digital ID cards containing:

  • Who the user is
  • When the token expires
  • A secret signature to prove it's real

How to use JWTs:

  • User logs in with username and password
  • Your server creates a JWT and sends it back
  • The user's app stores the JWT
  • For each API request, the app sends the JWT
  • Your API checks the JWT before responding

By combining whitelisting and JWTs https://github.com/jwt/ruby-jwt, you create two layers of security. It's like having a guard at the door (whitelisting) and checking ID cards inside (JWTs).
Example of a more robust approach in Ruby:

require 'sinatra'
require 'jwt'

SECRET_KEY = 'my_secret_key'
ALLOWED_DOMAINS = ['client-1.com', 'client-2.net']

before do
  # Check Origin as a first line of defense
  origin = request.env['HTTP_ORIGIN']
  unless ALLOWED_DOMAINS.any? { |domain| origin&.include?(domain) }
    halt 403, "Access denied"
  end

  # Verify JWT for more robust security
  token = request.env['HTTP_AUTHORIZATION']
  begin
    @payload = JWT.decode(token, SECRET_KEY, true, algorithm: 'HS256')[0]
  rescue JWT::DecodeError
    halt 401, "Invalid token"
  end
end

get '/api/orders' do
  # At this point, we've verified both Origin and JWT
  # Your code with @payload['username']
end

Other approaches

API Keys

Simple to implement, but less secure for user-specific operations.
Example in Ruby:

require 'sinatra'

API_KEYS = ['key1', 'key2', 'key3']

before do
  api_key = request.env['HTTP_X_API_KEY']
  halt 401, 'Invalid API Key' unless API_KEYS.include?(api_key)
end

get '/api/data' do
  "Here's your data!"
end

OAuth 2.0

More complex but very secure and widely used for third-party authentication.
Example using the oauth2 gem:

require 'sinatra'
require 'oauth2'

client = OAuth2::Client.new('client_id', 'client_secret', site: 'https://example.com')

get '/auth' do
  redirect client.auth_code.authorize_url(redirect_uri: 'http://localhost:4567/callback')
end

get '/callback' do
  token = client.auth_code.get_token(params[:code], redirect_uri: 'http://localhost:4567/callback')
  session[:access_token] = token.token
  "You're authenticated!"
end

Basic Authentication

Example:

require 'sinatra'

use Rack::Auth::Basic, "Restricted Area" do |username, password|
  username == 'admin' && password == 'secret'
end

get '/api/data' do
  "Here's your protected data!"
end

HMAC (Hash-based Message Authentication Code)

Similar to API keys but more secure.
Example:

require 'sinatra'
require 'openssl'

SECRET_KEY = 'your_secret_key'

def valid_signature?(data, signature)
  calculated = OpenSSL::HMAC.hexdigest('SHA256', SECRET_KEY, data)
  calculated == signature
end

post '/api/data' do
  data = request.body.read
  signature = request.env['HTTP_X_SIGNATURE']
  
  halt 401, 'Invalid signature' unless valid_signature?(data, signature)
  
  "Data received and verified!"
end

Each of these methods has its own pros and cons:

  • API Keys are simple but don't provide user-specific authentication.
  • OAuth 2.0 is very secure and flexible but more complex to implement.
  • Basic Auth is simple but sends credentials with every request.
  • HMAC provides a good balance of security and simplicity but requires careful implementation.

The choice depends on your specific needs, such as the type of application, security requirements, and ease of implementation. For many APIs, a combination of these methods can provide robust security.