Build Single Sign-on in Java

In modern app development, you quite frequently have a single resource server that provides data to multiple client applications. These applications may share a similar set of users, but need to enforce different permissions. For example, it’s possible that not all users of the first application should be allowed to access the second (think of, for example, an admin console application versus a client or user application). How would you implement this?

One way to do this is to use Okta, a software-as-service software security provider. Not only does Okta provide a quick and easy OAuth 2.0 single sign-on implementation, it also allows you to set access policies and user permissions via OAuth scopes that span multiple applications.

In this post, you are going to use Okta and Spring Boot to implement single sign-on with two client applications and a single resource server. You will see how to use access policies to enforce authentication and authorization policies. You will also restrict access to the resource server based on application scopes.

The first step, before you dive into the code, is to get the user authentication configurations in place. As mentioned, you’ll be using Okta as the OAuth 2.0 and OIDC (OpenID Connect) provider. With Okta you can manage users and groups, as well as easily implement features like social login and multi-factor authentication.

If you haven’t already, head over to register and create a free Okta developer account. Look for an email with instructions to complete the initialization of your Okta account.

Once you have an account, log in because you need to set up your web applications, users, resource server, and authorization server! You may need to click the yellow Admin button on your first login to get to the developer’s console.

Create Two OpenID Connect Applications

The first step is to create two OIDC applications. OpenID Connect is an authentication protocol built on top of OAuth 2.0, which is an authorization protocol. Each OIDC application defines an authentication provider endpoint for each web application instance.

In the Okta developer console, navigate to Applications and click Add Application. Choose Web and click Next. Populate the fields with these values:

Field Value
Name OIDC App 1
Base URIs http://localhost:8080
Login redirect URIs http://localhost:8080/login/oauth2/code/okta

Click Done.

Scroll down and make a note of the Client ID and Client Secret. You’ll use those values shortly.

Repeat these steps for your second application with these values:

Field Value
Name OIDC App 2
Base URIs http://localhost:8081
Login redirect URIs http://localhost:8081/login/oauth2/code/okta

Click Done.

You’ll also need the Client ID and Client Secret from this OIDC application as well.

Create Test Users for your Java Application

Next, you need to create two users. The first will be a user that can only log into the first application (OIDC App 1) and the second user will be one that can log into both applications.

In the developer console, click on Users > People and then click on Add Person. Fill out the form with the information for the first user using the table below. Repeat this for the second user, also using the table below.

  First User Second User Comments
First Name Amanda Tanya Can be anything you like
Last Name Tester Tester Can be anything you like
Username amandaTester@mail.com tanyaTester@mail.com Might prefer to test with an email you can actually access
Primary Email amandaTester@mail.com tanyaTester@mail.com  
Secondary Email      
Groups      
Password Set by Admin Set by Admin This is to simplify the demo. In a production environment, you will likely want this set to “set by user”
Password Value Test1234 Test1234 Complexity requirements: - at least 8 characters - a lowercase letter - an uppercase letter - a number - no parts of your username
User must change password unchecked unchecked This is to simplify the demo. In a production environment, you will likely want this checked if you have admin set password.

Take note of the username and password for both users (you’ll be testing the app with them later).

Once you have the user created, you can click on the user name, then click on profile and then click on edit. In here, add some info to each user for the following fields: middle name and nickname. This will allow you to see this info from the application later.

Create a Service Application for Your Resource Server

Now you need to create an OIDC application for your resource server. This will configure access to the REST API.

In the Okta developer console, navigate to Applications and click Add Application. Choose Service and click Next. Populate the fields with these values:

Field Value
Name OIDC Resource Server

Click Done.

Scroll down and copy the Client ID and Client Secret. You’ll use those values shortly.

Create an Authorization Server

The last step in Okta is to create and configure an authorization server. This allows you to configure custom claims and to set custom access policies. This determines whether or not Okta will issue a token when one is requested, which governs a user’s ability to access the client applications and the resource server.

Navigate to API > Authorization Servers. Click Add Authorization Server and fill in the values as follows:

Field Value
Name OIDC Auth Server
Audience api://oidcauthserver
Description OIDC Auth Server

Click Done and then click the Claims tab. In Claims, click Add Claim, fill in the fields with the values for Claim 1 below, and click Create. You can leave any value not mentioned below as default. When done, repeat and create a second claim with the values under Claim 2 below.

Field Claim 1 Claim 2
Name fullName userEmail
Include in token type Access Token Always Access Token Always
Value user.fullName user.email
Include in -> The following scopes profile email

Next, you will add an Access Policy for the first application. This application will allow both users access to it. Click the Access Policies tab, Add New Access Policy, fill in the fields with these values, and click Create Policy.

