TL;DR: In this article, you will learn how to create and secure a jQuery SPA and a Spring Boot API using OAuth 2.0. You will start by scaffolding a new Spring Boot project. Then you will add some endpoints to it. After that, you will use Spring Security to secure the whole thing. Lastly, you will create a SPA (with jQuery) to consume the API. If needed, you can find the reference code developed throughout the article in this GitHub repository.

"Learn how to secure Spring Boot APIs and SPAs with OAuth 2.0."

Prerequisites

To follow this article along, you will need to have the following software installed in your local machine:

What is OAuth 2.0?

Nothing better then the official specification itself to teach you what OAuth 2.0 is:

OAuth 2.0 is an authorization framework that enables a third-party application to obtain limited access to an HTTP service, either on behalf of a resource owner (usually a user) or by allowing a third-party application to obtain access on its own behalf. - The OAuth 2.0 Authorization Framework

In other words, this protocol allows a user to grant limited access to their data on one app (web app, mobile app, etc.), to another app, without having to expose their credentials. If you don't know much about OAuth 2.0 and want to learn more, make sure you check out this resource.

OAuth 2.0 and Single-Page Apps: using the Implicit Grant

Since you are going to develop a demo application that is composed of a Single-Page App (SPA) that consumes resources from a Spring Boot API that is secured with OAuth 2.0, you will have to implement what is known as the OAuth 2.0 Implicit Grant. The Implicit Grant is an OAuth 2.0 flow specifically tailored for public SPAs clients that want to consume APIs. If you were developing a different kind of client (for example, a mobile app), you would have to choose another flow.

To learn more about the different flows and how to implement each one, take a look at this resource.

Securing Spring Boot APIs with OAuth 2.0

In this section, you will start from scratch, create a new Spring Boot API, secure it with OAuth 2.0, and then create a SPA to consume this API. However, before you can dive deep in the code, you will need an identity provider that implements the OAuth 2.0 protocol. For this demo application, you will use Auth0 and, for that, you'll need to sign up for a free Auth0 account here.

After signing up for your Auth0 account, you will need to create an API on Auth0 to represent your backend API and to be able to configure it to authenticate requests. To do this, head to the APIs section on your Auth0 dashboard and click on the Create API button. After that, the dashboard will show you a form where you will have to enter:

  • a name for your API (this can be something like "Spring Boot Implicit Flow");
  • an identifier (in this case, it can be http://localhost:8080/api or anything that resembles a valid URL);
  • and the signing algorithm (for this field, make sure you choose RS256).

Then, you can create your Auth0 API by clicking on the Create button.

After clicking on this button, the dashboard will redirect you to a section where you will find instructions on how to configure your backend. As this article will address everything related to the configuration, you can ignore this section and move to the Scopes section. There, you will register an OAuth scope:

  • Name: read:messages
  • Description: "Read messages"

After inserting the above values on the form, hit the Add button to save this new scope into your Auth0 API. With this in place, you are done with the configuration and can start working on your backend API.

Scaffolding Your Spring Boot API

In this section, you will create a new Spring Boot application that will serve as your API. This API will expose public and private endpoints. For starters, go to the Spring Initializr page and fill out the form like this:

  • Generate a: Gradle Project
  • with: Java
  • and Spring Boot: 2.0.x
  • Group: com.example
  • Artifact: spring-boot-oauth2

Then, on the Dependencies section, you will have to use the search box to include two libraries: Web and Security.

Generating a new Spring Boot project with Spring Initializr

After filling out the form, click on the Generate Project button to download your new application. When your browser finished downloading it, extract the contents of the downloaded file and import the project into your preferred IDE.

As you can see through your IDE, for now, your project contains only one class called SpringBootOauthApplication. If you open the build.gradle file, you will also see that your project defines four dependencies: spring-boot-starter-security, spring-boot-starter-web, spring-boot-starter-test, and spring-security-test. If you are using Java 10, you will have to update this file to include a library that handles Java to XML marshalling:

// ... leave everything else untouched ...

dependencies {
  // ... other dependencies ...
  compile('org.glassfish.jaxb:jaxb-runtime:2.3.1')
}

Defining Endpoints on Spring Boot

After scaffolding your Spring Boot project, you can focus on creating its first endpoints. To do so, you will create a new class that will define the following endpoints:

  • /api/public: This endpoint will be publicly accessible and will return a simple text message.
  • /api/private: This endpoint will only be accessible by users who have been authenticated.
  • /api/private-scoped: This endpoint will only be accessible by users who have been authenticated and that have granted a particular scope.
  • /config: This endpoint will be publicly accessible and will return some configuration properties that your SPA will use to authenticate users.

So, to define these endpoints, create a class called AppController inside the com.example.springbootoauth2 package and insert the following code into it:

package com.example.springbootoauth2;

import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class AppController {

  @Value("${security.oauth2.resource.id}")
  private String resourceId;

  @Value("${auth0.domain}")
  private String domain;

  @Value("${auth0.clientId}")
  private String clientId;

  @RequestMapping(value = "/api/public", method = RequestMethod.GET, produces = "application/json")
  @ResponseBody
  public String publicEndpoint() {
    return new JSONObject()
      .put("message", "Hello from a public endpoint! You don\'t need to be authenticated to see this.")
      .toString();
  }

  @RequestMapping(value = "/api/private", method = RequestMethod.GET, produces = "application/json")
  @ResponseBody
  public String privateEndpoint() {
    return new JSONObject()
      .put("message", "Hello from a private endpoint! You need to be authenticated to see this.")
      .toString();
  }

  @RequestMapping(value = "/api/private-scoped", method = RequestMethod.GET, produces = "application/json")
  @ResponseBody
  public String privateScopedEndpoint() {
    return new JSONObject()
      .put("message", "Hello from a private endpoint! You need to be authenticated and have a scope of read:messages to see this.")
      .toString();
  }

  @RequestMapping(value = "/config", method = RequestMethod.GET, produces = "application/json")
  @ResponseBody
  public String getAppConfigs() {
    return new JSONObject()
      .put("domain", domain)
      .put("clientID", clientId)
      .put("audience", resourceId)
      .toString();
  }
}

After inserting this code, your IDE will probably start yelling at you because it does not know where to find the org.json.JSONObject class. To solve this issue, add the following dependency to your build.gradle file:

// ... leave everything else untouched ...

dependencies {
  // ... other dependencies ...
  compile('org.json:json:20180813')
}

With that in place, the next thing you will need to do is to define the environment variables that the AppController class consumes. That is, if you take a close look at this class, you will see that it contains three String fields annotated with @Value:

  • resourceId: This will reference your Auth0 API identifier (e.g., http://localhost:8080/api).
  • domain: This will reference your Auth0 domain (e.g., blog-samples.auth0.com)
  • clientId: This will reference the Client ID of the Auth0 Application that you still need to create.

To define these environment variables, open the application.properties file and add the following content to it:

auth0.domain=<DOMAIN>
auth0.clientId=<CLIENT-ID>

security.oauth2.resource.id=<AUTH0-API-IDENTIFIER>

Just make sure you replace <DOMAIN> and <AUTH0-API-IDENTIFIER> with your own Auth0 values. Don't worry about the <CLIENT-ID> placeholder now, you will replace it later.

Securing the Spring Boot API with OAuth 2.0

Now that you have defined your endpoints, it is time to secure your API using OAuth 2.0. To do so, you are going to import another library provided by Spring that will facilitate the configuration of everything. So, open the gradle.build file and add the following dependency to it:

// ... leave everything else untouched ...

dependencies {
  // ... other dependencies ...
  compile('org.springframework.security.oauth.boot:spring-security-oauth2-autoconfigure:2.0.6.RELEASE')
}

Then, to secure your API, create a new class called SecurityConfig inside the com.example.springbootoauth2 package and add the following code to it:

package com.example.springbootoauth2;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;

@Configuration
@EnableResourceServer
public class SecurityConfig extends ResourceServerConfigurerAdapter {
  @Value("${security.oauth2.resource.id}")
  private String resourceId;

  @Override
  public void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
      .mvcMatchers("/api/public").permitAll()
      .antMatchers("/api/private-scoped").access("#oauth2.hasScope('read:messages')")
      .mvcMatchers("/api/**").authenticated()
      .anyRequest().permitAll();
  }

  @Override
  public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
    resources.resourceId(resourceId);
  }
}

