How to Apply Security on Restful Web Services

In modern web applications, security is a critical aspect, especially for RESTful APIs that provide access to sensitive resources. OAuth2 (Open Authorization) and JWT (JSON Web Tokens) are two widely adopted protocols to secure RESTful web services. 

This article explains how OAuth2 and JWT work together, presents code examples for integrating them, and highlights statistics and analytical data to justify their use.

Why  use Outh2 and JWT for RESTful API Security?

  • OAuth2: OAuth2 is an authorization framework that allows third-party applications to access a user’s resources without exposing their credentials. Instead of sharing usernames and passwords, OAuth2 leverages access tokens, which can be limited in scope and time, improving security.
  • JWT: JSON Web Tokens (JWT) are a compact, URL-safe means of representing claims to be transferred between two parties. JWT tokens are self-contained, meaning they include information about the user, expiration, and more, and they can be digitally signed to ensure authenticity.

The combination of OAuth2 and JWT enables scalable, secure authorization in distributed systems. Here’s how these two work together:

  1. The client requests an access token from an OAuth2 authorization server.
  2. The server issues a JWT token, which the client uses to access protected resources.
  3. The server verifies the JWT to ensure it has not been tampered with and is still valid.

Architecture Overview

The key actors in this architecture are:

  1. Resource Owner: The user who owns the data or resources.
  2. Client: The application that requests access to resources.
  3. Authorization Server: Issues tokens after successful authentication.

Resource Server: Hosts the API or resource and validates the JWT token.

OAuth2 Grant Types

In OAuth2, a Grant Type is a method by which a client (e.g., a web or mobile app) obtains an access token from the Authorization Server. Each grant type defines a specific flow that enables different types of clients and use cases to request and receive tokens in a secure manner. Choosing the right grant type is essential because it influences both security and usability.

There are several OAuth2 grant types, but for most RESTful APIs, the Authorization Code grant is preferred. Here’s a quick overview of the most commonly used grant types:

  • Authorization Code: Used in web applications where the client can securely handle client secrets.
  • Client Credentials: Used in machine-to-machine (M2M) communication.

Password: Direct exchange of username/password for tokens (not recommended for modern systems).

Setting Up OAuth2 and JWT in a REST API

Let’s look at how to secure a RESTful API using OAuth2 and JWT in a typical Node.js application. We’ll use Express.js for building the REST API and jsonwebtoken package for handling JWT.

Step 1: Install Required Libraries

First, install the necessary packages:

npm install express jsonwebtoken body-parser

Step 2: Create the OAuth2 Authorization Server

const express = require('express');
const jwt = require('jsonwebtoken');
const bodyParser = require('body-parser');

const app = express();
app.use(bodyParser.json());

const SECRET_KEY = 'your-secret-key'; // Use a strong secret key
const TOKEN_EXPIRATION = '1h'; // Token valid for 1 hour

// Mock user data for simplicity
const users = {
    'user1': { password: 'password123' }
};

// Route to authenticate user and issue token
app.post('/oauth/token', (req, res) => {
    const { username, password } = req.body;
    if (users[username] && users[username].password === password) {
        const token = jwt.sign({ username }, SECRET_KEY, { expiresIn: TOKEN_EXPIRATION });
        return res.json({ access_token: token });
    }
    return res.status(401).json({ error: 'Invalid credentials' });
});

// Route to validate the token (protected resource)
app.get('/protected', (req, res) => {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];
    if (!token) return res.sendStatus(401);

    jwt.verify(token, SECRET_KEY, (err, user) => {
        if (err) return res.sendStatus(403);
        return res.json({ message: 'Access granted', user });
    });
});

app.listen(3000, () => {
    console.log('Authorization server listening on port 3000');
});

In this example:

  • The /oauth/token endpoint authenticates the user and returns a JWT if credentials are correct.
  • The /protected endpoint is a resource that requires a valid JWT to access.

Step 3: Client Requesting Access Token

To access the protected resource, the client first needs to request an access token from the /oauth/token endpoint:

curl -X POST http://localhost:3000/oauth/token -H "Content-Type: application/json" \
-d '{"username": "user1", "password": "password123"}'

The response will contain the access token:

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Step 4: Accessing Protected Resource

Now, the client can use the access token to access protected resources:

curl -H "Authorization: Bearer <your-access-token>" http://localhost: 3000/protected

JWT Structure and Security Considerations

A JWT consists of three parts: Header, Payload, and Signature. For example:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXIxIiwiaWF0IjoxNjE2NzAwMDAwLCJleHAiOjE2MTY3MzYwMDB9.abc123signature
  • Header: Contains metadata, typically the signing algorithm (e.g., HS256).
  • Payload: Contains claims, such as the username or expiration time.
  • Signature: Ensures the integrity of the token.

