Getting Started with the Authlete APIs in Java

Introduction

This short tutorial will guide you through the process of integrating a Java web application with the Authlete API.

Scenario

AuthleticGear, a bricks-and-mortar retailer, runs a loyalty program for its customers. Program members can log in to the loyalty program website to view their points balance, transactions, and redeem points for a cash transfer to a linked bank account. The loyalty program website comprises a Java web application backend that exposes a RESTful API via Eclipse Jersey and an HTML5/JavaScript front end.


Loyalty Program Web Application

The company will soon be launching an e-commerce website. The requirement for this initial proof-of-concept is for existing loyalty program members to be able to link their loyalty account to the e-commerce site and view their points balance on the e-commerce front page. Later, the production system will allow customers to use loyalty points towards purchases.


eCommerce Web Application

Note that this is a ‘first party’ integration - the loyalty program and e-commerce site are both run by the same company. Typically, in this kind of use case, we skip the step of explicitly asking the user if the client can access their data at the service. We will, however, require that the user logs in when linking their account, even if they already have an active session at the loyalty program website, as a positive confirmation that they do wish to link their account.

The e-commerce team has already implemented an OAuth 2.0 Client. Your mission, as a developer on the loyalty program team, is to implement the OAuth 2.0 Authorization Server and Resource Server roles in the loyalty program web application.

You understand the basics of OAuth 2.0, and the message flows between the customer’s browser (the ‘Resource Owner’s ‘User Agent’ in the diagram below), the e-commerce website (‘Client’) and the loyalty program website (‘Authorization Server’ and ‘Resource Server’):


OAuth Flow

  1. Initiate Authorization
    • A customer clicks ‘Link Account’ at the e-commerce website.
    • The e-commerce OAuth Client redirects the customer’s browser to the Loyalty Program Authorization Server, including client id and redirection URL as query parameters.
    • The loyalty program Authorization Server shows its login form.
    • The customer logs in to the loyalty program website as they normally would.
  2. Issue an Authorization Code to the Client
    • The Authorization Server redirects the customer’s browser to the e-commerce website with an authorization code.
  3. Handle the Client’s Request for the Access Token
    • The e-commerce OAuth Client sends the authorization code, client id and client secret to the Authorization Server.
    • The Authorization Server responds with an access token.
  4. Validate the Client’s Request to the Loyalty Program API
    • The e-commerce website includes the access token in its request for the customer’s points balance
    • The loyalty program Resource Server validates the access token and attaches the customer’s identity to the request on its way to the loyalty program API.

Note: the OAuth specifications, and the above diagram, differentiate between the Authorization Server, responsible for authenticating and authorizing users, and the Resource Server, responsible for handling API calls. Although the specification calls these out as two separate roles, a single application may fill both of them, as the loyalty program web app does in this tutorial.

It looks like a big job, but you have Authlete in your toolkit, so this will only take an hour or two!

Prerequisites

You will need:

  • Docker Desktop
  • A code editor - whichever is your preference
  • Basic knowledge of Java EE

Setup

The demo system is implemented as a pair of Docker containers, each of which holds a Java EE web application, one for the e-commerce website and another for the loyalty site.

Create a Docker Network

In step 3 of the above flow, the e-commerce OAuth client sends a request directly to the loyalty authorization server. We need to create a Docker network so that the e-commerce container can resolve the IP address of the loyalty container. Run the following command:

docker network create --driver bridge authlete-net

Start the E-Commerce Container

Start the e-commerce container with

docker run -d \
  --name authlete-ecommerce \
  --network authlete-net \
  --publish 8080:8080 \
  us-docker.pkg.dev/authlete/demo/java-getting-started-ecommerce:latest

You can modify the docker run arguments to suit your environment:

  • Tomcat is configured to listen on port 8080 inside the container. In the example above, Docker is publishing that port to port 8080 on the host. If port 8080 is already in use on your machine, you can specify --publish 12345:8080 to select a different host port. If you do so, you will need to change 8080 to the port you chose throughout the tutorial.

You can check the container logs to verify that the container started correctly and Tomcat is ready with:

docker logs authlete-ecommerce

You should see output ending with two lines similar to:

19-Mar-2022 22:24:15.466 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["http-nio-8080"]
19-Mar-2022 22:24:15.472 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in [3463] milliseconds

If there are no errors reported, but you don’t see those lines at the end of the output, Tomcat is still starting up. Allow a few seconds and try again.

You can pause the Docker container if necessary with:

docker pause authlete-ecommerce

When you are ready to continue the tutorial:

docker resume authlete-ecommerce

Start the Loyalty Container

The loyalty container exposes its source code in a local directory via a Docker bind mount. This means that you can edit the code on your machine (the Docker host) with your preferred source code editor or IDE.

You can create the source directory at any location on your machine. We’ll call this directory $SOURCE_ROOT in the tutorial.

Note that the source directory must exist before you can start the Docker container, and you must reference it in the --mount option when you start the loyalty container with docker run.

For example, to use /Users/jdoe/authlete_src as the source directory:

mkdir /Users/jdoe/authlete_src
docker run -d \
  --name authlete-loyalty \
  --network authlete-net \
  --publish 8081:8080 \
  --mount type=bind,source="/Users/jdoe/authlete_src",target=/mount \
  us-docker.pkg.dev/authlete/demo/java-getting-started-loyalty:latest

Once the container has started, if you examine the source directory with ls /Users/jdoe/authlete_src , the contents should include the loyalty subdirectory, containing the loyalty web application’s source code. The source directory includes a git repository, so you can easily checkout the code as it should be after each step of the tutorial if you need to.

Note that the loyalty container must listen on a different host port to the e-commerce container. In this tutorial, the e-commerce container will listen on port 8080 and the loyalty container on port 8081.

You can modify the docker run arguments as mentioned in the previous section to change the host port number.

Use the same commands as the e-commerce container to check the container logs, and to pause and resume the container, substituting the loyalty instance name, authlete-loyalty in place of the e-commerce instance name.

Explore the sample websites

Loyalty Program Website