As you can see, you annotated this class with @EnableResourceServer. This annotation is a convenient feature for resource servers secured with OAuth 2.0 as it automatically enables a Spring Security filter that authenticates requests via an incoming OAuth 2.0 token. Besides this annotation, this class contains the following methods:

  • configure(ResourceServerSecurityConfigurer resources), used to set the identifier of your API (e.g., http://localhost:8080/api);
  • configure(HttpSecurity http), used to specify which API endpoints are secured (in this case, mvcMatchers("/api/**").authenticated()), which are secured and require a particular scope (i.e., antMatchers("/api/private-scoped").access("#oauth2.hasScope('read:messages')")), and which endpoints are public (i.e., mvcMatchers("/api/public").permitAll()).

The only problem now is that the SecurityConfig class does not know how to verify your access token. That is, whenever your users send requests to your secured endpoints, they will include access tokens that are signed with a private key. In order for this class to know if it can trust or not these access tokens, Spring will need the public key that pairs with this private key used to sign the token. To get this public key, your backend API will need to issue a request to an endpoint known as the JWKS endpoint. You don't really have to understand the details about how this works but, if you are curious, you can read all about it here and on the official specification.

Suffices to say that, for your Spring Boot API to get a copy of the public key used to validate access tokens, you will have to open the application.properties file and add the following property into it:

# ... leave other properties untouched ...
security.oauth2.resource.jwk.keySetUri=https://<DOMAIN>/.well-known/jwks.json

Note: You will have to replace <DOMAIN> with your own Auth0 domain. Just like you did before while configuring the auth0.domain environment variable.

From the backend API point of view, this is everything you need to define endpoints and to secure them with OAuth 2.0. The next section will focus on creating and defining a client app (a SPA) to consume this API. Basically, this SPA will be responsible for three things:

  1. enabling users to authenticate;
  2. getting access tokens back from the authentication server (Auth0);
  3. enabling users to use this access tokens to issue requests to your new Spring Boot API.

"Spring Boot comes with some utilities classes that facilitate securing APIs with OAuth 2.0."

Creating and Securing a SPA with the OAuth 2.0 Implicit Grant

In this section, you are going to create a jQuery SPA that will interact with your Spring Boot API. Before starting, you will need to create an Auth0 Application to represent your SPA. To do so, head to the Applications page in your Auth0 dashboard and click on Create Application. After clicking on it, Auth0 will show a dialog where you will have to input a name for your app (you can call it "Spring Boot Client SPA" or anything like that) and define the type of the application. As you are going to build an app that resembles a SPA, choose the Single Page Web Applications type.

Creating an Auth0 Application in Dashboard

Now, clicking on the Create button will make Auth0 redirect you to the Quick Start section of your new app. From there, click on the Settings tab and add http://localhost:8080 to the Allowed Callback URLs field. As a security measure, Auth0 will only redirect users (after the authentication process) back to the URLs listed on this field (i.e., whenever you move into production, make sure you have a configuration that only lists your real internet domain). Now, click on Save Changes.

After saving it, copy the value showed on the Domain field and insert it after auth0.domain= in the application.properties field (if you haven't done so yet). Then, copy the value showed on the Client ID field and use it to replace <CLIENT-ID> in this same file. In the end, your file will look like this:

auth0.domain=blog-samples.auth0.com
auth0.clientId=4Oi9...Vlab8uB

security.oauth2.resource.id=http://localhost:8080/api
security.oauth2.resource.jwk.keySetUri=https://blog-samples.auth0.com/.well-known/jwks.json

With that covered, you are ready to create your SPA. For starters, create an assets directory under /src/main/resources/static. This is where you are going to put all files related to your client application.

Now, create a new file called style.css inside the assets directory and fill it with the following CSS rules:

.btn-margin {
  margin-top: 7px
}

#profile-view,
#ping-view {
  display: none;
}

.profile-area img {
  max-width: 150px;
  margin-bottom: 20px;
}

.panel-body h3 {
  margin-top: 0;
}

This is just a Cascading Style Sheets (CSS) file to make your client app look nice. Now, you need a page for your SPA. So, create an index.html page in your static directory and fill it with the following content:

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>Calling an API</title>
  <base href="/">

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
  <link rel="stylesheet" href="assets/style.css">
</head>
<body>

<div class="content">
  <nav class="navbar navbar-default">
    <div class="container-fluid">
      <div class="navbar-header">
        <a class="navbar-brand" href="#">Auth0 - jQuery</a>

        <button id="btn-home-view" class="btn btn-primary btn-margin">
          Home
        </button>

        <button id="btn-profile-view" class="btn btn-primary btn-margin">
          Profile
        </button>

        <button id="btn-ping-view" class="btn btn-primary btn-margin">
          Ping
        </button>

        <button id="btn-login" class="btn btn-primary btn-margin">
          Log In
        </button>

        <button id="btn-logout" class="btn btn-primary btn-margin">
          Log Out
        </button>

      </div>
    </div>
  </nav>

  <main class="container">
    <!-- home view -->
    <div id="home-view">
      <h4></h4>
    </div>

    <!-- profile view -->
    <div id="profile-view" class="panel panel-default profile-area">
      <div class="panel-heading"><h3>Profile</h3></div>
      <div class="panel-body">
        <img class="avatar" alt="avatar">
        <div>
          <label><i class="glyphicon glyphicon-user"></i> Nickname</label>
          <h3 class="nickname"></h3>
        </div>
        <pre class="full-profile"></pre>
      </div>
    </div>

    <!-- ping view -->
    <div id="ping-view">
      <h1>Make a Call to the Server</h1>

      <p id="call-private-message">
        Log in to call a private (secured) server endpoint.
      </p>

      <button id="btn-ping-public" class="btn btn-primary">
        Call Public
      </button>

      <button id="btn-ping-private" class="btn btn-primary">
        Call Private
      </button>

      <button id="btn-ping-private-scoped" class="btn btn-primary">
        Call Private Scoped
      </button>

      <h2 id="ping-message"></h2>
    </div>
  </main>
</div>

<script src="https://cdn.auth0.com/js/auth0/9.5.1/auth0.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
<script src="assets/app.js"></script>
</body>
</html>

It is worth noting that this file includes Bootstrap's CSS library and your custom stylesheet (style.css) in the head element. Also, before the closing body tag, this file includes Bootstrap's JavaScript library, Auth0's JavaScript library, [jQuery])(https://jquery.com/), and a local JavaScript file called app.js. Don't worry about the missing app.js, you will create it in no time.

Now, if you take a closer look at the HTML definition, you will see that your page contains eight buttons:

  • Home (btn-home-view): This button will show the default view.
  • Profile (btn-profile-view): This button will show a view with the profile of the logged-in user.
  • Ping (btn-ping-view): This button will show a view where users will be able to issue different types of requests.
  • Log In (btn-login): This button will start the authentication process.
  • Log Out (btn-logout): This button will log out the current user.
  • Call Public (btn-ping-public): This button will call the public endpoint (/api/public) and show the returned message.
  • Call Private (btn-ping-private): This button will call one of the private endpoints (/api/private in this case) and show the returned message.
  • Call Private Scoped (btn-ping-private-scoped): This button will call the other private endpoint (/api/private-scoped) and show the returned message.

The rest of the elements defined in this HTML file is there so you can show users' profiles, messages returned by the server, etc.

Now, the last thing you will need to do is to create the app.js file in the assets directory. After creating it, add the following code to this file:

$('document').ready(function() {
  const apiUrl = 'http://localhost:8080/api';

  // load environment variables
  const envVar = $.parseJSON($.ajax({
    url:  '/config',
    dataType: 'json',
    async: false
  }).responseText);

  // create an Auth0 client
  const webAuth = new auth0.WebAuth({
    domain: envVar.domain,
    clientID: envVar.clientID,
    redirectUri: location.href,
    audience: envVar.audience,
    responseType: 'token id_token',
    scope: 'openid profile read:messages',
    leeway: 60
  });

  // sections and buttons
  const homeView = $('#home-view');
  const profileView = $('#profile-view');
  const pingView = $('#ping-view');
  const callPrivateMessage = $('#call-private-message');
  const pingMessage = $('#ping-message');

  // buttons
  const loginBtn = $('#btn-login');
  const logoutBtn = $('#btn-logout');
  const homeViewBtn = $('#btn-home-view');
  const profileViewBtn = $('#btn-profile-view');
  const pingViewBtn = $('#btn-ping-view');
  const pingPublic = $('#btn-ping-public');
  const pingPrivate = $('#btn-ping-private');
  const pingPrivateScoped = $('#btn-ping-private-scoped');

  // listeners
  pingPublic.click(() => callAPI('/public', false));
  pingPrivate.click(() => callAPI('/private', true));
  pingPrivateScoped.click(() => callAPI('/private-scoped', true));
  loginBtn.click(() => webAuth.authorize());
  logoutBtn.click(logout);
  homeViewBtn.click(displayHome);
  profileViewBtn.click(displayProfile);
  pingViewBtn.click(displayPingView);

  let accessToken = null;
  let userProfile = null;

  handleAuthentication();
  displayButtons();

  // function definitions
  function logout() {
    // Remove tokens and expiry time from browser
    accessToken = null;
    pingMessage.css('display', 'none');
    displayButtons();
  }

  function isAuthenticated() {
    return accessToken != null;
  }

  function handleAuthentication() {
    webAuth.parseHash(function(err, authResult) {
      if (authResult && authResult.accessToken) {
        window.location.hash = '';
        accessToken = authResult.accessToken;
        userProfile = authResult.idTokenPayload;
        loginBtn.css('display', 'none');
        homeView.css('display', 'inline-block');
      } else if (err) {
        homeView.css('display', 'inline-block');
        console.log(err);
        alert(
          'Error: ' + err.error + '. Check the console for further details.'
        );
      }
      displayButtons();
    });
  }

  function callAPI(endpoint, secured) {
    const url = apiUrl + endpoint;

    let headers;
    if (secured && accessToken) {
      headers = { Authorization: 'Bearer ' + accessToken };
    }

    $.ajax({
      url: url,
      headers: headers
    }).done(({message}) => $('#ping-view h2').text(message))
  .fail(({statusText}) => $('#ping-view h2').text('Request failed: ' + statusText));
  }

  function displayButtons() {
    const loginStatus = $('.container h4');
    if (isAuthenticated()) {
      loginBtn.css('display', 'none');
      logoutBtn.css('display', 'inline-block');
      profileViewBtn.css('display', 'inline-block');
      pingPrivate.css('display', 'inline-block');
      pingPrivateScoped.css('display', 'inline-block');
      callPrivateMessage.css('display', 'none');
      loginStatus.text(
        'You are logged in! You can now send authenticated requests to your server.'
      );
    } else {
      homeView.css('display', 'inline-block');
      loginBtn.css('display', 'inline-block');
      logoutBtn.css('display', 'none');
      profileViewBtn.css('display', 'none');
      profileView.css('display', 'none');
      pingView.css('display', 'none');
      pingPrivate.css('display', 'none');
      pingPrivateScoped.css('display', 'none');
      callPrivateMessage.css('display', 'block');
      loginStatus.text('You are not logged in! Please log in to continue.');
    }
  }

  function displayHome() {
    homeView.css('display', 'inline-block');
    profileView.css('display', 'none');
    pingView.css('display', 'none');
  }

  function displayProfile() {
    // display the elements
    homeView.css('display', 'none');
    pingView.css('display', 'none');
    profileView.css('display', 'inline-block');

    // display profile data
    $('#profile-view .nickname').text(userProfile.nickname);
    $('#profile-view .full-profile').text(JSON.stringify(userProfile, null, 2));
    $('#profile-view img').attr('src', userProfile.picture);
  }

  function displayPingView() {
    homeView.css('display', 'none');
    profileView.css('display', 'none');
    pingView.css('display', 'inline-block');
  }
});

As you can see, this file is quite big (almost 150 lines of JavaScript code). However, it's easy to understand what is going on there. First, your script is waiting until the document is ready ($('document').ready(...)) before doing anything. When your document is ready, your script prepares the client app by:

  1. issuing an AJAX request to http://localhost:8080/api/config to read some environment variables;
  2. configuring an Auth0 client (var webAuth = new auth0.WebAuth(...)) with these variables;
  3. and by defining and attaching event listeners to the buttons defined in your HTML file.

Another important thing that this script does is to define a function that is responsible for fetching user details after they authenticate through Auth0. If you take a look, you will see a function called handleAuthentication inside this script. This function calls webAuth.parseHash to fetch two things returned by Auth0:

  • accessToken: Your script will use this token while issuing requests to the secured endpoints in your Spring Boot API.
  • idTokenPayload: Your client app will use this info to display information about the logged-in user.

That's it! You are ready to run your Spring Boot API and to consume it through this simple SPA. So, open a terminal and execute the following commands:

# move into the project root
cd spring-boot-oauth2

# use Gradle to start your project
gradle bootRun

Note: If you imported your project correctly inside your IDE, you will probably be able to run through the IDE's interface.

After running your app, if you head to http://localhost:8080, you will see a screen where you will be able to login.

Locally running the SPA that consumes the Spring Boot API

After logging in, you will be redirected back to the SPA where you will see a similar screen but with some other buttons. If you click on the Ping button, you will see a section where you will have three options. The first one will be a button to issue requests to the public endpoint. The second one will be a button to issue requests to the endpoint that is secured, but that doesn't require any scopes. The last option will be a button that issues requests to the endpoint that is both secured and that requires a scope (in this case, read:messages). Clicking on any of these buttons will issue requests to your Spring Boot API and will output the result on the screen.

Locally running the SPA - Secure call with OAuth 2.0 and consuming a Spring Boot API

"I just learned how to secure Spring Boot APIs and SPAs with OAuth 2.0."

Conclusion

In this article, you learned how to create and secure a Spring Boot API with OAuth 2.0 using the official OAuth 2.0 library provided by the framework. You also took a quick look at how to create and authenticate a SPA so that it can consume your Spring Boot API. If you are interested in learning more about OAuth 2.0 and its Spring Boot support, visit the following links:

Again, you can find the accompanying repository and code developed throughout this article in this GitHub repository. If you have comments or questions, don't hesitate to reach out to us through the comments area down there. I hope you enjoyed.