Introduction And Lesson Overview

Welcome to the first lesson of our course on API Key Authentication & Security. In this lesson, you will learn the basics of generating and securely storing API keys — a fundamental part of building secure web applications and APIs. API keys are unique codes that allow applications or users to access certain features or data. They are widely used to control and monitor access to APIs, and they help keep your services safe from unauthorized use.

By the end of this lesson, you will understand how to generate strong, random API keys, how to store them securely in your database, and how to set up an endpoint for users to create their own API keys. You will also learn about best practices for managing API keys throughout their lifecycle. This knowledge will prepare you for the hands-on exercises that follow, where you will practice these skills in a real coding environment.

Understanding The API Key Model

Let's start by looking at the data model for API keys. In our example, we use JPA (Java Persistence API) with Hibernate to define the ApiKey model. This model represents how API keys are stored in the database. Here is the code for the model:

package com.codesignal.pastebin.model;

import jakarta.persistence.*;
import java.time.LocalDateTime;

@Entity
@Table(name = "api_keys")
public class ApiKey {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    
    @Column(nullable = false)
    private Integer userId;
    
    @Column(nullable = false)
    private String name;
    
    @Column(nullable = false, unique = true)
    private String keyHash;
    
    @Column(nullable = false)
    private Boolean isActive = true;
    
    @Column(nullable = false)
    private LocalDateTime expiresAt;
    
    @Column(nullable = false)
    private LocalDateTime createdAt = LocalDateTime.now();
    
    // Getters and setters
    public Integer getId() { return id; }
    public void setId(Integer id) { this.id = id; }
    
    public Integer getUserId() { return userId; }
    public void setUserId(Integer userId) { this.userId = userId; }
    
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    
    public String getKeyHash() { return keyHash; }
    public void setKeyHash(String keyHash) { this.keyHash = keyHash; }
    
    public Boolean getIsActive() { return isActive; }
    public void setIsActive(Boolean isActive) { this.isActive = isActive; }
    
    public LocalDateTime getExpiresAt() { return expiresAt; }
    public void setExpiresAt(LocalDateTime expiresAt) { this.expiresAt = expiresAt; }
    
    public LocalDateTime getCreatedAt() { return createdAt; }
    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
}

Each API key has several important properties. The id is a unique identifier for each key. The userId links the key to a specific user in your system using a foreign key relationship. The name allows users to label their keys for easy identification. The keyHash stores a hashed version of the API key, which is important for security (we will discuss this soon). The isActive field lets you enable or disable keys without deleting them, and expiresAt sets an expiration date for each key. This structure helps you manage API keys safely and efficiently.

Configuring Spring Security For Password Hashing

Before we can generate and hash API keys, we need to set up Spring Security's password hashing functionality. Spring Security provides a PasswordEncoder interface that handles secure hashing using algorithms like BCrypt. To use this in our application, we need to add the Spring Security dependency to our project (this is already included in the CodeSignal environment) and create a configuration class:

@Configuration
public class SecurityConfig {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

The BCryptPasswordEncoder uses the BCrypt hashing algorithm, which is specifically designed for password and key storage. BCrypt is a strong choice because it includes a built-in work factor (also called a cost parameter) that controls how computationally expensive the hashing process is. The default work factor is 10, which means the algorithm performs 2^10 (1,024) iterations. You can increase this value to make hashing slower and more secure:

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder(12);  // 2^12 = 4,096 iterations
}

A higher work factor provides better protection against brute-force attacks because each hashing attempt takes more time. However, it also means legitimate hash generation and verification will be slower. For most applications, a work factor between 10 and 12 provides a good balance between security and performance. As computers become faster over time, you may want to increase this value.

Once this bean is defined, Spring will automatically make it available for dependency injection throughout your application. This allows any controller or service to use the PasswordEncoder by simply declaring it as a constructor parameter or field.

Generating A Secure API Key

When creating an API key, it is important to make sure it is random and hard to guess. In our example, we use Java's SecureRandom class to generate a secure key. We also use Spring's PasswordEncoder that we configured in the previous section. Here is the relevant code:

import java.security.SecureRandom;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.HashMap;
import java.util.Map;

private final PasswordEncoder passwordEncoder;

private Map<String, String> generateApiKey() {
    // Generate 32 random bytes = 256 bits of security
    SecureRandom random = new SecureRandom();
    byte[] randomBytes = new byte[32];
    random.nextBytes(randomBytes);
    
    // Convert bytes to hexadecimal string
    StringBuilder hexString = new StringBuilder();
    for (byte b : randomBytes) {
        String hex = Integer.toHexString(0xff & b);
        if (hex.length() == 1) hexString.append('0');
        hexString.append(hex);
    }
    String key = hexString.toString();
    
    // Hash the key for secure storage
    String keyHash = passwordEncoder.encode(key);
    
    // Return formatted key with pastebin prefix
    String formattedKey = "pb_" + key;
    
    Map<String, String> result = new HashMap<>();
    result.put("key", formattedKey);
    result.put("keyHash", keyHash);
    return result;
}