Browse to http://localhost:8081/loyalty/. You’ll see the home page for the loyalty program, with some placeholder text and a login link. Click the link and login with one of the sets of credentials shown on the page. You will see an account overview, with a list of transactions. The loyalty program web application uses an in-memory database which is loaded afresh with sample transactions each time the application is started. You can click ‘Redeem Points’ to simulate redeeming loyalty points for cash transferred to a linked bank account. This feature lets you easily change the account balance while the system is live, so you can check that the balance is being fetched dynamically.


Loyalty Web Application

The only other action you can take is to logout, which takes you back to the home page.

E-Commerce Website

Open http://localhost:8080/ecommerce/ in your browser. This page is a simple mockup of a typical e-commerce website. The only functional element is the ‘Link my Loyalty account’ link. Click the link. If you are not already logged in to the loyalty site, you’ll be sent to the loyalty site to login. If you’re already logged in, or after you do so, you’ll see a 404 error for http://localhost:8081/loyalty/oauth/authorization, since the loyalty program does not yet support OAuth 2.0.


404 error

This tutorial contains everything you need to OAuth-enable the loyalty program web application, but feel free to examine the source code for the loyalty and e-commerce web applications at https://github.com/authlete/java-getting-started. Both are written in Java JDK 11 for Apache Tomcat 9.0.x. The following technologies are used in the apps:

  • Eclipse Jersey Java REST framework, version 2.34
  • Hibernate Java Persistence API (JPA) implementation, version 5.6.4.Final
  • H2 in-memory database engine, version 2.1.210
  • Apache Log4j logging framework, version 2.17.1
  • Java Server Pages (JSP) in the e-commerce website.
  • HTML5, JavaScript and CSS in the loyalty website.

The ecommerce team has implemented their OAuth Client and agreed on URLs for the loyalty authorization server, but it doesn’t yet exist. Let’s fix that!

Create an Authlete Account and Configure the Client Application

Sign up for your Authlete account and click through to the Service Owner Console. You’ll see a default Authlete API Service instance.


Authlete service list

Click the Service to see its details.


Authlete service details

The loyalty program is the ‘Service’ from Authlete’s point of view, and this is where you manage its Authlete configuration.

Make a note of the API Key and API Secret - you’ll need those in the next step.

Scroll down, and you’ll see a link to the Client Application Developer Console.


Authlete service details

Click the link, and log in using your API Key as the login ID and your API Secret as the password.

If you do not see a default Client Application, then click Create App to create one. The Client Application will hold the OAuth 2.0 configuration for the e-commerce site:


Authlete application list

Click the Application to see its details.


Authlete application details

Make a note of the Client ID and Client Secret - you’ll need those as well.

Now you’ll make a few changes in the default Client configuration to tailor it to this scenario.

Scroll down to the bottom of the page and click Edit.

The e-commerce application is a web application capable of maintaining the confidentiality of its credentials (see Client Types for more detail) - they are stored securely on the server, so change Application Type to WEB and Client Type to CONFIDENTIAL:


Edit the Authlete application

Now click the Authorization tab. In this scenario, the client is using the authorization code grant type, expecting a code parameter from the authorization server, so uncheck all of the Grant Types except AUTHORIZATION_CODE, and all of the Response Types except CODE.

Recall, in step 2 above, that, after authenticating the user, the Authorization Server redirects the user’s browser to the Client application. You need to add that redirect URI to the client configuration. Click Create Redirect URI, enter the client’s redirect URI:

http://localhost:8080/ecommerce/oauth

and click Create. Delete the mock redirect URI.


Edit the Authlete application

When retrieving an access token, the client expects to POST its credentials to the authorization server according to section 2.3 of RFC 6749, the OAuth 2.0 Authorization Framework (step 3 above). To configure this, scroll down to Token Endpoint and change Client Authentication Method to CLIENT_SECRET_POST. Scroll down to the bottom and click Update, then OK to save the configuration.

Restart the Client Application with its Credentials

The e-commerce app reads its credentials from environment variables, so you will need to shutdown the e-commerce container and restart it, passing in the client ID and secret you noted earlier.

First, stop and remove the e-commerce container:

docker stop authlete-ecommerce
docker rm authlete-ecommerce

Now, run it again, setting the CLIENT_ID and CLIENT_SECRET environment variables on the command line:

docker run -d \
  --name authlete-ecommerce \
  --network authlete-net \
  --publish 8080:8080 \
  --env CLIENT_ID="your_client_id" \
  --env CLIENT_SECRET="your_client_secret" \
  us-docker.pkg.dev/authlete/demo/java-getting-started-ecommerce:latest

Note: if you are running the applications with host ports other than the default 8080 and 8081, you will also need to update the e-commerce application’s configuration file. Create a file on your machine named oauthService.json with the following content: \

{
  "service_name": "Loyalty",
  "auth_uri": "http://localhost:8081/loyalty/oauth/authorization",
  "redirect_uri": "http://localhost:8080/ecommerce/oauth",
  "token_uri": "http://authlete-loyalty:8080/loyalty/oauth/token",
  "api_endpoint": "http://authlete-loyalty:8080/loyalty/api/currentCustomer",
  "query_params": {
    "prompt": "login"
  },
}

The auth_uri and redirect_uri endpoints are accessed by the browser running on your local machine (the Docker host). Change the port numbers in those two URLs to the host ports you are using for the loyalty container and ecommerce container respectively.

Do not change the port number in token_uri or api_endpoint - the e-commerce application accesses these endpoints directly via the Docker network.

Copy oauthService.json into the e-commerce app’s container:

docker cp oauthService.json authlete-ecommerce:/src/ecommerce/src/main/webapp/WEB-INF/oauthService.json

Finally, rebuild the e-commerce app:

docker exec -it authlete-ecommerce /run/rebuild.sh

The e-commerce app now has everything it needs as an OAuth 2.0 client. It’s time to focus on the loyalty app.

Configure the Service Credentials

Create a file, named authleteCredential.json, in $SOURCE_ROOT/loyalty/src/main/webapp/WEB-INF with the service owner credentials (API key and secret) you noted earlier:

{
  "api_key" : "<your api key>",
  "api_secret" : "<your api secret>"
}

Note that each credential must be enclosed in double quotes - these are part of the JSON file format.

Now it’s time to get coding!

Step 1: Initiate Authorization


OAuth step 1

In this step, you’ll create a servlet at http://localhost:8081/loyalty/oauth/authorization to process the incoming authorization request.

The servlet will simply forward the incoming authorization request to the Authlete Authorization API, and then act according to the response, returning a redirect to the login page.

Before You Start

As mentioned above, the source directory includes a git repository. Verify that the repository is in the correct starting state by running:

git status

You should see the following output:

On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean

If the repository is not on the main branch, run the following command to checkout the correct branch:

git checkout main

Create the Authorization Servlet

Create a new directory, oauth, in $SOURCE_ROOT/loyalty/src/main/java/com/authlete/simpleauth, then create a new Java source file, OAuthAuthorizationServlet.java, with this content:

package com.authlete.simpleauth.oauth;

import com.authlete.simpleauth.LoginUtils;
import com.authlete.simpleauth.UserAccount;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.MediaType;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@WebServlet("/oauth/authorization")
public class OAuthAuthorizationServlet extends HttpServlet {
    private static final Logger logger = LogManager.getLogger();
    private static final long serialVersionUID = 1L;
    private final ObjectMapper mapper = new ObjectMapper();

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
    }
}

The loyalty web application uses a servlet filter to redirect HTTP requests to the login page. The isPublicPage() method in the LoginUtils class determines which resources can be returned without requiring a user login. You must add the path to the OAuth servlet to its list of resources so that the servlet can control the login process.

Open $SOURCE_ROOT/loyalty/src/main/java/com/authlete/simpleauth/LoginUtils.java and replace the isPublicPage() method with the following code:

  public static boolean isPublicPage(HttpServletRequest request) {
    String requestUri = request.getRequestURI();
    String contextPath = request.getContextPath();

    // The login page, front page and CSS path are public
    return requestUri.equals(contextPath + "/login") ||
            requestUri.equals(contextPath + "/index.html") ||
            requestUri.equals(contextPath + "/") ||
            requestUri.startsWith(contextPath + "/css") ||
            requestUri.startsWith(contextPath + "/oauth/");
  }

Call the Authlete Authorization API

Return to the OAuthAuthorizationServlet.java file you created and add the following code as the body of the doGet() method:

        initiateAuthleteAuthorization(request, response);

Now add the following code to define that initiateAuthleteAuthorization() method in the same class:

    private void initiateAuthleteAuthorization(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // 1. Get a Jersey HTTP client
        Client client = OAuthUtils.getClient(getServletContext());

        // 2. We will wrap the incoming query string in a JSON object
        Map<String, Object> requestMap = Collections.singletonMap("parameters", request.getQueryString());

        // 3. Call the Authlete Authorization endpoint
        String url = "https://api.authlete.com/api/auth/authorization";

        logger.info("Sending API request to {}:\n{}", url, OAuthUtils.prettyPrint(requestMap));

        // 4. Make the API call, parsing the JSON response into a map
        Map<String, Object> authApiResponse = client.target(url)
                .request()
                .post(Entity.entity(requestMap, MediaType.APPLICATION_JSON_TYPE), new GenericType<>() {
                });

        logger.info("Received API response:\n{}", OAuthUtils.prettyPrint(authApiResponse));

        // 5. 'action' tells us what to do next, 'responseContent' is the payload we'll return
        String action = (String) authApiResponse.get("action");
        String responseContent = (String) authApiResponse.get("responseContent");

        // 6. Perform the action
        switch (action) {
            case "INTERACTION":
                List<Object> prompts = (List<Object>) authApiResponse.get("prompts");
                for (Object prompt : prompts) {
                    if (prompt.equals("LOGIN")) {
                        // 7. Prompt the user to login
                        request.getSession().setAttribute("authApiResponse", authApiResponse);
                        LoginUtils.redirectForLogin(request, response);
                        return;
                    }
                }
                break;

            // 8. Handle errors
            case "INTERNAL_SERVER_ERROR":
                OAuthUtils.setResponseBody(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, responseContent);
                return;

            case "BAD_REQUEST":
                OAuthUtils.setResponseBody(response, HttpServletResponse.SC_BAD_REQUEST, responseContent);
                return;
        }

        // 9. We should never get here!
        Map<String, String> errorResponse = Map.of(
            "error", "unexpected_error",
            "error_description", "Contact the service owner for details"
        );
        OAuthUtils.setResponseBody(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, mapper.writeValueAsString(errorResponse));
    }

Note - if you are using an IDE, it will likely show an error regarding the OAuthUtils class not being defined. We’ll add that class in a moment.

It looks like there’s a lot going on, but the process is actually quite straightforward. Following the comments in the method:

  1. The servlet will be interacting with the Authlete API via HTTP, so we get a Jersey HTTP client.

    Note: There is an Authlete Java SDK we could use, but this tutorial shows how to interact directly with the Authlete API so you get a clear understanding of the message flow.

  2. The Authlete API is expecting a JSON payload with the incoming query string in the parameters property, like this:

    {
      "parameters": "response_type=code&client_id=..."
    }
    

    Since we’re building a quick proof-of-concept, we create a Map with the required structure, rather than generating Java classes corresponding to the JSON structures.

  3. We are calling the Authlete Authorization endpoint at /auth/authorization to continue the OAuth flow.

  4. The servlet makes the API call, parsing the response into another Java Map.

  5. The response’s action property indicates what the servlet should do next, and responseContent holds data that the servlet will relay back to the client.

  6. OAuth is a very flexible protocol, and there are many possible actions that the service might need to take at this point, depending on the use case and service configuration. Click ‘Description’ in the Authlete Authorization endpoint documentation to see all of the possibilities.

    The main one we are concerned with right now is INTERACTION - this indicates that some kind of interaction with the user is required. Thinking back to the use case, at this point, the user should be prompted to login, regardless of whether they already have an active session at the loyalty program website. The code here handles that case by checking that there is a prompts property in the API response that contains a single LOGIN entry.

    If there is no prompts property, or it contains anything other than a single LOGIN entry, execution will flow to the error handler at the end of the method (9).

  7. The API response is saved in the session for later processing and the user is redirected to login. After logging in, the user will be redirected back to this servlet.

  8. The Authlete API may indicate errors via the action property, and the servlet must handle them accordingly. Notice that, in these cases, responseContent holds the payload for the error response.

  9. If action is not one of the expected values, or there was some other error, then a generic error is returned.

