Back to OAuth2 & OpenID Connect
Q4Question 4 of 5

OAuth2 Authorization Code Flow and Why the Code Exchange Is Server-to-Server

Key Concepts
  • The Authorization Code flow is the recommended OAuth2 flow for web apps with a secure backend.
  • The browser only carries a short-lived, single-use authorization code: the real token is exchanged privately on the backend.
  • The backend exchanges the code plus its client secret at the token endpoint over a server-to-server TLS call.
  • The state parameter prevents CSRF on the redirect by linking the request to the response.
  • The code is useless without the client secret, so an intercepted code cannot be turned into a token.
  • The old Implicit flow returned tokens in the browser URL and is now deprecated in favor of Authorization Code plus PKCE.

Definition

The browser only carries a throwaway code; the real token is swapped privately on the backend using the client secret.
  • Authorization Code flow – the recommended OAuth2 flow for applications with a secure backend.
  • Authorization code – a short-lived, single-use code returned to the browser in the redirect; not a token.
  • Back-channel exchange – the backend sends the code plus its client_secret to the token endpoint over a server-to-server TLS call to receive the access token.
  • State parameter – a random value sent in the authorization request and verified on return to prevent CSRF.
  • Security property – the token never passes through the browser URL, history, referrer headers, or logs.

Rajesh wants to log into a partner site using his National Bank of India identity. The partner site sends him to the bank, where he logs in and approves. The bank hands him only a claim ticket: a code. The partner's back office then quietly walks up to the bank's counter, shows the claim ticket plus its own trusted staff badge (client secret), and only then receives the actual access pass. The valuable pass is exchanged privately at the counter, never carried openly through the public lobby where a pickpocket could grab it.

Understanding the Concepts

What is the Authorization Code flow?

The Authorization Code flow is the recommended OAuth2 flow for web applications that have a secure backend. It works in two phases: a front-channel redirect that carries only a throwaway code, and a back-channel exchange that privately swaps the code for the real token.

The key security property is that the access token never passes through the browser.

TEXT
Step 1: Redirect to Authorization Server
  GET /authorize?response_type=code
      &client_id=budgeting-app
      &redirect_uri=https://app.com/callback
      &scope=transactions:read
      &state=xyz-random-csrf-string

Step 2: User logs in and consents at the bank's page

Step 3: Authorization Server redirects back with a code
  302 Location: https://app.com/callback?code=AUTH_CODE_123&state=xyz-random-csrf-string

Step 4: Backend exchanges code for token (server-to-server)
  POST /token
  grant_type=authorization_code
  &code=AUTH_CODE_123
  &redirect_uri=https://app.com/callback
  &client_id=budgeting-app
  &client_secret=SECRET_456

Step 5: Authorization Server returns tokens
  {"access_token": "eyJhb...", "refresh_token": "...", "expires_in": 900}

How does the front-channel redirect work?