Field Value
Name OIDC App 1
Description OIDC App 1
Assign to The following clients
Assign to clients Start typing: OIDC in the input area below The following clients and click Add to the right of OIDC App 1.

This binds the policy to your OIDC app.

Next click Add Rule. Set OIDC App 1 for the Rule Name field. Deselect all the grant types except for Authorization Code and click Create Rule.

This ensures that the request must use the authorization code flow in order for Okta to create tokens. This is the most secure flow of all the available OAuth flows. It ensures that all sensitive information (like tokens) are delivered via a response to a POST request.

Next, you will add an Access Policy for the second application. This application will allow only the second user, Tanya Tester to access it. From the Access Policies tab, Add Policy, fill in the fields with these values and click Create Policy.

Field Value
Name OIDC App 2
Description OIDC App 2
Assign to The following clients
Assign to clients Start typing: OIDC in the input area below The following clients and click Add to the right of OIDC App 2.

This binds the policy to your OIDC app.

Next click Add Rule. Set OIDC App 2 for the Rule Name field. Deselect all the grant types except for Authorization Code. Find the User Is section and select the second radio button labeled “Assigned the app and a member of one of the following:” In the Users box that appears, start typing Tanya and select Tanya Tester from the list. This tells it that only this user can log in to the OIDC App 2 application.

Click Create Rule.

Click the Settings tab and copy the Issuer URL. You’ll make use of this value shortly.

You’re all done configuring things in Okta. On to the code!

Create the OAuth 2.0 Resource App

You will be working with two different code bases. The first is the code base for the resource server, which will be used to provide the client application with additional user information if the client is authorized to get such information.

Start by downloading the code for the resource server available in the GitHub repository.

git clone https://github.com/oktadeveloper/okta-java-spring-sso-example.git
cd okta-java-spring-sso-example/oauth2-resource-server

You will need to configure the resource application with the values from the “OIDC Resource Server” application you created in Okta. Open the src/main/resources/application.properties file.

okta.oauth2.issuer={issuerUri}
okta.oauth2.clientId={clientId}
okta.oauth2.clientSecret={clientSecret}
okta.oauth2.audience=api://oidcauthserver
server.port=8082

Replace the {clientId} and {clientSecret} with the ones you wrote down for the resource server above. The {issuerUri} is the Issuer URI for the authorization server you created above. go to API and Authorization Servers and look in the table next to OIDC Auth Server.

To see what the resource server does, take a look at the code in the DemoResourceServer class.

src/main/java/com/okta/examples/sso/DemoResourceServer.java

package com.okta.examples.sso;

import java.security.Principal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class DemoResourceServer  {

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

    @GetMapping("/welecomeMessage")
    @PreAuthorize("hasAuthority('SCOPE_profile')")
    public String getWelcomeMessage(Principal principal) {
        JwtAuthenticationToken jwtAuth = (JwtAuthenticationToken) principal;
        String fullName = jwtAuth.getToken().getClaimAsString("fullName");
        return "Welcome " + fullName + "!";
    }

    @GetMapping("/userEmail")
    @PreAuthorize("hasAuthority('SCOPE_email')")
    public String getUserEmail(Principal principal) {
        JwtAuthenticationToken jwtAuth = (JwtAuthenticationToken) principal;
        String email = jwtAuth.getToken().getClaimAsString("userEmail");
        return email;
    }
}

This code sets up both a spring boot application and controller all at once. The @SpringBootApplication annotation tells the application that it should support auto-configuration, component scanning, and bean registration.

The @RestController annotation tells the system that this file is a Rest API controller which simply means that it contains a collection of API endpoints. The @EnableGlobalMethodSecurity annotation tells the system that the endpoints may have security put on the method level, which both of the methods do. Each of the get endpoints uses the @PreAuthorize annotation to tell the system that the calling application must have a particular scope specified in order to be authorized. For instance, if the /userEmail endpoint gets called without the email scope, it will throw an error.

The getWelcomeMessage method returns a welcome message with the user’s full name in it. The getUserEmail method will return the user’s email. Both of these pieces of data are pulled from the token claims, which you set up from the Okta console earlier.

Open a shell and launch an instance of the resource server using Maven.

./mvnw spring-boot:run

It will now be listening on port 8082.

Create the OAuth 2.0 Client App

The second codebase you will use is the code base for the two different client applications. Both client applications will use the same code but be launched with different configurations.

When you run the client application, you will run it first for OIDC App 1, which has the profile scope set.

You will also run it for OIDC App 2, but for this application, you will run it with both the profile and email scopes set.