You might have noticed that the servlet references a couple of utility classes. Create OAuthUtils.java in the $SOURCE_ROOT/loyalty/src/main/java/com/authlete/simpleauth/oauth directory with the following content:

package com.authlete.simpleauth.oauth;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.MediaType;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;

public class OAuthUtils {
   private static final String AUTHLETE_CREDENTIAL_JSON = "/WEB-INF/authleteCredential.json";
   private static final ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);

   private static AuthleteCredential getAuthleteCredential(ServletContext context) throws IOException {
       ObjectMapper mapper = new ObjectMapper();
       InputStream inputStream = context.getResourceAsStream(AUTHLETE_CREDENTIAL_JSON);
       if (inputStream == null) {
           throw new FileNotFoundException(AUTHLETE_CREDENTIAL_JSON);
       }
       return mapper.readValue(inputStream, AuthleteCredential.class);
   }

   public static synchronized Client getClient(ServletContext context) throws IOException {
       Client client = (Client)context.getAttribute("authleteClient");
       if (client == null) {
           AuthleteCredential authleteCredential = getAuthleteCredential(context);
           client = ClientBuilder.newClient(new ClientConfig())
                   .register(HttpAuthenticationFeature.basic(authleteCredential.getApiKey(), authleteCredential.getApiSecret()));
           context.setAttribute("authleteClient", client);
       }

       return client;
   }

   static void setResponseBody(HttpServletResponse response, int statusCode, String responseContent) throws IOException {
       response.setStatus(statusCode);
       response.setContentType(MediaType.APPLICATION_JSON);
       response.setHeader("Cache-Control", "no-store");
       response.setHeader("Pragma", "no-cache");
       response.getWriter().print(responseContent);
   }

   public static String prettyPrint(Map<String, Object> requestMap) {
       try {
           return mapper.writeValueAsString(requestMap);
       } catch (JsonProcessingException e) {
           return null;
       }
   }
}

Looking at OAuthUtils.java:

  • getAuthleteCredential() loads the Authlete API key and secret from the JSON file you created earlier.
  • getClient(), the first time it runs, creates an HTTP client that authenticates to the Authlete API via HTTP Basic authentication using the Authlete API key and secret. The client is saved in the servlet context for future use.
  • setResponseBody() populates an HttpServletResponse object with a status code and JSON content.
  • prettyPrint() renders a Java Map as indented JSON.

Create AuthleteCredential.java in the same directory with the following content:

package com.authlete.simpleauth.oauth;

import com.fasterxml.jackson.annotation.JsonProperty;

public class AuthleteCredential {
  @JsonProperty("api_key")
  private String apiKey;

  @JsonProperty("api_secret")
  private String apiSecret;

  @JsonProperty("api_key")
  public String getApiKey() {
    return apiKey;
  }

  @JsonProperty("api_key")
  public void setApiKey(String apiKey) {
    this.apiKey = apiKey;
  }

  @JsonProperty("api_secret")
  public String getApiSecret() {
    return apiSecret;
  }

  @JsonProperty("api_secret")
  public void setApiSecret(String apiSecret) {
    this.apiSecret = apiSecret;
  }
}

Add a Stub for the Next Step

If you were to rebuild and run the app right now, you’d find that, after clicking ‘Link my Loyalty Account’ at the e-commerce site and logging in, you’d be prompted to log in again, and again, and again. Recall that the servlet saves the Authlete API response in the HTTP session. We need to check for its presence so that we can take the next step.

Back in OAuthAuthorizationServlet.java, replace the doGet() function with the code below.

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        Map<String, Object> authApiResponse = (Map<String, Object>) request.getSession().getAttribute("authApiResponse");
        request.getSession().removeAttribute("authApiResponse");
        logger.info("{} API response in the session", authApiResponse == null ? "No" : "Found an");
        if (authApiResponse == null) {
            initiateAuthleteAuthorization(request, response);
        } else {
            processAuthleteAuthorization(request, response, authApiResponse);
        }
    }

Now the servlet will try to retrieve an API response from the session, and only initiate the authentication process if it doesn’t find one. Otherwise, it moves to the next step of the flow.

Finally, add a stub for that next step to the OAuthAuthorizationServlet class:

    private void processAuthleteAuthorization(HttpServletRequest request, HttpServletResponse response, Map<String, Object> authApiResponse) throws IOException {
        // Not yet implemented!
        Map<String, String> errorResponse = Map.of(
            "error", "not_yet_implemented",
            "error_description", "This step is not yet implemented"
        );
        OAuthUtils.setResponseBody(response, HttpServletResponse.SC_NOT_IMPLEMENTED, mapper.writeValueAsString(errorResponse));
    }

Now we get to see our hard work in action!

Rebuild the Application

The Docker container includes a script to rebuild the applications and redeploy them into Tomcat. Save all your changes and run the script in a terminal:

docker exec -it authlete-loyalty /run/rebuild.sh

You’ll see the build output scroll past. If you have an error in your code, the build will halt and indicate the location of the error, for example:


Build error

Use the build output to locate the issue, correct it, and retry the build.

If all is well, you should see a successful build:


Build success

Test Your Changes

Browse to http://localhost:8080/ecommerce and click ‘Link my Loyalty Account’. You should be prompted to login. Once you’ve logged in, rather than the earlier 404 error, you’ll see the ‘not yet implemented’ error that you just added:

{"error": "not_yet_implemented", "error_description": "This step is not yet implemented"}

We did a lot in this step, but most of it was laying the foundation for the remainder of the OAuth flow. Let’s move on!

Troubleshooting

If you run into any issues, you can discard your changes so that the source is in its original state, or checkout the source as it should be at the end of step 1.

To discard your changes:

git restore .

To discard your changes and skip to the end of step 1:

git restore .
git checkout step-1

Step 2: Issue an Authorization Code to the Client


OAuth step 2

Looking back to the flow diagram, we’ve implemented step 1 of the OAuth 2.0 flow. Now the e-commerce app is expecting the loyalty program app to redirect the user’s browser back, with an OAuth 2.0 authorization code as a query parameter.

Our next task, then, is to make another Authlete API call, to obtain that authorization code, implementing step 2.

Call the Authlete Authorization API

Replace the stub implementation of the processAuthleteAuthorization() method in OAuthAuthorizationServlet.java with the following:

    private void processAuthleteAuthorization(HttpServletRequest request, HttpServletResponse response, Map<String, Object> authApiResponse) throws IOException {
        // 1. Create a Map to send in the Authlete API request
        Map<String, Object> requestMap = new HashMap<>();

        // 2. Copy the ticket from the last API response into the map
        requestMap.put("ticket", authApiResponse.get("ticket"));

        // 3. Verify that the user is actually logged in
        UserAccount authenticatedUser = LoginUtils.getAuthenticatedUser(request.getSession());
        if (authenticatedUser == null) {
            requestMap.put("reason", "NOT_LOGGED_IN");
            OAuthUtils.handleAuthleteApiCall(getServletContext(), response, "/auth/authorization/fail", requestMap);
            return;
        }

        // 4. Issue the code
        requestMap.put("subject", authenticatedUser.getUsername());
        OAuthUtils.handleAuthleteApiCall(getServletContext(), response, "/auth/authorization/issue", requestMap);
    }

Walking through the method:

  1. We create a map to send as the Authlete API request.

  2. Each authorization with Authlete is identified by a unique ticket value returned by the first call to the Authlete API. The service must include this in subsequent messages.

  3. We verify that the user has actually logged in successfully. Notice the call to the /auth/authorization/fail endpoint to signal the failure if they have not. We’ll cover the OAuthUtils.handleAuthleteApiCall() method in a moment.

  4. We add the authenticated user’s username to the request map as the subject entry and call the Authlete /auth/authorization/issue endpoint.

Depending on the use case, the servlet might perform much more processing here. For example, in this single-party proof-of-concept, the client does not pass a scope parameter in its request. In a third-party OAuth interaction, the client typically passes a list of permissions in the scope parameter and the servlet prompts the end user for their consent to the client receiving those permissions.

Refactor the Authorization Servlet

If you look at the documentation for /auth/authorization/issue, you’ll notice that, although it is performing a different operation, it behaves in a very similar way to the /auth/authorization endpoint. It returns a JSON object with an action and responseContent, with a similar set of possible values for action as before. This time, the servlet is expecting the LOCATION action, instructing it to return HTTP status 302 Found to the client, redirecting it to the location held in responseContent, which is a URL including the authorization code in the query string.

Rather than copying code from initiateAuthleteAuthorization() to processAuthleteAuthorization(), we can factor the common code out. Paste this static method into OAuthUtils:

    public static Map<String, Object> handleAuthleteApiCall(ServletContext context, HttpServletResponse response,
                                                            String api, Map<String, Object> requestMap) throws IOException {
        logger.debug("Calling API {} with params {}", api, OAuthUtils.prettyPrint(requestMap));

        Map<String, Object> responseMap = getClient(context).target(AUTHLETE_BASE + api)
                .request()
                .post(Entity.entity(requestMap, MediaType.APPLICATION_JSON_TYPE), new GenericType<>() {
                });

        logger.debug("Received API response {}", OAuthUtils.prettyPrint(responseMap));

        String action = (String)responseMap.get("action");
        String responseContent = (String)responseMap.get("responseContent");

        switch (action) {
            case "INTERNAL_SERVER_ERROR":
                setResponseBody(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, responseContent);
                return null;
            case "BAD_REQUEST":
                setResponseBody(response, HttpServletResponse.SC_BAD_REQUEST, responseContent);
                return null;
            case "LOCATION":
                response.setStatus(HttpServletResponse.SC_FOUND);
                response.setHeader("Location", responseContent);
                response.setHeader("Cache-Control", "no-store");
                response.setHeader("Pragma", "no-cache");
                return null;
        }

        return responseMap;
    }

As you can see, this method includes the error handling from initiateAuthleteAuthorization() and adds handling for the LOCATION action. Add the following static variables at the top of OAuthUtils:

    private static final String AUTHLETE_BASE = "https://api.authlete.com/api";
    private static final Logger logger = LogManager.getLogger();

Now return to OAuthAuthorizationServlet, and replace initiateAuthleteAuthorization() with this code:

    private void initiateAuthleteAuthorization(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // 1. Call the Authlete Authorization endpoint, wrapping the incoming query string in a JSON object
        Map<String, Object> authApiResponse = OAuthUtils.handleAuthleteApiCall(
                getServletContext(), response, "/auth/authorization",
                Collections.singletonMap("parameters", request.getQueryString()));

        // 2. handleAuthleteApiCall() returns null if it already returned a response to the client
        if (authApiResponse == null) {
            return;
        }

        // 3. Perform the action
        String action = (String)authApiResponse.get("action");
        if (action.equals("INTERACTION")) {
            List<Object> prompts = (List<Object>) authApiResponse.get("prompts");
            for (Object prompt : prompts) {
                if (prompt.equals("LOGIN")) {
                    request.getSession().setAttribute("authApiResponse", authApiResponse);
                    LoginUtils.redirectForLogin(request, response);
                    return;
                }
            }
        }

        // 4. We should never get here!
        Map<String, String> errorResponse = Map.of(
                "error", "unexpected_error",
                "error_description", "Contact the service owner for details"
        );
        OAuthUtils.setResponseBody(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, mapper.writeValueAsString(errorResponse));
    }

As you can see, we have factored out much of the ‘boilerplate’ code to OAuthUtils.handleAuthleteApiCall(). The remaining code is focused on the specifics of initiating authorization.

Rebuild the Application and Test Your Changes

Again, save all your changes, run the rebuild script to rebuild and redeploy the apps:

docker exec -it authlete-loyalty /run/rebuild.sh