The client builds an authorization URL with required parameters and redirects the user's browser to it. The user authenticates at the Authorization Server's page (not the client's) and consents to the requested scopes.

The state parameter is a random value that the client verifies on return to prevent CSRF attacks on the redirect.

JAVA
package com.bank.oauth2;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;

public class AuthorizationUrlBuilder {

    private static final String AUTH_ENDPOINT = "https://auth.nationalbank.in/authorize";
    private static final String CLIENT_ID = "budgeting-app";
    private static final String REDIRECT_URI = "https://app.com/callback";

    public static String buildUrl() {
        String state = generateState();
        // Store state in session for verification on callback

        return AUTH_ENDPOINT
            + "?response_type=code"
            + "&client_id=" + CLIENT_ID
            + "&redirect_uri=" + URLEncoder.encode(REDIRECT_URI, StandardCharsets.UTF_8)
            + "&scope=" + URLEncoder.encode("transactions:read", StandardCharsets.UTF_8)
            + "&state=" + state;
    }

    private static String generateState() {
        byte[] bytes = new byte[24];
        new SecureRandom().nextBytes(bytes);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
    }

    public static void main(String[] args) {
        System.out.println(buildUrl());
    }
}
TEXT
https://auth.nationalbank.in/authorize?response_type=code&client_id=budgeting-app
  &redirect_uri=https%3A%2F%2Fapp.com%2Fcallback&scope=transactions%3Aread
  &state=kR7xP2qM9nTvWz3Y

Why does the code exchange happen server-to-server?

If the token were returned directly in the browser redirect (as the old Implicit flow did), it would appear in the URL, browser history, referrer headers, and logs, where it is easy to leak.

By returning only a single-use code and requiring the client secret to redeem it, even an attacker who intercepts the code cannot turn it into a token without the secret. The code expires in seconds and can be used only once.

JAVA
package com.bank.oauth2;

import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import java.util.Map;

public class TokenExchangeService {

    private static final String TOKEN_ENDPOINT = "https://auth.nationalbank.in/token";
    private static final String CLIENT_ID = "budgeting-app";
    private static final String CLIENT_SECRET = "SECRET_456";
    private static final String REDIRECT_URI = "https://app.com/callback";

    public Map<String, Object> exchangeCodeForToken(String authorizationCode) {
        RestTemplate restTemplate = new RestTemplate();

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        headers.setBasicAuth(CLIENT_ID, CLIENT_SECRET); // HTTP Basic auth

        MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
        body.add("grant_type", "authorization_code");
        body.add("code", authorizationCode);
        body.add("redirect_uri", REDIRECT_URI);

        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(body, headers);
        return restTemplate.postForObject(TOKEN_ENDPOINT, request, Map.class);
    }
}
TEXT
Server-to-server exchange:
POST https://auth.nationalbank.in/token
Authorization: Basic YnVkZ2V0aW5nLWFwcDpTRUNSRVRfNDU2
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=AUTH_CODE_123&redirect_uri=https://app.com/callback

Response: {"access_token": "eyJhb...", "refresh_token": "...", "expires_in": 900}

How does Spring Security handle this flow?

Spring Security's OAuth2 client support automates the entire flow. You configure the client registration, and Spring handles the redirect, state validation, code exchange, and token storage.

JAVA
package com.bank.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class OAuth2LoginConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated()
            )
            .oauth2Login(oauth2 -> oauth2
                .loginPage("/login")
            );
        return http.build();
    }
}
YAML
# application.yml
spring:
  security:
    oauth2:
      client:
        registration:
          national-bank:
            client-id: budgeting-app
            client-secret: SECRET_456
            scope: transactions:read
            authorization-grant-type: authorization_code
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
        provider:
          national-bank:
            authorization-uri: https://auth.nationalbank.in/authorize
            token-uri: https://auth.nationalbank.in/token

What is the state parameter and why is it important?

The state parameter is a random value the client generates, stores in the user's session, and sends in the authorization request. When the Authorization Server redirects back, the client verifies that the returned state matches the stored one.

Without state, an attacker could craft a redirect URL with their own authorization code and trick the victim into using it (CSRF on the redirect).

JAVA
package com.bank.oauth2;

public class StateValidation {

    public boolean validateCallback(String returnedState, String storedState) {
        if (storedState == null || !storedState.equals(returnedState)) {
            System.out.println("REJECTED: state mismatch (possible CSRF attack)");
            return false;
        }
        System.out.println("State validated. Proceeding with code exchange.");
        return true;
    }
}

How is this different from the Implicit flow?

The Implicit flow (now deprecated) returned the access token directly in the browser URL fragment (#access_token=...). This meant the token appeared in browser history, could be leaked via referrer headers, and was visible to any JavaScript on the page.

The Authorization Code flow fixes all of these by returning only a code in the URL and exchanging it privately.

| Aspect | Authorization Code | Implicit (deprecated) |
|---|---|---|
| Token location | Server-to-server (never in URL) | Browser URL fragment |
| Client secret | Required for exchange | Not used |
| Security | Code + secret required | Token in URL, easy to leak |
| Refresh token | Yes | No |
| Recommended | Yes | No (use Auth Code + PKCE) |

What are common mistakes?

The most common mistake is not validating the state parameter on the callback, which opens the flow to CSRF attacks.

Another mistake is registering a wildcard redirect_uri. The redirect URI must be an exact match to prevent an attacker from redirecting the code to their own server.

JAVA
// WRONG: no state validation
public void callback(String code) {
    // Directly exchange code without checking state
    tokenService.exchange(code); // CSRF vulnerable!
}

// CORRECT: validate state first
public void callback(String code, String state, HttpSession session) {
    String storedState = (String) session.getAttribute("oauth2_state");
    if (!state.equals(storedState)) {
        throw new SecurityException("State mismatch. Possible CSRF.");
    }
    tokenService.exchange(code);
}

What should a senior engineer know?

Senior engineers know that the code is single-use and short-lived (seconds to one minute). The Authorization Server must reject a code that has already been used or has expired. If a code is replayed, the server should revoke any tokens already issued with that code.

Senior engineers also know that the redirect_uri sent in the token exchange must exactly match the one sent in the authorization request. This prevents an attacker from swapping the redirect to their own server after the user consents.

Visual Overview

DIAGRAM
  Authorization Code Flow:
  ┌────────────┐                       ┌──────────────────┐
  │   Browser   │ ── 1. Redirect ──►   │  Authorization   │
  │  (Rajesh)   │    /authorize?       │  Server (Bank)   │
  │             │    response_type=    │                  │
  │             │    code&scope=...    │  2. Rajesh logs  │
  │             │    &state=xyz        │     in, consents │
  │             │                      │                  │
  │             │ ◄─ 3. 302 Redirect ─ │  ?code=ABC       │
  │             │    ?code=ABC         │  &state=xyz      │
  │             │    &state=xyz        │                  │
  └──────┬──────┘                      └────────┬─────────┘
         │                                      │
         │ code=ABC                              │
         ▼                                      │
  ┌─────────────┐  4. POST /token              │
  │   Backend   │ ── code + secret ────────────►│
  │  (App       │                               │
  │   Server)   │ ◄──── 5. access_token ────────│
  │             │       + refresh_token         │
  └─────────────┘                               │
                                                │
  KEY: Token NEVER passes through the browser!  │

Key Takeaway

The Authorization Code flow is the recommended OAuth2 flow for applications that have a secure backend. The user is redirected to the Authorization Server, logs in and consents, and the server redirects back to the application with a short-lived, single-use authorization code, not a token. The application's backend then exchanges that code, along with its client secret, directly with the token endpoint over a server-to-server call, and receives the access token, and usually a refresh token and ID token. The token never passes through the browser.

In the National Bank of India project, a partner investment site lets customers sign in with their bank identity. The flow is used precisely because the valuable access token must never travel through the browser URL, where it would leak into history, referrer headers, and logs. The browser only ever carries a throwaway code, and a random state parameter is verified on return to stop CSRF on the redirect.

The reason the exchange happens server-to-server is the heart of the flow's security. The backend redeems the code using the confidential client secret over TLS, so even if an attacker intercepts the code from the redirect, they cannot turn it into a token without the secret, and the code expires in seconds and can be used only once.

The old Implicit flow returned the token directly in the browser and is now deprecated. By separating the public front channel that carries only a code from the private back channel that carries the token, the Authorization Code flow gives a bank the strong, leak-resistant delegation it needs for third-party and partner logins.

Remember one sentence: the browser carries only a disposable code, and the real token is exchanged privately on the backend where the client secret proves the caller's identity.

---

Share
Use ← → arrow keys to navigate