Get Started with Jetty, Java, and OAuth

avatar-moksamedia.jpg Andrew Hughes

Jetty is a small, highly-scalable Java-based web server and servlet engine. It supports HTTP/2, WebSockets, and many other protocols. It powers websites and frameworks, both large and small, such as Google AppEngine. Because it is an Eclipse project, its open source project is called Eclipse Jetty. it is standards compliant and open source, as well as commercially usable. It is the main alternative to Tomcat when hosting Java applications. Like you can with Tomcat, you can use Jetty both embedded and stand-alone.

By default, Spring Boot creates applications with embedded web servers, which means that the server is embedded within the application code itself, so you don’t have to run a separate web server to publish Java web applications. However, with a little configuration, you can also publish a WAR file to a separate Jetty or Tomcat servlet container (old-school application server style). Spring also uses Tomcat by default, but you can easily change this, as you’ll see.

In this tutorial, you will build a simple web service with Jetty embedded. After that, you will build the same web service in Spring Boot and Jetty. Finally, you’ll add JWT (JSON Web Token) authentication and authorization to the web service using method-level security with Okta as the OAuth/OIDC provider.

Install the Project Dependencies

You’ll need to install a few things before you get started.

Java 11: This project uses Java 11. If you don’t have Java 11, you can install OpenJDK. You can also install OpenJDK using Homebrew. SDKMAN is another great option for installing and managing Java SDKs.

HTTPie: This is a simple command-line utility for making HTTP requests. You’ll use this to test the REST application. Check out the installation instructions on their website.

Okta Developer Account: You’ll use Okta as an OAuth/OIDC provider to add JWT authentication and authorization to the application. Go to their website and sign up for one of their free developer accounts, if you haven’t already.

Gradle: This is an optional install. If you download the project for this tutorial from the repo, you can run the project using the Gradle wrapper and don’t need to install Gradle. . If you want to build the project from scratch, you’ll need to install Gradle.

Build a Simple Web Service With Java and Jetty

The first step in this tutorial is to build a simple web service using Java and Gradle. To accomplish this, you’ll be using the Gretty plugin for Gradle. Gretty makes it super easy to run web apps on embedded servlet containers using Gradle and supports both Tomcat and Jetty.

If you choose to download the project for this tutorial from the GitHub repo, just follow along for the next few steps while I explain how to build the project from scratch.

git clone https://github.com/oktadeveloper/okta-spring-boot-jetty-example.git

First, open a shell and navigate to an appropriate directory (or create one) where you’d like the project to reside. Initialize the project using the Gradle CLI.

mkdir jetty
cd jetty
gradle init --type=basic --dsl=groovy --project-name=JettyEmbedded

Edit the build.gradle file in the project root directory:

plugins {  
    id 'java'  
    id 'war'  
    id 'org.gretty' version '2.3.1'  
}  
  
repositories {  
    jcenter()  
}  
  
dependencies {  
    providedCompile 'javax.servlet:javax.servlet-api:3.1.0'  
}  
  
gretty {  
    contextPath = '/'  
}

I want to point out a few things here. Notice the org.gretty plugin in the plugins block. Also, notice the javax.servlet:javax.servlet-api dependency you added using providedCompile. This adds it to the compile classpath but doesn’t add it to the packaged war file, since this will be provided by the servlet container when it’s deployed. Finally, notice that the context path for the embedded servlet container has been set to root in the gretty block.

Now make the root directory for the Java files (src/main/java is the standard Java root folder, plus the com.okta.jettyembedded package):

mkdir -p src/main/java/com/okta/jettyembedded

Create a simple servlet:

src/main/java/com/okta/jettyembedded/Hello.java

package com.okta.jettyembedded;  
  
import javax.servlet.ServletException;  
import javax.servlet.annotation.WebServlet;  
import javax.servlet.http.HttpServlet;  
import javax.servlet.http.HttpServletRequest;  
import javax.servlet.http.HttpServletResponse;  
import java.io.IOException;  
  
@WebServlet(name = "HelloServlet", urlPatterns = {"hello"}, loadOnStartup = 1)  
public class Hello extends HttpServlet {  
      
    protected void doGet(HttpServletRequest request, HttpServletResponse response)  
        throws ServletException, IOException {  
        response.getWriter().print("Howdy");  
    }  
      
}

Now you can run the app:

gradlew apprun

To test it, open another shell window and use HTTPie:

http :8080/hello
HTTP/1.1 200 OK
Content-Length: 5
Date: Fri, 06 Sep 2019 20:23:40 GMT
Server: Jetty(9.2.26.v20180806)

Howdy

You’ve built a super simple web servlet. It doesn’t do much yet, but it works (hopefully). Notice the @WebServlet annotation in the Hello.java class. Here you can configure some of the servlet parameters, rather than in a web.xml file. Moving this configuration into the code makes it easier to build and maintain some of the servlet configuration.

Next, you’ll take a look at a more fully-featured web servlet.

Improve the Java Servlet with Add and Delete

Now, you will create a web application to track a list of hikes. It will demonstrate how to support POST and DELETE operations, as well as simple GETs and some simple error handling.

Create a new Java file:

src/main/java/com/okta/jettyembedded/HikesTodoServlet.java

package com.okta.jettyembedded;  
  
import java.io.IOException;  
import javax.servlet.annotation.WebServlet;  
import javax.servlet.http.HttpServlet;  
import javax.servlet.http.HttpServletRequest;  
import javax.servlet.http.HttpServletResponse;  
import java.util.ArrayList;  
import java.util.Arrays;
import java.util.List;
  
@WebServlet(name = "HikesTodoServlet", urlPatterns = {"hikes"}, loadOnStartup = 1)  
public class HikesTodoServlet extends HttpServlet {  
  
    // Not synchronized  
   private List<String> hikes = new ArrayList<>(Arrays.asList(
            "Wonderland Trail", "South Maroon Peak", "Tour du Mont Blanc",
            "Teton Crest Trail", "Everest Base Camp via Cho La Pass", "Kesugi Ridge"
    ));
      
    protected void doGet(HttpServletRequest request, HttpServletResponse response)  
        throws IOException {  
        response.getWriter().print(String.join("\n", this.hikes));  
    }  
  
    protected void doPost(HttpServletRequest request, HttpServletResponse response)  
        throws IOException {  
        String hike = request.getParameter("hike");  
        if (hike == null) {  
            response.setStatus(400);  
            response.getWriter().print("Param 'hike' cannot be null.");  
        }  
        else if (this.hikes.contains(hike)) {  
            response.setStatus(400);  
            response.getWriter().print("The hike '"+hike+"' already exists.");  
        }  
        else {  
            this.hikes.add(hike);  
            response.getWriter().print(String.join("\n", this.hikes));  
        }  
    }  
  
    protected void doDelete(HttpServletRequest request, HttpServletResponse response)  
        throws IOException {  
        String hike = request.getParameter("hike");  
        if (hike == null) {  
            response.setStatus(400);  
            response.getWriter().print("Param 'hike' cannot be null.");  
        }  
        else {  
            this.hikes.remove(hike);  
            response.getWriter().print(String.join("\n", this.hikes));  
        }  
    }  
      
}

Use Control-C to stop the server and restart it with gradle apprun.

GET the list of hikes:

http :8080/hikes
HTTP/1.1 200 OK
...

Wonderland Trail
South Maroon Peak
Tour du Mont Blanc
Teton Crest Trail
Everest Base Camp via Cho La Pass
Kesugi Ridge

POST a new hike:

http -f POST :8080/hikes hike="Pear Lake"
HTTP/1.1 200 OK
...

Wonderland Trail
South Maroon Peak
Tour du Mont Blanc
Teton Crest Trail
Everest Base Camp via Cho La Pass
Kesugi Ridge
Pear Lake

DELETE a hike:

http DELETE :8080/hikes hike=="South Maroon Peak"
HTTP/1.1 200 OK
...

Wonderland Trail
Tour du Mont Blanc
Teton Crest Trail
Everest Base Camp via Cho La Pass
Kesugi Ridge
Pear Lake

Now try to delete a hike that doesn’t exist, or send an empty value:

http DELETE :8080/hikes
HTTP/1.1 400 Bad Request
...

Param 'hike' cannot be null.

To deploy this to a live server, you could just deploy the project as is, using gradle apprun to run the app using the embedded Jetty server. You could also deploy it to an external Jetty server by building the war file using gradle war and copying the war file (found in build/libs) to your server.

NOTE: this is a very naive implementation of a REST service. It uses an in-memory ArrayList as a data source, which is not synchronized (and thus would run into threading problems in a real web servlet). For anything beyond the scope of this tutorial, you’d need to implement a database backend of some kind. For help on how to do this, see the example blog posts listed at the end of the tutorial. You would typically also add a PUT endpoint and assign each item an ID to use as an index so data can be updated, but that is beyond the scope of this tutorial.

So far things are going pretty great. In the next section, you will recreate the same Hikes ToDo app using Spring Boot, withJWT token auth to the app using Okta as an OAuth/OIDC provider.

Create an OIDC Application

Now let’s take a little field trip over to Okta and set things up for OAuth/OpenID Connect (OIDC). Together, they are a set of open standards for implementing secure authorization and authentication. In this tutorial, Okta will act as the identity provider, and your Spring Boot app will be the client.

You should have already signed up for a free developer account with Okta. Navigate to the developer dashboard at https://developer.okta.com. If this is your first time logging in, you may need to click the Admin button.

To configure JWT authentication and authorization, you need to create an OIDC application.

From the top menu, click on the Application button. Click the Add Application button.

Select application type Web.

Click Next.

Give the app a name. I named mine “Spring Boot Jetty”.

Under Login redirect URIs, add two new URIs:

  • https://oidcdebugger.com/debug
  • http://localhost:8080/login/oauth2/code/okta

Under Grant types allowed, check Implicit (Hybrid).

The rest of the default values will work.

Click Done.

Leave the page open or take note of the Client ID. You’ll need it in a bit when you generate a token.

NOTE: You will use the oidcdebugger.com redirect URI and implicit grant type to create an access token you can use from the command line with HTTPie. The second URI is the default redirect URI that Spring Security uses for Okta when using its OAuth login feature.

Create a Spring Boot Project with Jetty

To create the Spring Boot project, you will use Spring Initializr. Check out its GitHub project to see its code. Initializr has a nice web form for configuring and downloading Spring Boot starter projects, but for this project, you will use its REST API.

From a shell, execute the following command to download a zip file of the configured starter project.

http https://start.spring.io/starter.zip \
  javaVersion==11 \
  dependencies==web \
  language==java \
  type==gradle-project \
  name==SpringBootJetty \
  groupId==com.okta.springbootjetty \
  artifactId==SpringBootJetty \
  packageName==com.okta.springbootjetty -o SpringBootJetty.zip

Unzip the downloaded file and open the directory in the IDE of your choice.

First, modify the build.gradle file so the project uses a Jetty-embedded container, instead of the default Tomcat. Add the spring-boot-starter-jetty dependency and exclude the spring-boot-starter-tomcat dependency.

Change the build.gradle file to match the following:

plugins {
    id 'org.springframework.boot' version '2.2.0.RELEASE'
    id 'io.spring.dependency-management' version '1.0.8.RELEASE'
    id 'java'
}

group = 'com.okta.springbootjetty'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-jetty' 
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
}

test {
    useJUnitPlatform()
}


configurations {  
   compile.exclude module: "spring-boot-starter-tomcat"  
}

Now add a WebController.java file.

src/main/java/com/okta/springbootjetty/WebController.java

package com.okta.springbootjetty;  
  
import org.springframework.stereotype.Controller;  
import org.springframework.web.bind.annotation.*;  
  
import javax.servlet.http.HttpServletResponse;  
import java.util.ArrayList;  
import java.util.Arrays;
import java.util.List;
  
@Controller  
public class WebController {  
  
   private List<String> hikes = new ArrayList<>(Arrays.asList(
            "Wonderland Trail", "South Maroon Peak", "Tour du Mont Blanc",
            "Teton Crest Trail", "Everest Base Camp via Cho La Pass", "Kesugi Ridge"
    ));
  
    @GetMapping("/")  
    @ResponseBody  
    public String indexGet() {  
        return String.join("\n", this.hikes);  
    }  
  
    @PostMapping("/")  
    @ResponseBody  
    public String indexPost(@RequestParam String hike, HttpServletResponse response) {  
        if (hike == null) {  
            response.setStatus(400);  
            return "Param 'hike' cannot be null.";  
        }  
        else if (this.hikes.contains(hike)) {  
            response.setStatus(400);  
            return "The hike '"+hike+"' already exists.";  
        }  
        else {  
            this.hikes.add(hike);  
            return String.join("\n", this.hikes);  
        }  
    }  
  
    @DeleteMapping("/")  
    @ResponseBody  
    public String indexDelete(@RequestParam String hike, HttpServletResponse response) {  
        if (hike == null) {  
            response.setStatus(400);  
            return "Param 'hike' cannot be null.";  
        }  
        else {  
            this.hikes.remove(hike);  
            return String.join("\n", this.hikes);  
        }  
    }  
  
}

This controller recreates the same functionality found in the first Jetty Hikes ToDo app, but now with Spring Boot. You’ll notice Spring simplified some of the syntax. The @ResponseBody annotation tells Spring Boot the controller is returning the response body directly (as opposed to returning a template name). Also, notice the code uses dependency injection to get the HttpServletResponse as well as the request params.

Run the Spring Boot REST service (make sure your other service is stopped or you’ll get a port conflict error):

gradle bootRun

From a second shell window, GET the list of hikes (notice that there’s no /hikes path below).

http :8080

Also, try adding and deleting a new hike.

POST a new hike:

http -f POST :8080 hike="Pear Lake"

DELETE a hike:

http DELETE :8080 hike=="South Maroon Peak"

Deploy the Spring Boot Project

You now have a Spring Boot application that runs on an embedded Jetty container. To deploy it to a production server, build an executable jar file using gradle bootJar, copy this jar file to the server, and run it using java -jar <your jar file name>.jar. There’s no need for a separate web server since this jar contains an embedded Jetty web server.

NOTE: For a more old-school deployment to an application server with multiple separate applications on the same server, you need to build a war file. The Spring docs on how to do this are a great resource. Essentially you need to do two things: 1) add the war plugin to the project dependencies, and 2) change the Jetty or Tomcat dependency to providedRuntime so it’s not included in the packaged war. Then you build a war file and deploy it to the servlet web app path on the server.

Add OAuth/OIDC Login to the Spring Boot App

You signed up for Okta and created the OIDC application. Now it’s time to configure the Spring Boot app to use OAuth/OIDC for authentication and authorization.

First, add your Issuer URI to the src/main/resources/application.properties file. You need to replace {yourOktaUrl} with your actual Okta URL. If you go to https://developer.okta.com and navigate to API and Authorization Servers, you’ll see the Issuer URI for the default authorization server.

Issuer URI

okta.oauth2.issuer=https://{yourOktaUrl}/oauth2/default

While you’re in that file, add the client ID and client secret from the “Spring Boot Jetty” app you created earlier.

okta.oauth2.clientId={clientId}
okta.oauth2.clientSecret={clientSecret}

Next, you need to add the following dependency to your build.gradle file in the dependencies {} block.

implementation 'com.okta.spring:okta-spring-boot-starter:1.3.0'

This includes the Okta Spring Boot Starter, a nice project that simplifies the use of Okta authentication and authorization with Spring Boot. Take a look at the project page for more info.

You also need to update the SpringBootJettyApplication class to match the following:

package com.okta.springbootjetty;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@SpringBootApplication
public class SpringBootJettyApplication extends WebSecurityConfigurerAdapter {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootJettyApplication.class, args);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .anyRequest().permitAll()
                .and()
            .oauth2Login()
                .and()
            .oauth2ResourceServer().jwt();
    }

}

Protect Your DELETE and POST Endpoints

The configure(HttpSecurity http) method configures your Spring Boot app with OAuth 2.0 login, as an OAuth 2.0 resource server, and to permit all requests by default. You’ll use method-level security via the @PreAuthorize annotation to protect the DELETE and POST endpoints below.

Finally, add the @PreAuthorize("isAuthenticated") annotation to the indexPost() and indexDelete() methods of the WebController class.

package com.okta.springbootjetty;  
  
import org.springframework.security.access.prepost.PreAuthorize;  
import org.springframework.stereotype.Controller;  
import org.springframework.web.bind.annotation.*;  
  
import javax.servlet.http.HttpServletResponse;  
import java.util.ArrayList;  
import java.util.Arrays;
import java.util.List;  
  
@Controller  
public class WebController {  
  