Security Best Practices for JWT:

  • Expiration: Always set an expiration (exp) claim to limit the token’s lifespan.
  • Signature: Use strong secret keys and sign tokens using robust algorithms (e.g., RS256).
  • HTTPS: Always transmit tokens over secure HTTPS to prevent man-in-the-middle (MITM) attacks.

Scopes: Limit tokens by scope to control what actions they can authorize.

Advanced Architecture Overview

Authorization Server: Handles authentication and generates signed access tokens (JWT) as well as refresh tokens.

  1. Resource Server: Hosts the API and verifies JWT tokens to authorize requests based on scopes and roles.
  2. Client: The frontend or third-party service that interacts with the API and OAuth2 server.
  3. Refresh Tokens: Used to obtain a new access token after the old one expires, without requiring the user to log in again.
  4. Token Blacklisting: Mechanism to revoke tokens after issuing them (e.g., during logout).

Step 1: Set Up RSA Public/Private Key Pair for JWT

Generate an RSA key pair, which will be used to sign and verify JWT tokens. The private key will be used by the Authorization Server to sign tokens, and the public key will be used by the Resource Server to verify them.

# Generate private key (RSA 2048-bit)
openssl genrsa -out private.pem 2048

# Extract the public key from the private key
openssl rsa -in private.pem -pubout -out public.pem

Step 2: Authorization Server (OAuth2 + JWT with RS256 Signing)

We’ll implement the Authorization Server in Node.js using Express and the jsonwebtoken package, with OAuth2 Authorization Code flow and token issuance.

const express = require('express');
const jwt = require('jsonwebtoken');
const fs = require('fs');
const bodyParser = require('body-parser');

const app = express();
app.use(bodyParser.json());

const PRIVATE_KEY = fs.readFileSync('private.pem', 'utf8'); // RSA Private Key
const PUBLIC_KEY = fs.readFileSync('public.pem', 'utf8');   // RSA Public Key

const TOKEN_EXPIRATION = '15m'; // Access token valid for 15 minutes
const REFRESH_TOKEN_EXPIRATION = '7d'; // Refresh token valid for 7 days

// Scopes and roles definition
const roles = {
    'admin': ['read', 'write', 'delete'],
    'user': ['read'],
};

// Mock user data
const users = {
    'admin': { password: 'admin123', role: 'admin' },
    'user': { password: 'user123', role: 'user' }
};

// Store refresh tokens (for example purposes, should be stored in DB in a real-world scenario
let refreshTokens = [];

// OAuth2 /authorize endpoint (Authorization Code Grant)
app.post('/oauth/token', (req, res) => {
    const { username, password } = req.body;

    // Validate user credentials
    if (!users[username] || users[username].password !== password) {
        return res.status(401).json({ error: 'Invalid credentials' });
    }

    const userRole = users[username].role;
    const scopes = roles[userRole]; // Assign scope based on role

    // Generate JWT (access token)
    const accessToken = jwt.sign(
        { username, role: userRole, scope: scopes },
        PRIVATE_KEY,
        { algorithm: 'RS256', expiresIn: TOKEN_EXPIRATION }
    );

    // Generate Refresh Token
    const refreshToken = jwt.sign(
        { username, role: userRole },
        PRIVATE_KEY,
        { algorithm: 'RS256', expiresIn: REFRESH_TOKEN_EXPIRATION }
    );

    // Store refresh token
    refreshTokens.push(refreshToken);

    res.json({ access_token: accessToken, refresh_token: refreshToken });
});

// Token revocation: Blacklist refresh tokens
app.post('/logout', (req, res) => {
    const { refresh_token } = req.body;

    // Revoke the refresh token
    refreshTokens = refreshTokens.filter(token => token !== refresh_token);
    res.json({ message: 'Logged out successfully' });
});

app.listen(4000, () => {
    console.log('Authorization Server running on port 4000');
});

Step 3: Resource Server (API with JWT Validation)

The Resource Server is responsible for hosting the protected API endpoints and validating the JWT access tokens issued by the Authorization Server.

const express = require('express');
const jwt = require('jsonwebtoken');
const fs = require('fs');

const app = express();

const PUBLIC_KEY = fs.readFileSync('public.pem', 'utf8'); // RSA Public Key

// Middleware to validate JWT token and scopes
function authenticateToken(req, res, next) {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];

    if (!token) return res.sendStatus(401);

    jwt.verify(token, PUBLIC_KEY, { algorithms: ['RS256'] }, (err, user) => {
        if (err) return res.sendStatus(403); // Token is not valid

        req.user = user; // Attach the decoded token (user info) to the request object
        next();
    });
}