Browse to http://localhost:8080/ecommerce and start the flow by clicking ‘Link my Loyalty Account’. Log in, and you’ll see that the client is trying to send a request to the loyalty program’s OAuth token endpoint, but it does not yet exist:


404 error

Now we’ll add a second servlet to the loyalty program website that will exchange the client’s authorization code for an access token.

Troubleshooting

If you run into any issues, you can discard your changes so that the source is at the end of step 1, or checkout the source as it should be at the end of step 2.

To discard your changes:

git restore .

To discard your changes and skip to the end of step 2:

git restore .
git checkout step-2

Step 3: Handle the Client’s Request for the Access Token


OAuth step 3

The authorization servlet’s work is done; now it’s time to create a servlet to handle the token request. Create a new Java source file, OAuthTokenServlet.java, in the same directory as OAuthAuthorizationServlet.java.

package com.authlete.simpleauth.oauth;

import com.fasterxml.jackson.databind.ObjectMapper;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Map;

@WebServlet("/oauth/token")
public class OAuthTokenServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;
    private final ObjectMapper mapper = new ObjectMapper();

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // 1. Call the /auth/token endpoint, passing the request body
        String body = new String(request.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
        Map<String, Object> authApiResponse =
                OAuthUtils.handleAuthleteApiCall(getServletContext(), response, "/auth/token",
                        Collections.singletonMap("parameters", body));

        // 2. handleAuthleteApiCall() returns null if it already returned a response to the client
        if (authApiResponse == null) {
            return;
        }

        // 3. Perform the action
        String action = (String)authApiResponse.get("action");
        if (action.equals("OK")) {
            String responseContent = (String)authApiResponse.get("responseContent");
            OAuthUtils.setResponseBody(response, HttpServletResponse.SC_OK, responseContent);
            return;
        }

        // 4. We should never get here!
        Map<String, String> errorResponse = Map.of(
                "error", "unexpected_error",
                "error_description", "Contact the service owner for details"
        );
        OAuthUtils.setResponseBody(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, mapper.writeValueAsString(errorResponse));
    }
}

This servlet is very similar to the initiateAuthleteAuthorization() method in the authorization servlet. In fact, the only real differences are:

  • It handles an HTTP POST rather than a GET
  • It calls /auth/token rather than /auth/authorization
  • It handles an OK action, rather than INTERACTION, by relaying responseContent back to the client with a 200 OK HTTP status

Looking at the documentation for the Authlete API Token Endpoint, we can see that /auth/token is expecting the request body, and will return a response with action set to either OK or one of three error values. We’ve seen two of the error codes before, but there’s a new one, INVALID_CLIENT. It’s straightforward to extend handleAuthleteApiCall in OAuthUtils.java to handle INVALID_CLIENT.

Replace the switch statement with this code:

        switch (action) {
            case "INTERNAL_SERVER_ERROR":
                setResponseBody(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, responseContent);
                return null;
            case "BAD_REQUEST":
            case "INVALID_CLIENT":
                setResponseBody(response, HttpServletResponse.SC_BAD_REQUEST, responseContent);
                return null;
            case "LOCATION":
                response.setStatus(HttpServletResponse.SC_FOUND);
                response.setHeader("Location", responseContent);
                response.setHeader("Cache-Control", "no-store");
                response.setHeader("Pragma", "no-cache");
                return null;
        }

As you can see, we handle INVALID_CLIENT in exactly the same way as BAD_REQUEST.

Rebuild the Application and Test Your Work

Run the rebuild script to rebuild and redeploy the apps:

docker exec -it authlete-loyalty /run/rebuild.sh

As before, browse to http://localhost:8080/ecommerce and start the flow by clicking ‘Link my Loyalty Account’. Log in, and, this time, you’ll see an error like this:


500 error

We implemented step 3 of the OAuth flow, so the client has an access token. Now the client is trying to call the loyalty program REST API endpoint http://authlete-loyalty:8080/loyalty/api/currentCustomer with that access token, but is receiving the HTML for the login page rather than the expected JSON response.

We must complete one final step to allow the e-commerce app to retrieve the end user’s loyalty program data.

Troubleshooting

If you run into any issues, you can discard your changes so that the source is at the end of step 2, or checkout the source as it should be at the end of step 3.

To discard your changes:

git restore .

To discard your changes and skip to the end of step 3:

git restore .
git checkout step-3

Step 4: OAuth-Enable the Loyalty Program API


OAuth step 4

The loyalty authorization server has successfully authorized the e-commerce app, issuing it an access token. Our last task is to extend the current loyalty program API to recognize the OAuth Authorization header on incoming API calls, implementing step 4 of the OAuth flow.

The existing loyalty program code implements a servlet filter to redirect the user to login when they try any URLs that are not public. Here is the code:

  @Override
  public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException,
      ServletException {
    HttpServletRequest request = (HttpServletRequest) req;
    HttpServletResponse response = (HttpServletResponse) resp;

    logger.info("Requested {} to {}", request.getMethod(), request.getRequestURL());

    // 1. If the request is for a public page, we can allow it to proceed
    if (LoginUtils.isPublicPage(request)) {
      logger.info("Allowing request for public page");
      chain.doFilter(request, response);
      return;
    }

    // 2. Try to retrieve user information from the HTTP session
    HttpSession session = request.getSession();
    UserAccount authenticatedUser = LoginUtils.getAuthenticatedUser(session);

    // 3. If there is an authenticated user we attach their username to the request and allow it to proceed
    if (authenticatedUser != null) {
      logger.info("Allowing request for authenticated user");
      chain.doFilter(new UserRequestWrapper(authenticatedUser.getUsername(), request), response);
      return;
    }

    // 4. There is no authenticated user - redirect for login
    logger.info("Redirecting for login");
    LoginUtils.redirectForLogin(request, response);
  }

It’s straightforward to follow the logic:

  1. If the requested page is ‘public’, we can allow the request to proceed. Only the login page, front page, CSS files, and OAuth servlets are public. All other URLs require the user to authenticate.

  2. When the user authenticates, the login servlet attaches an account object to the HTTP session. The filter attempts to retrieve the authenticated user’s account from the session.

  3. If there is an account, then the filter attaches the authenticated user’s username to the request as the user principal and allows it to proceed.

  4. Otherwise, the filter responds with a redirect to the login page.