Notice the private final PasswordEncoder passwordEncoder; field at the top. This is where Spring will inject the PasswordEncoder bean we configured earlier. When Spring creates an instance of our controller class, it will automatically provide the PasswordEncoder implementation through constructor injection (you'll see the full controller constructor later in this lesson). This is one of the key benefits of Spring's dependency injection system — you don't have to manually create or manage these dependencies.

The SecureRandom class generates 32 random bytes, which gives us 256 bits of entropy. This makes the key very difficult to guess or brute-force. We then convert these bytes to a hexadecimal string for easy storage and use. Adding a prefix like pb_ helps identify the key as belonging to your application, which can be useful for debugging or logging.

Hashing The API Key For Secure Storage

Storing raw API keys in your database is risky. If your database is ever compromised, attackers could use those keys to access your services. To prevent this, we hash the API key before saving it using the PasswordEncoder we configured earlier:

String keyHash = passwordEncoder.encode(key);

The passwordEncoder.encode() method uses BCrypt to transform the original key into a fixed-length string that cannot be reversed. BCrypt automatically handles salt generation (random data added to the input) and incorporates the work factor we configured. Even if someone gains access to your database, they cannot recover the original API key from the hash.

When a user presents an API key, you can use passwordEncoder.matches() to verify it against the stored hash:

boolean isValid = passwordEncoder.matches(providedKey, storedKeyHash);

This comparison is secure because BCrypt applies the same hashing process to the provided key and checks if it matches the stored hash. This is the same approach used for storing passwords securely in web applications.

Creating The API Endpoint For Key Generation

Now let's look at how we allow users to create new API keys. We use a Spring Boot controller that handles POST requests to /api/api-keys/create. This endpoint is protected by JWT authentication, which checks that the user is logged in and authorized. Here is the main part of the controller:

package com.codesignal.pastebin.controller;

import com.auth0.jwt.interfaces.DecodedJWT;
import com.codesignal.pastebin.model.ApiKey;
import com.codesignal.pastebin.model.User;
import com.codesignal.pastebin.repo.ApiKeyRepository;
import com.codesignal.pastebin.repo.UserRepository;
import com.codesignal.pastebin.util.ErrorResponse;
import com.codesignal.pastebin.util.JwtUtil;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;

import java.security.SecureRandom;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/api/api-keys")
public class ApiKeyController {
    
    private final ApiKeyRepository apiKeys;
    private final UserRepository users;
    private final JwtUtil jwt;
    private final PasswordEncoder passwordEncoder;
    
    public ApiKeyController(ApiKeyRepository apiKeys, UserRepository users, JwtUtil jwt, PasswordEncoder passwordEncoder) {
        this.apiKeys = apiKeys;
        this.users = users;
        this.jwt = jwt;
        this.passwordEncoder = passwordEncoder;
    }
    
    // JWT authentication middleware
    private AuthOutcome verifyToken(String authorizationHeader) {
        if (authorizationHeader == null || authorizationHeader.isBlank()) {
            return new AuthOutcome(null, error(HttpStatus.UNAUTHORIZED, "Missing authorization header"));
        }
        
        String token = authorizationHeader.startsWith("Bearer ") 
            ? authorizationHeader.substring(7) 
            : authorizationHeader;
        
        try {
            DecodedJWT decoded = jwt.verify(token);
            Integer userId = decoded.getClaim("userId").asInt();
            
            var userOpt = users.findById(userId);
            if (userOpt.isEmpty()) {
                return new AuthOutcome(null, error(HttpStatus.NOT_FOUND, "User not found"));
            }
            
            return new AuthOutcome(userOpt.get(), null);
        } catch (Exception e) {
            return new AuthOutcome(null, error(HttpStatus.UNAUTHORIZED, "Invalid token"));
        }
    }
    
    // Generate secure API key
    private Map<String, String> generateApiKey() {
        SecureRandom random = new SecureRandom();
        byte[] randomBytes = new byte[32];
        random.nextBytes(randomBytes);
        
        StringBuilder hexString = new StringBuilder();
        for (byte b : randomBytes) {
            String hex = Integer.toHexString(0xff & b);
            if (hex.length() == 1) hexString.append('0');
            hexString.append(hex);
        }
        String key = hexString.toString();
        
        String keyHash = passwordEncoder.encode(key);
        String formattedKey = "pb_" + key;
        
        Map<String, String> result = new HashMap<>();
        result.put("key", formattedKey);
        result.put("keyHash", keyHash);
        return result;
    }
    
    // Create new API key
    @PostMapping("/create")
    public ResponseEntity<?> createApiKey(
            @RequestHeader(value = "authorization", required = false) String authorization,
            @RequestBody CreateKeyRequest request) {
        var outcome = verifyToken(authorization);
        if (outcome.error() != null) return outcome.error();
        
        User user = outcome.user();
        String name = request.name();
        
        // Basic validation
        if (name == null || name.length() < 3) {
            return error(HttpStatus.BAD_REQUEST, "API key name must be at least 3 characters");
        }
        
        // Limit to 3 API keys per user
        long existingKeys = apiKeys.countByUserIdAndIsActiveTrue(user.getId());
        if (existingKeys >= 3) {
            return error(HttpStatus.BAD_REQUEST, "Maximum 3 API keys allowed per user");
        }
        
        // Generate secure API key
        Map<String, String> keyData = generateApiKey();
        
        // Set expiration to 1 year from now
        LocalDateTime expiresAt = LocalDateTime.now().plus(365, ChronoUnit.DAYS);
        
        // Save to database
        ApiKey apiKey = new ApiKey();
        apiKey.setUserId(user.getId());
        apiKey.setName(name);
        apiKey.setKeyHash(keyData.get("keyHash"));
        apiKey.setExpiresAt(expiresAt);
        
        apiKeys.save(apiKey);
        
        System.out.println("API key created: keyId=" + apiKey.getId() + 
                         ", userId=" + user.getId() + ", name=" + name);
        
        return ResponseEntity.ok(Map.of(
            "message", "API key created successfully",
            "apiKey", Map.of(
                "id", apiKey.getId(),
                "name", name,
                "key", keyData.get("key"),
                "expiresAt", expiresAt.toString()
            ),
            "warning", "Store this key securely. It will not be shown again."
        ));
    }
    
    private ResponseEntity<ErrorResponse> error(HttpStatus status, String detail) {
        return ResponseEntity.status(status).body(new ErrorResponse(detail));
    }
    
    private record AuthOutcome(User user, ResponseEntity<ErrorResponse> error) {}
    public record CreateKeyRequest(String name) {}
}

Notice how the controller's constructor receives PasswordEncoder passwordEncoder as a parameter. Spring automatically injects the PasswordEncoder bean we configured in our SecurityConfig class. This is constructor-based dependency injection, which is the recommended approach in Spring because it makes dependencies explicit and enables immutable fields (using final).

When a user requests a new API key, the server first checks that the request is valid and that the user does not already have too many active keys. It then generates a secure key, hashes it using the injected PasswordEncoder, and saves the hash in the database. The raw key is returned to the user only once, with a warning to store it securely. If the user loses the key, they will need to generate a new one.

A typical response from this endpoint might look like:

{
  "message": "API key created successfully",
  "apiKey": {
    "id": 7,
    "name": "My App Key",
    "key": "pb_4f3c2e1a...",
    "expiresAt": "2025-06-10T12:34:56.789"
  },
  "warning": "Store this key securely. It will not be shown again."
}
Best Practices In API Key Management

Managing API keys is not just about generating and storing them. You should also think about their lifecycle. Each key in our model has an expiresAt field, which means keys will automatically become invalid after a certain date. The isActive field allows you to deactivate keys without deleting them, which is useful if a key is compromised or is no longer needed.

Here are some tips for managing API keys safely:

  • Always hash API keys before storing them using Spring's PasswordEncoder with BCrypt.
  • Choose an appropriate BCrypt work factor based on your security requirements and performance constraints (typically 10-12).
  • Limit the number of active keys per user to reduce risk.
  • Set expiration dates for keys and encourage users to rotate them regularly.
  • Never log or display the raw API key after creation.
  • Encourage users to store their keys securely, such as in environment variables or secret managers.

Following these practices will help keep your application and your users safe.

Summary And Next Steps

In this lesson, you learned how to configure Spring Security's PasswordEncoder for secure hashing with BCrypt, design a secure API key model using JPA, generate strong random keys with SecureRandom, hash them for safe storage using dependency injection, and create an endpoint for users to generate their own API keys using Spring Boot. You also learned about best practices for managing API keys throughout their lifecycle.

You are now ready to move on to the hands-on practice exercises, where you will apply what you have learned in a real coding environment. Remember, on CodeSignal, all the necessary libraries are pre-installed, so you can focus on writing and testing your code. Great job reaching the end of this lesson! Keep up the good work, and let's continue building your skills in API key authentication and security.

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal