DEV Community

Cover image for Understanding OAuth2 Flow with a Complete Java Servlet Demo (Step-by-Step)
Sanjay Ghosh
Sanjay Ghosh

Posted on

Understanding OAuth2 Flow with a Complete Java Servlet Demo (Step-by-Step)

OAuth2 is everywhere.

  • “Login with Google”
  • “Continue with GitHub”
  • “Sign in with Microsoft”

We use it daily—but when it comes to explaining how it actually works, things quickly get confusing.

Most tutorials either:

  • Explain only the theory ❌
  • Or show isolated code without context ❌

Very few connect the full flow end-to-end.

🎯 What This Article Does Differently

In this article, we will:

  • Break down the 4 core actors
  • Walk through the entire OAuth2 flow
  • Map each step to working Java servlet code
  • Build a complete runnable demo

🧠 The Key Idea (Read This First)

OAuth2 is not about authentication.

It is about delegating access.

Instead of giving your username/password to an application, you allow it to access your data using a token issued by a trusted server.

🔷 Actors in OAuth2

This framework involves four key roles:

1. Resource Owner (User)

The user who owns the data
Grants or denies access

2. Client (Application)

The application requesting access to user data

3. Authorization Server

Authenticates the user
Issues authorization code and access token

4. Resource Server

Hosts protected data
Validates access tokens before responding

🔷 Real-World Example

A common example is “Login with Google”.

User → You
Client → Airbnb
Authorization Server → Google
Resource Server → Google APIs

Flow:

  1. You click “Login with Google”
  2. You are redirected to Google
  3. You login and click Allow
  4. Google sends a **code **to the client
  5. Client exchanges code for token
  6. Client fetches your data using the token

👉 This is exactly what we will implement.

🔷 High-Level Flow

  1. Client requests authorization from the user
  2. User authenticates and grants/denies access
  3. Authorization Server returns an authorization code
  4. Client exchanges code for access token
  5. Client uses access token to call Resource Server
  6. Resource Server validates token and returns data

🔷 Endpoints in This Demo

Endpoint Role
/login Starts OAuth flow (Client)
/authorize User login + consent (Authorization Server)
/callback Receives authorization code (Client)
/getToken Helper to call token endpoint
/token Issues access token
/data Protected API (Resource Server)

🔷 Full Flow Overview

/login
→ redirect to /authorize
→ user login + allow
→ /callback (gets code)
→ /token (gets access token)
→ /data (uses token)

🔷 Step-by-Step Flow with Code

Full implementation is available in the GitHub repository at the end of this article.
🟢 Step 1: /login → Start Flow

LoginServlet.java

package com.oauth.demo;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * Servlet implementation class LoginServlet
 */
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {

        String redirect = "http://172.21.236.75:9090/oauth/authorize?" +
                "response_type=code&client_id=test&redirect_uri=http://172.21.236.75:9090/oauth/callback";

        resp.sendRedirect(redirect);
    }
}

Enter fullscreen mode Exit fullscreen mode

This is the entry point. It redirects the user to the Authorization Server.

resp.sendRedirect("http://host/oauth/authorize?...");

👉 This is how the Client requests authorization from the User.

🟢 Step 2: /authorize → User Login & Consent

AuthorizeServlet.java

  • Displays login form
  • Accepts username/password
  • Allows user to Allow **/ **Deny
package com.oauth.demo;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * Servlet implementation class AuthorizeServlet
 */