API calls from the e-commerce website will include an Authorization header containing an access token. We can implement a new filter that runs before the existing login filter to validate the access token and attach the user’s account object to the HTTP session so that the login filter will allow the request to proceed.

Implement a new Servlet Filter for API Requests

Create another Java source file, OAuthFilter.java, in the com.authlete.simpleauth.oauth package with the following content:

package com.authlete.simpleauth.oauth;

import com.authlete.simpleauth.UserRequestWrapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;

@WebFilter(filterName="oauthFilter")
public class OAuthFilter implements Filter  {
    private static final String BEARER_SPACE = "Bearer ";
    private static final Logger logger = LogManager.getLogger();
    private final ObjectMapper mapper = new ObjectMapper();

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)req;
        HttpServletResponse response = (HttpServletResponse)resp;

        // 1. Is there an Authorization header with an access token?
        String authHeader = request.getHeader("Authorization");
        if (authHeader == null || !authHeader.startsWith(BEARER_SPACE)) {
            chain.doFilter(request, response);
            return;
        }

        // 2. Extract the bearer token from the header value and send it to the introspection endpoint.
        String token = authHeader.substring(BEARER_SPACE.length());
        logger.info("Found access token {} - validating it with Authlete", token);
        Map<String, Object> authApiResponse = OAuthUtils.handleAuthleteApiCall(req.getServletContext(), response,
                "/auth/introspection", Collections.singletonMap("token", token));

        if (authApiResponse == null) {
            // 3. There was an error calling the Authlete API. OAuthUtils.handleAuthleteApiCall has handled it.
            // Nothing more to do here.
            logger.error("Disallowing API request with access token {}", token);
            return;
        }

        // 4. Attach the username to the request and pass the request down the chain
        String action = (String)authApiResponse.get("action");
        if (action.equals("OK")) {
            String username = (String) authApiResponse.get("subject");
            logger.info("Allowing API request to {} for user {}", request.getRequestURI() , username);
            chain.doFilter(new UserRequestWrapper(username, request), response);
            return;
        }

        // 5. We should never get here!
        Map<String, String> errorResponse = Map.of(
                "error", "unexpected_error",
                "error_description", "Contact the service owner for details"
        );
        OAuthUtils.setResponseBody(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, mapper.writeValueAsString(errorResponse));
    }

    @Override
    public void destroy() {
    }
}

The filter’s logic is straightforward:

  1. The filter looks for an Authorization HTTP header with a value that starts with "Bearer " (note the space). If there isn’t such a header, then there’s nothing more to do here, and the request can pass on down the filter chain.

  2. The filter extracts the access token from the HTTP header and sends it to the Authlete API’s Introspection endpoint for validation. At this point, a real-world client would likely cache the introspection response to improve performance on subsequent API calls.

  3. If the action returned by Authlete indicates an error then OAuthUtils.handleAuthleteApiCall() will have already taken the appropriate action, so we can simply log the fact that we are disallowing access and return at this point.

  4. The response from the API includes the authenticated user’s username in the subject property. We can attach it to the request, and pass the request down the chain.

  5. If we reach this point, it means that the action was not handled. This should never happen!

In this simple first-party demo, we are not using OAuth scopes. A Resource Server validating API calls from a third-party OAuth client would receive a list of scopes associated with the token in the introspection response. The Resource Server would then verify that the HTTP method and URL were permitted by a scope associated with the token.

Add Error Handling for Possible Introspection Errors

Looking at the introspection endpoint’s documentation, we can see that, if the token is valid, the action will be OK, and the response will contain a subject property identifying the end user. There are also additional possible error values: UNAUTHORIZED and FORBIDDEN.

UNAUTHORIZED means that the access token was not recognized or has expired; FORBIDDEN means that the access token does not cover the required scopes or that the subject associated with the access token is different from the subject contained in the request. Neither of the FORBIDDEN situations can occur in this simple demo, but we include the error handling for completeness.

Both of these errors require the service to return the contents of responseContent to the client in the WWW-Authenticate HTTP header.

We need to add more error handling to OAuthUtils.handleAuthleteApiCall(). Replace the switch statement with this code:

        switch (action) {
            case "INTERNAL_SERVER_ERROR":
                setResponseBody(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, responseContent);
                return null;
            case "BAD_REQUEST":
            case "INVALID_CLIENT":
                setResponseBody(response, HttpServletResponse.SC_BAD_REQUEST, responseContent);
                return null;
            case "UNAUTHORIZED":
                setAuthenticateHeader(response, HttpServletResponse.SC_UNAUTHORIZED, responseContent);
                return null;
            case "FORBIDDEN":
                setAuthenticateHeader(response, HttpServletResponse.SC_FORBIDDEN, responseContent);
                return null;
            case "LOCATION":
                response.setStatus(HttpServletResponse.SC_FOUND);
                response.setHeader("Location", responseContent);
                response.setHeader("Cache-Control", "no-store");
                response.setHeader("Pragma", "no-cache");
                return null;
            default:
                break;
        }

Also, add the new utility function referenced above:

    private static void setAuthenticateHeader(HttpServletResponse response, int statusCode, String responseContent) {
        response.setStatus(statusCode);
        response.setHeader("WWW-Authenticate", responseContent);
        response.setHeader("Cache-Control", "no-store");
        response.setHeader("Pragma", "no-cache");
    }

Modify the Login Filter to Check the User Principal

The new OAuth filter sets the username in the HTTP request. We need to modify the existing login filter to allow those requests. Open $SOURCE_ROOT/loyalty/src/main/java/com/authlete/simpleauth/LoginFilter.java and add the following code between steps 1 and 2:

    // 1.5 Is there already a username attached to the request?
    Principal principal = request.getUserPrincipal();
    if (principal != null) {
      String username = principal.getName();
      if (username != null) {
        logger.info("Allowing request for user principal {}", username);
        chain.doFilter(request, response);
        return;
      }
    }