This is one of the key differences in configuration between these two applications.

For both instance of the client applications, start with the code in the oauth2-client directory of the example project.

This web application is pretty simple. It imports the needed Okta and Spring dependencies and then just defines a client application that can be launched given certain parameters. The complete code for this application is in the SingleSignOnApplication class.

src/main/java/com/okta/examples/sso/SingleSignOnApplication.java

package com.okta.examples.sso;

import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.servlet.ModelAndView;

@Controller
@SpringBootApplication
public class SingleSignOnApplication {

    private WebClient webClient;
    @Value("#{ @environment['resourceServer.url'] }")
    private String resourceServerUrl;

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

    public SingleSignOnApplication(WebClient webClient) {
        this.webClient = webClient;
    }

    @GetMapping("/")
    public ModelAndView home(@AuthenticationPrincipal OidcUser user) {
        ModelAndView mav = new ModelAndView();
        mav.addObject("user", user.getUserInfo());
        Map<String,String> userBasicProfile = new HashMap<String,String>();
        userBasicProfile.put("First Name",user.getGivenName());
        userBasicProfile.put("Middle Initial",user.getMiddleName());
        userBasicProfile.put("Last Name",user.getFamilyName());
        userBasicProfile.put("Nick Name",user.getNickName());
        String welcomeMessage = this.webClient.get()
                .uri(this.resourceServerUrl + "/welecomeMessage").retrieve()
                .bodyToMono(String.class).block();
        mav.addObject("welcomeMessage",welcomeMessage);
        
        try {
            String email = this.webClient.get()
                    .uri(this.resourceServerUrl + "/userEmail").retrieve()
                    .bodyToMono(String.class).block();

            if (email != null) {
                userBasicProfile.put("Email", email);
            }
        } catch (Exception e) {
            mav.addObject("emailError", true);
        }

        mav.addObject("profile", userBasicProfile);
        mav.setViewName("home");
        return mav;
    }

    @Configuration
    public static class OktaWebClientConfig {

        @Bean
        WebClient webClient(ClientRegistrationRepository clientRegistrations,
                            OAuth2AuthorizedClientRepository authorizedClients) {
            ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2 = new ServletOAuth2AuthorizedClientExchangeFilterFunction(
                    clientRegistrations, authorizedClients);
            oauth2.setDefaultOAuth2AuthorizedClient(true);
            oauth2.setDefaultClientRegistrationId("okta");
            return WebClient.builder().apply(oauth2.oauth2Configuration()).build();
        }
    }
}

This file is annotated with both the @Controller and @SpringBootApplication annotations. The @SpringBootApplication annotation tells the application that it should support auto-configuration, component scanning, and bean registration.

The @Controller annotation tells the system that this file is a Rest API controller. In this case, there is only one endpoint, which handles GET requests to the base / URL.

That endpoint calls the code in the home method, which in the simplest terms, builds up a bunch of data to be displayed on the page and tells the page which template to use to display this data.

Within the home method, there are two calls to a resource server. First, it calls the resource server to get the welcome message to display on the page. This message will be returned successfully as long as the application is configured with the profile scope set, which as I mentioned earlier, it will be set for both instances of the client application. The next call gets the email for the user. The email is only successfully returned if the email scope is set for the application.

Remember, only the second instance of the client application will have the email scope set, so for the first instance, it will throw an error. This is an example of failed authorization using scopes to determine authorization.

If the email can’t be retrieved, it instead sets a flag that tells the template (configured in the home.html file) to display a message saying that the application is not authorized to get the email for the user.

Configure the Client Apps Using Spring Run Profiles

Now you need to configure the two different instances of the client application in the oauth2-client project folder. You need to be able to run two different instances of the client app using two different configuration values. To accomplish this, you’re going to take advantage of Spring Boot’s run profiles. This is typically used to separate things like test and dev and production, but there’s no reason we can’t use it here.

If you look under oauth2-client/src/main/resources, you’ll see three .properties files.

application.properties is common to all three profiles application-client1.properties has the config values for client 1 application-client2.properties has the config values for client 2

Open oauth2-client/src/main/resources/application.properties and fill in the Issuer URI for the resource server you created above.

To find the Issuer URI (if you didn’t write it down) go to API and Authorization Servers. Look in the table next to OIDC Auth Server under Issuer URI.

okta.oauth2.issuer={yourIssuerUri}
resourceServer.url=http://localhost:8082

Open oauth2-client/src/main/resources/application-client1.properties and fill in the Client ID and Client Secret for the first OIDC client app.

If you need to find these values again, from the Okta Developer’s console, go to Applications, click on the OIDC application name (OIDC App 1) in the table, and click on the General tab. The Client ID and Client Secret are at the bottom.