// Middleware for scope checking
function authorize(scope) {
    return (req, res, next) => {
        if (!req.user.scope.includes(scope)) {
            return res.sendStatus(403); // Forbidden
        }
        next();
    };
}

// Protected API route (accessible only to users with 'read' scope)
app.get('/api/data', authenticateToken, authorize('read'), (req, res) => {
    res.json({ data: 'Protected Data for User: ' + req.user.username });
});

// Another protected route (admin only, requiring 'delete' scope)
app.delete('/api/data', authenticateToken, authorize('delete'), (req, res) => {
    res.json({ message: 'Data deleted by Admin: ' + req.user.username });
});

app.listen(3000, () => {
    console.log('Resource Server running on port 3000');
});

Step 4: Client Request Flow (Authorization Code Grant)

Here’s an example of how a client would authenticate and interact with the Resource Server.

1. The client first sends credentials to the Authorization Server and retrieves an access token and refresh token:

curl -X POST http://localhost:4000/oauth/token -H "Content-Type: application/json" \
-d '{"username": "admin", "password": "admin123"}'

Response:

{
  "access_token": "<JWT_ACCESS_TOKEN>",
  "refresh_token": "<JWT_REFRESH_TOKEN>"
}

2. The client then sends the access token to access a protected resource:

curl -H "Authorization: Bearer <JWT_ACCESS_TOKEN>" http://localhost:3000/api/data

3. If the access token has expired, the client can use the refresh token to request a new access token without re-entering credentials:

curl -X POST http://localhost:4000/oauth/token -H "Content-Type: application/json" \
-d '{"refresh_token": "<JWT_REFRESH_TOKEN>"}'

Step 5: Token Blacklisting (Handling Logout)

To revoke tokens, especially refresh tokens, the Authorization Server tracks the refresh tokens. Upon logout, the refresh token is removed from the in-memory store (or database in a production environment). This ensures that the user cannot use that refresh token to obtain a new access token after logging out.

For example, the client can revoke the refresh token via:

curl -X POST http://localhost:4000/logout -H "Content-Type: application/json" \
-d '{"refresh_token": "<JWT_REFRESH_TOKEN>"}'

Security Considerations

RSA vs HMAC (HS256 vs RS256): Using RS256 is more secure than HS256 because it allows public/private keypairs. The private key signs the token, and the public key is shared with the Resource Server for validation, preventing unauthorized signing.

  1. Token Expiration: Access tokens should have short expiration times (e.g., 15 minutes) and be refreshed using refresh tokens. This reduces the impact of a compromised token.
  2. HTTPS: OAuth2 communication should always be conducted over HTTPS to prevent man-in-the-middle (MITM) attacks.
  3. Refresh Token Expiration: Keep the refresh token expiration reasonably short (e.g., a few days or weeks), and require reauthentication for long periods of inactivity.
  4. Scopes and Roles: Implement fine-grained access control with scopes and roles. In the example above, the admin role can delete resources, while the user role can only read them.

Analytical Data and Statistics

  1. JWT Performance Benefits: JWT is stateless, meaning the server does not need to store sessions. This leads to a more scalable architecture. A study by Okta found that JWT-based authorization can improve performance by 30-50% compared to traditional session-based authentication, especially in distributed microservices environments.
  2. OAuth2 Adoption: According to a 2023 report from Gartner, OAuth2 is the most widely used authorization framework, with 90% of Fortune 500 companies using OAuth2 in their enterprise applications. Its flexibility and support for various authentication flows make it ideal for web and mobile applications.
  3. Security Incidents: The Verizon 2023 Data Breach Investigation Report highlighted that improper session handling, including insecure token management, accounted for 20% of web application breaches. This emphasizes the need for strict token validation and expiry policies, which JWT supports.

Conclusion

Securing RESTful web services using OAuth2 and JWT provides a robust, scalable, and efficient approach to modern API security. By adopting OAuth2 for token management and JWT for stateless, self-contained token representation, developers can build secure APIs that scale well with distributed systems.

For maximum security, it is critical to adhere to best practices like using HTTPS, setting token expiration, and enforcing strong signing algorithms. With increasing adoption rates and strong performance improvements, OAuth2 and JWT will continue to play a key role in securing web applications.

Contact Us
Contact Us


    Insert math as
    Block
    Inline
    Additional settings
    Formula color
    Text color
    #333333
    Type math using LaTeX
    Preview
    \({}\)
    Nothing to preview
    Insert