Add the Servlet Filter to the Filter Chain

The final task is to ensure that, for API requests, the OAuth filter is called before the login filter. In $SOURCE_ROOT/loyalty/src/main/webapp/WEB-INF/web.xml, insert this filter mapping before the mapping for the login filter:

  <filter-mapping>
    <filter-name>oauthFilter</filter-name>
    <url-pattern>/api/*</url-pattern>
  </filter-mapping>

Note that url-pattern is relative to the loyalty web application’s context root, /loyalty.

Rebuild the Application and Test Your Work

As before, run the rebuild script to rebuild and redeploy the apps:

docker exec -it authlete-loyalty /run/rebuild.sh

One last time, browse to http://localhost:8080/ecommerce. You will immediately see the user’s name and loyalty account points balance:


E-Commerce web application with loyalty points balance

You might be asking “Why wasn’t I prompted to login this time?”

When you logged in at the end of step 3, the e-commerce application received an access token and stored it in the HTTP session. When the browser retrieved the e-commerce app’s front page just now, that session was still active, so the ecommerce app used the access token to call the API, just as it did in step 3. This time, the loyalty app was able to validate the access token with Authlete and retrieve the username, so the API call succeeds.

Click Unlink my Loyalty Account, then click to link it again, and verify that you are prompted to login, as expected. Again, you will see the user’s name and loyalty account points balance.

Success – you’ve completed the OAuth proof-of-concept in record time!

Housekeeping

If you’re done with the Docker container for now, you can stop it:

docker stop authlete-loyalty

To start the container again:

docker start authlete-loyalty

To remove the container altogether:

docker rm authlete-loyalty

Troubleshooting

If you run into any issues, you can discard your changes so that the source is at the end of step 3, or checkout the source as it should be at the end of step 4.

To discard your changes:

git restore .

To discard your changes and skip to the end of step 4:

git restore .
git checkout step-4

Review

You added very little code to the loyalty program web application to enable it as an OAuth 2.0 authorization server and resource server, and most of that code was simply passing the client’s requests to the Authlete API and relaying the API’s responses back to the client.

Here’s the OAuth 2.0 flow in full again:


OAuth flow

Let’s review what Authlete is doing in each step of the flow:

Step 1: Initiate Authorization


OAuth step 1

In this initial step, the user clicks the link at the ecommerce site, which responds with an authorization request in the form of a redirect to the authorization server with a URL such as:

http://localhost:8081/loyalty/oauth/authorization?
response_type=code&
client_id=01234567890123&
redirect_uri=http://localhost:8080/ecommerce/oauth&
state=Loyalty&
prompt=login

(Line breaks added for clarity)

The loyalty app’s OAuth authorization servlet forwards the query string (everything after the ‘?’) to the Authlete /auth/authorization endpoint. Note that the servlet need not parse the query string or, indeed, have any knowledge of the OAuth parameters.

Authlete’s response includes a ticket value uniquely identifying this series of message exchanges, the action that the servlet should take - INTERACTION, and the form that the action should take - LOGIN.

The response indicates that the servlet’s responsibility is to authenticate the user and move on to the next step.

Step 2: Issue an Authorization Code to the Client


OAuth step 2

Now the servlet calls on Authlete to issue an authorization code. The servlet must ensure at this step that the user is indeed logged in.

If, for some reason, the servlet finds itself at this step and the user has not been authenticated, then the servlet calls Authlete’s /auth/authorization/fail endpoint with a reason of NOT_LOGGED_IN. In this case, Authlete will respond with an action of LOCATION and a URL containing the client’s redirect URL and error parameters indicating that the client requested that the user be logged in, but this did not take place.

If all is well, on the other hand, the servlet sends a request including the ticket issued in step 1 and the user’s username to Authlete’s /auth/authorization/issue endpoint. Authlete’s response again contains an action, in this case, LOCATION, instructing the servlet to return HTTP status 302 Found to the client, redirecting it to the location held in responseContent, a URL including the authorization code in the query string.

Again, notice that servlet is not concerned with the details of the OAuth protocol. It is simply an intermediary between the client and Authlete, acting according to each API response.

Step 3: Handle the Client’s Request for the Access Token


OAuth step 3

In step 3, the client directly POSTs the authorization code it obtained via the redirect in the previous step to the OAuth token servlet.

The token servlet simply sends the entire, unparsed, body of the request to the Authlete /auth/token endpoint. Again, the servlet need not have any knowledge of the content of the request, in this case, a form post containing an OAuth access token request, for example:

grant_type=authorization_code&
code=some_random_string_of_characters&
client_id=01234567890123&
client_secret=shared_secret_between_the_client_and_authorization_server&
redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fecommerce%2Foauth

(Line breaks added for clarity)

The token servlet acts on the response from the Authlete API, just as the authorization servlet did, in this case, returning a JSON payload to the client:

{
  "access_token": "another_random_string_of_characters",
  "scope": null,
  "token_type": "Bearer",
  "expires_in": 86400
}

(JSON formatted for clarity)

Step 4: Validate the Client’s Request to the Loyalty Program API


OAuth step 4

In this final step, we turn our attention to the OAuth servlet filter. This filter is configured to intercept all requests to the loyalty API endpoints. The filter extracts the access token from the incoming request and sends it to the Authlete /auth/introspection endpoint. Authlete validates the token and, if it is valid, returns a response with an action of OK and subject set to the user’s username. The filter then attaches the username to the request and passes it down the chain.

Again, if an error occurs, Authlete sets the appropriate action and responseContent and the loyalty app responds appropriately.

When the request reaches the login filter, it can allow the request to proceed based on the fact that the username is present. The loyalty API can return the relevant data based on the username in the request.

Conclusion

In about an hour, you took the existing loyalty program web application and, step-by-step, added OAuth Authorization Server and Resource Server capabilities. You didn’t have to parse any input from the OAuth client, the ecommerce website; neither did you have to implement any OAuth protocol or assemble any responses to the client.

By simply adding code to hand incoming OAuth requests to the Authlete API and act on the API’s responses, you OAuth-enabled the loyalty program web app, and completed the proof-of-concept in record time!