   private List<String> hikes = new ArrayList<>(Arrays.asList(
            "Wonderland Trail", "South Maroon Peak", "Tour du Mont Blanc",
            "Teton Crest Trail", "Everest Base Camp via Cho La Pass", "Kesugi Ridge"
    ));
  
    @GetMapping("/")  
    @ResponseBody  
    public String indexGet() {  
        return String.join("\n", this.hikes);  
    }  
  
    @PreAuthorize("isAuthenticated")  // <- ***ADDED***
    @PostMapping("/")  
    @ResponseBody  
    public String indexPost(@RequestParam String hike, HttpServletResponse response) {  
        if (hike == null) {  
            response.setStatus(400);  
            return "Param 'hike' cannot be null.";  
        }  
        else if (this.hikes.contains(hike)) {  
            response.setStatus(400);  
            return "The hike '"+hike+"' already exists.";  
        }  
        else {  
            this.hikes.add(hike);  
            return String.join("\n", this.hikes);  
        }  
    }  
  
    @PreAuthorize("isAuthenticated")  // <- ***ADDED***
    @DeleteMapping("/")  
    @ResponseBody  
    public String indexDelete(@RequestParam String hike, HttpServletResponse response) {  
        if (hike == null) {  
            response.setStatus(400);  
            return "Param 'hike' cannot be null.";  
        }  
        else {  
            this.hikes.remove(hike);  
            return String.join("\n", this.hikes);  
        }  
    }  
  
}

You now have a protected web service. You can make GET requests, but you won’t be able to POST or DELETE. Restart your server and verify this with the following HTTPie commands.

http :8080
HTTP/1.1 200 OK
...

Wonderland Trail
South Maroon Peak
Tour du Mont Blanc
Teton Crest Trail
Everest Base Camp via Cho La Pass
Kesugi Ridge

http -f POST :8080 hike="Pear Lake"
HTTP/1.1 403 Forbidden
...

{
    "error": "Forbidden",
    "message": "Forbidden",
    "path": "/",
    "status": 403,
    "timestamp": "2019-09-07T16:13:59.474+0000"
}

Generate a JWT Using the OIDC Debugger

To access the protected endpoints, you need to generate a JWT. To do this, you can use the OIDC Debugger. You’ll need the Client ID from the OIDC app you created earlier, as well as your base Okta URI (the same as the base URI in the Issuer URI).

Open the OIDC Debugger.

Update the Authorization URI to: https://{yourOktaUri}/oauth2/default/v1/authorize

Update the Client ID to the Client ID from your OIDC application.

Put something in the State field. For the purposes of this tutorial, this can be anything. This value is used to help protect against cross-site forgery requests.

OIDC Debugger Configuration

Scroll down and click Send Request.

Copy the token to your clipboard and store it in a shell variable in the shell window you’re using to make requests.

TOKEN=eyJraWQiOiJIb05xb01mNE9jREltWnBGRnBINjZGTkFOM0J... 

Now try and POST a new hike and then remove it.

http -f POST :8080 hike="Pear Lake" "Authorization: Bearer $TOKEN"
HTTP/1.1 200 OK
...

Wonderland Trail
South Maroon Peak
Tour du Mont Blanc
Teton Crest Trail
Everest Base Camp via Cho La Pass
Kesugi Ridge
Pear Lake

http DELETE :8080 hike=="South Maroon Peak" "Authorization: Bearer $TOKEN"
HTTP/1.1 200 OK
...

Wonderland Trail
Tour du Mont Blanc
Teton Crest Trail
Everest Base Camp via Cho La Pass
Kesugi Ridge
Pear Lake

You configured this app to use Spring Security’s oauth2Login() as well. This means you can go to http://localhost:8080/login, click your issuer URL, and log in that way too.

Learn More about Java, Spring Boot, and Spring Security

That’s that. In this tutorial, you saw how to make a simple Java servlet service and run it with Jetty. You also saw how to recreate the same service in Spring Boot, configure it to use Jetty, and simplify your Java code. Finally, you saw how to use a free developer account from Okta to add OAuth/OIDC security to your Spring Boot app.

You can find the code for this tutorial on GitHub at oktadeveloper/okta-spring-boot-jetty-example.

Here are some related blog posts:

If you have any questions about this post, please add a comment below. For more awesome content, follow @oktadev on Twitter, like us on Facebook, or subscribe to our YouTube channel.