okta.oauth2.clientId={yourClient1Id}
okta.oauth2.clientSecret={yourClient1Secret}
okta.oauth2.scopes=openid,profile
server.port=8080

Open oauth2-client/src/main/resources/application-client2.properties and fill in the Client ID and Client Secret for the second OIDC client app.

okta.oauth2.clientId={yourClient2Id}
okta.oauth2.clientSecret={yourClient2Secret}
okta.oauth2.scopes=openid,profile,email
server.port=8081

The last step is to run two instances of this client application. Run the following commands in two separate shell windows. This loads a client with each run profile.

Run client app 1 at http://localhost:8080:

./mvnw spring-boot:run -Dspring-boot.run.profiles=client1

Run client app 2 at http://localhost:8081:

./mvnw spring-boot:run -Dspring-boot.run.profiles=client2

That was a whole lot of configuration of things, so lets recap quickly what you’ve just set up and run.

  • http://localhost:8082 is your local resource server
  • http://localhost:8080 is client app 1 (any authenticated user can access)
  • http://localhost:8081 is client app 2 (access policy set to only allow Tany Tester to access)

On the Okta side:

  • You created matching OIDC applications for the server and the two client apps. This generates unique Client IDs and Client Secrets for each application, which allows Okta to authenticate the applications and allows you to use Okta to configure them.
  • You also created a custom authorization server. This will manage all the authentication and authorization requests from the applications.
  • Within the authorization server, you created two access policies, one for each of the client applications. Both access policies restrict access to the authorization code flow. The first client app is open to any authenticated user (any user authenticated through Okta’s single sign-on). The second app is restricted to the user Tanya Tester.

Thus you have created a pretty typical production scenario where you have a resource server that serves data to multiple client applications and you are using Okta’s dashboard to provide single sign-on, manage users, and set access policies to the client apps and the resource server.

Got it? Time to try it out.

Test Your Java Single Sign-On

In the next few steps, you will be logging in and out of different Okta accounts on the two different applications. Using an incognito window will avoid the need to log out of the Okta developer console or the single sign-on account.

Open a new incognito browser window and enter the URL http://localhost:8080. This is the URL to the first application OIDC App 1.

Log in with the tanyaTester@mail.com user. You should be able to successfully log in!

Screen shot, Tanya Tester profile without email scope

Next, you can change the URL to http://localhost:8081. This is the URL to the second application OIDC App 2. You will notice that you don’t have to log in again. This is because you already logged into OIDC App 1 and this is single sign-on!

Screen shot, Amanda Tester profile without email scope

If you were to close the browser window, open a new incognito browser, and log in to OIDC App 2 again, you would be prompted to log in again, since it will no longer have your session.

Test Your Access Policy

You have already seen that Tanya Tester can log in to both applications. Next, you’ll see what happens when you log in to each application with the amandaTester@mail.com user. If you haven’t already, close any incognito browser windows that you were already using for testing.

Open a new incognito browser window and enter the URL http://localhost:8080. Log in with the amandaTester@mail.com user. You should be able to successfully login!

Screen shot, Amanda Tester profile without email scope

Close that browser window and open a new incognito browser window and enter the URL http://localhost:8081.

Log in with the amandaTester@mail.com user. You will get an “access denied” error.

Screen shot, access denied after login

This error is because you set up the access policy so that only Tanya Tester could log in to OIDC App 2.

Test Your Scope Authorization

Lastly, you are going to test how the resource server handled the authorization of each of the applications.

Open a new incognito browser window and enter the URL http://localhost:8080. Log in with the tanyaTester@mail.com user. You should be able to successfully log in.

Notice that you will see a message at the top with the user-specific welcome message. This is because the application was using the profile scope and was therefore allowed to access the welcome message endpoint. Below that, you will see a message saying that the application does NOT have access to the email information. This is because this instance of the application is NOT running with the email scope.

Screen shot, Tanya Tester profile without email scope

Next, you can change the URL to http://localhost:8081. Remember, this is the URL to the second application OIDC App 2. When you go to this page, you will notice that you do NOT see the message about not having access to the email. Instead, you will see the email as part of the profile information.

Screen shot, Tanya Tester's full profile

Learn More About Secure Single Sign-On in Java

I sincerely hope you enjoyed going through this tutorial, setting up a Spring Boot app, and finding out how easy it is to setup signle sign-on with Okta.

To learn more about Spring Boot, OpenID Connect, and other topics, you might find these links helpful:

Don’t hesitate to drop me a note in the comments below and please follow @OktaDev on Twitter for more technical posts like this one!