@WebServlet("/authorize")
public class AuthorizeServlet extends HttpServlet {

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        resp.setContentType("text/html");
        resp.getWriter().write(
            "<form method='post'>" +
            "User: <input name='username'/><br/>" +
            "Pass: <input name='password' type='password'/><br/>" +
            "<button name='action' value='allow'>Allow</button>" +
            "<button name='action' value='deny'>Deny</button>" +
            "<input type='hidden' name='redirect_uri' value='" + req.getParameter("redirect_uri") + "'/>" +
            "</form>"
        );
    }

    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {

        String action = req.getParameter("action");
        String redirectUri = req.getParameter("redirect_uri");

        if ("deny".equals(action)) {
            resp.sendRedirect(redirectUri + "?error=access_denied");
            return;
        }

        String user = req.getParameter("username");
        String pass = req.getParameter("password");

        if ("admin".equals(user) && "password".equals(pass)) {
            String code = "abc123"; // normally random + stored
            resp.sendRedirect(redirectUri + "?code=" + code);
        } else {
            resp.getWriter().write("Invalid login");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

👉 Output:

Success → ?code=abc123
Failure → ?error=access_denied

🟢 Step 3: /callback → Client Receives Code

CallbackServlet.java

package com.oauth.demo;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * Servlet implementation class CallbackServlet
 */
@WebServlet("/callback")
public class CallbackServlet extends HttpServlet {

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {

        String code = req.getParameter("code");

        if (code == null) {
            resp.getWriter().write("Authorization failed");
            return;
        }
        resp.setContentType("text/html");
        resp.getWriter().write(
            "<h3>Authorization Code: " + code + "</h3>" +
            "<a href='/oauth/getToken?code=" + code + "'>Get Access Token</a>"
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

👉 This is the authorization code, a temporary credential.

🟢 Step 4: /getToken → Prepare Token Request

GetTokenServlet.java

Creates a form to call /token:

package com.oauth.demo;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * Servlet implementation class GetTokenServlet
 */
@WebServlet("/getToken")
public class GetTokenServlet extends HttpServlet {

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {

        String code = req.getParameter("code");
        resp.setContentType("text/html");
        resp.getWriter().write(
            "<form method='post' action='/oauth/token'>" +
            "<input name='code' value='" + code + "'/><br/>" +
            "<input name='client_id' value='test'/><br/>" +
            "<input name='client_secret' value='secret123'/><br/>" +
            "<button type='submit'>Exchange Code for Token</button>" +
            "</form>"
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

👉 In real systems, this happens server-to-server, not via UI.

🟢 Step 5: /token → Exchange Code for Token

TokenServlet.java

package com.oauth.demo;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * Servlet implementation class TokenServlet
 */
@WebServlet("/token")
public class TokenServlet extends HttpServlet {

    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {

        String clientId = req.getParameter("client_id");
        String clientSecret = req.getParameter("client_secret");

        if (!"test".equals(clientId) || !"secret123".equals(clientSecret)) {
            resp.setStatus(401);
            resp.getWriter().write("Invalid client");
            return;
        }


        String code = req.getParameter("code");

        if (!"abc123".equals(code)) {
            resp.setStatus(400);
            resp.getWriter().write("Invalid code");
            return;
        }

        // Simple JWT-like token (replace with real JWT later)
        String token = "header.payload.signature";

        resp.setContentType("application/json");
        resp.getWriter().write("{\"access_token\":\"" + token + "\"}");
    }
}

Enter fullscreen mode Exit fullscreen mode

Returns:
{
"access_token": "header.payload.signature"
}
👉 This is the Access Token

🟢 Step 6: /data → Access Protected Resource

DataServlet.java
package com.oauth.demo;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * Servlet implementation class DataServlet
 */
@WebServlet("/data")
public class DataServlet extends HttpServlet {

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {

        String auth = req.getHeader("Authorization");

        if (auth == null || !auth.startsWith("Bearer ")) {
            resp.setStatus(401);
            resp.getWriter().write("Unauthorized");
            return;
        }

        String token = auth.substring(7);

        if (!"header.payload.signature".equals(token)) {
            resp.setStatus(403);
            resp.getWriter().write("Invalid token");
            return;
        }

        resp.getWriter().write("Secure Data for user: admin");
    }
}
Enter fullscreen mode Exit fullscreen mode

👉 Client must call:
Authorization: Bearer header.payload.signature

If valid:
Secure Data for user: admin

🔷 What is the Access Token in this Demo?

In OAuth2, the access token is just a string. The specification does not enforce any particular format.

However, in most real-world systems, the access token is implemented as a JWT (JSON Web Token).

In this demo, we return a simplified token:

header.payload.signature

This mimics the structure of a JWT.

👉 For a detailed explanation of how JWT works (structure, signing, validation), you can refer to my earlier article:

Understanding JWT Authentication in Java with Encryption and Decryption
(https://dev.to/sanjayghosh/understanding-jwt-authentication-in-java-with-encryption-and-decryption-2ig7)

In a production system:

The Authorization Server generates a signed JWT
The Resource Server validates the JWT before granting access

This bridges the gap between OAuth2 (authorization framework) and JWT (token format).

🔷 Why This Flow Exists

Why not just send username/password to the client?
Because:

  • Security risks ❌
  • Third-party access problems ❌
  • No control over permissions ❌

OAuth2 solves this using:

  • Tokens
  • Delegated access
  • Controlled permissions

🔷 Common Mistakes

  • ❌ Client handling user credentials
  • ❌ Confusing code vs token
  • ❌ Sending token in URL
  • ❌ Thinking client validates token
  • ❌ Skipping redirect-based flow ### 🔷 Important Notes

⚠️ 1. This is a simplified demo

  • Hardcoded values
  • No database
  • No real token security

⚠️ 2. Token is NOT a real JWT

header.payload.signature

👉 In real systems:

  • Token is a JWT
  • Signed and verified

⚠️ 3. Client Secret should NOT be exposed

In this demo:

  • Shown in UI ❌

In real OAuth:

  • Used only in backend communication ✅

⚠️ 4. Browser cannot send Authorization headers directly

Use:

  • curl
  • Postman
  • Java client

⚠️ 5. Resource Server must validate token

In real systems:

  • Verify signature
  • Check expiry
  • Validate issuer

🔷 Key Takeaways

  • OAuth2 is about delegation of access
  • Client never sees user password
  • Authorization Server issues tokens
  • Resource Server enforces access

Now that you understand the complete flow, let’s run it.

🔷 Try It Yourself

If you want to understand OAuth2 deeply, I highly recommend running this demo locally.

👉 GitHub Repository (complete working example with all servlets and configuration):

https://github.com/knowledgebase21st/Software-Engineering/tree/dev/security/OAUTH

Steps:

  1. Clone the repository
  2. Deploy it in Tomcat
  3. Open /login in your browser
  4. Follow the complete OAuth2 flow step by step

Hands-on practice will make the concepts much clearer than theory alone.
If you run into any issues while trying this locally, feel free to leave a comment—I’ll be happy to help.

🏁 Conclusion

OAuth2 may seem complex at first—but the core idea is simple:
👉 The client never gets your password
👉 The Authorization Server issues a token
👉 The Resource Server trusts that token

🔑 What You Built

You implemented:

  • Client
  • Authorization Server
  • Resource Server
  • Complete OAuth2 flow This is the same pattern used by Google and Microsoft.

🔗 OAuth2 + JWT

  • OAuth2 = how you get the token
  • JWT = what the token contains

⚠️ Production Note

Use tools like Keycloak or frameworks like Spring Boot for real systems.

Top comments (0)