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_ORIGINis based on theOriginheader 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,
Originis 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
Originfor 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.
