9. Authentication using OAuth2/OpenId Connect protocol

In this chapter, we create a Java EE 7 JSF application which is protected by an OAuth2 provider, Google in our main scenario. It shows how we can protect an application with a social login system and keep control over which users can use our application and what the individual permissions of the end users are.

9.1 Main scenario

The code for this example can be found in the gitbook/ex9 directory of this Git Repository.

9.1.1 Basic Octopus setup

Instead of repeating all the different steps to define the basic requirements of a Java EE 7 JSF application with the Octopus framework, I refer to them in chapter 1. Just repeat the step 1.1.1 to 1.1.7 (included) but use for example the directory name ex9.

Some of the files need a small change and will be explained in the next sections.

9.1.2 Add the Google OAuth2 module

  • Add the following dependency to the pom.xml file

<dependency>
    <groupId>be.c4j.ee.security.octopus.authentication</groupId>
    <artifactId>octopus-oauth2-google</artifactId>
    <version>${octopus.version}</version>
</dependency>

  • Define the final name of the war file and the contextRoot

<build>
    <finalName>oauth2</finalName>
</build>

9.1.3 define userBean

  • Create a CDI bean providing some information about the current user. Create the class be.atbash.ee.security.octopus.book.ex9.UserBean

  • Specify the CDI annotation @Model on the class.

  • Define logic for retrieving information

@Inject
private UserPrincipal userPrincipal;

private List<Serializable> keys;

@PostConstruct
public void init() {
    keys = new ArrayList<>(userPrincipal.getInfo().keySet());
}

public String getInfoValue(String key) {
    Object info = userPrincipal.getUserInfo(key);
    return info == null ? "" : info.toString();
}

public List<Serializable> getKeys() {
    return keys;
}

9.1.4 Update the main user page

  • Show some basic user information

<h:graphicImage value="#{userPrincipal.info.picture}"/> <br/>
<ui:repeat value="#{userBean.keys}" var="_key">
    #{_key} : #{userBean.getInfoValue(_key)} <br/>
</ui:repeat>

9.1.5 Update the method supplying the AuthenticationInfo

  • Update the method getAuthenticationInfo() from the class be.atbash.ee.security.octopus.book.ex9.ApplicationSecurityData class

@Override
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken authenticationToken) {
    if (authenticationToken instanceof OAuth2User) {
        OAuth2User user = (OAuth2User) authenticationToken;
        OAuth2AuthenticationInfoBuilder builder = new OAuth2AuthenticationInfoBuilder(user);
        return builder.build();
    }

    return null;
}

9.1.6 Configure Google OAuth2 integration

  • There are only 2 things we need to configure for the integration between our application and Google, the clientId and clientSecret. Put the following values in the octopusConfig.properties file.

OAuth2.clientId = 208845979122-dgn7s1umrv2ll15ilg7lvmt81e4651si.apps.googleusercontent.com
OAuth2.clientSecret = UHErnUvaQEcKVYYqVBdjutrD

9.1.7 Test the application by following this test scenario

  • Deploy the application on a Java EE 7 compliant application server capable of running on Java 8.
  • Open the browser pointing to the following URL : http:///ex9/index.xhtml
  • Click on the User page page link
  • Login to your Google Account if required
  • Give Consent that the Octopus Application is allowed to read your basic user information.
  • Check that the page shows your name, picture (from Google account) and some basic information.

9.2 Explanation

9.2.1 OAuth2 / OpenIdConnect

For a detailed description and explanation of OAuth2 and OpenIdConnect, see the various resources on the internet. This gives you only a brief overview.

In the case of OAuth2 / OpenIdConnect, the credentials are managed by an external party, in this case, it is Google. But there are many other Identity providers on the internet or products which offer the same functionality.

For Web applications which have a backend system (like Java EE), the most secure option is the Authorization code grant flow.

After you authenticate with the eternal system, your application receives an authorization code. This can be exchanged for a Access code and ID token in the case of OpenIdConnect. The idToken contains the information about the logged in user. Also with the access code, we can call the user info endpoint of the external party to receive more account information. This last option is used with Octopus.

But all the hard work is performed by the code (with the help of the Scribe Java library) of Octopus. As a developer, you just need to configure the clientId and clientSecret which you received when you have registered your application, as a client application at the external party.

9.2.2 Adding Google OAuth2 integration

All the required code and dependencies are added by the Maven artefact be.c4j.ee.security.octopus.authentication:octopus-oauth2-google. The developer only needs to specify the clientId and clientSecret and the integration is complete.

The root context is important on which the application will be available. The callback is automatically generated based on the runtime information it finds. Therefore we use the finalName oauth2 so that the value corresponds to the configured value at Google API console.

The rule for the endpoint which can take the authorization code (see also 9.3) and exchange it to an access code is


<host>/<contextRoot>/oauth2callback

9.2.3 userBean with information

Information retrieved from the external party which performs the authentication is stored within the info map of the user principal.

The UserBean returns the key properties of this map so that the screen can list all the properties we received.

9.2.4 show user information

One of the properties is the URL to the profile photo. This is displayed using the graphicImage component.
The other properties are listed as key-value pairs.

The developer can use these values to retrieve additional information for the logged on user.

9.2.5 Specific AuthenticationInfo procedure

Although the authentication is already performed, Octopus still makes the calls to the CDI bean implementing the SecurityDataProvider interface. This allows the developer to restrict the access to the application to a subset of the Google user. (for example limit to employees of your company)

There is a special AuthenticationInfoBuilder which performs the default processing for an authentication from an OAuth2 provider, OAuth2AuthenticationInfoBuilder. The information is not validated by a CredentialMatchers, is in other cases, because the OAuth2User class is an AuthenticationToken which implements the ValidatedAuthenticationToken interface.

This interface indicates that the token is created for an already authenticated situation (here by the OAuth2 provider) and thus that no verification is needed anymore.
But the developer can still return null from the getAuthenticationInfo() method which, just as in the other cases, is an indication of an unknown user for the system (and thus the user is not granted access although authentication was successful)

9.2.6 Configure Google OAuth2 integration

On the client side of the OAuth2 integration, only the values for clientId and clientSecret needs to be specified. This is done by defining them in the octopusConfig.properties file.
These values are given by the OAuth2 provider when the application is registered. At that time, the client callback also needs to be entered which is already explained in 9.2.2.

9.2.7 Testing

When you click on the user page link, Octopus sees that the current user is not yet authenticated (as defined by the securedURLs.ini configuration file). A redirect is performed, just as in the case where we used the user name and password entry like in chapter 1.
However, due to the addition of the Google OAuth2 integration, the redirection is no longer to the /login.xhtml page, but to the Google login page together with some URL parameters to indicate which client application is requesting this Google login.

After that the end user is authenticated with Google, this is maybe done by the SSO cookie which Google has left in your browser, so you actually don't see the request for username and password, the callback URL of our application is called with the authorization code as a parameter.

An Octopus servlet processes this call, by taking this authorization code and exchange this to the access code by calling the token endpoint of Google. During this process, various cross-site request attacks are countered (with the use of the state and nonce parameters) and authentication process is aborted when incorrect values are found in responses.

When everything went well, the information from Google is presented as an AuthenticationToken to the getAuthenticationInfo() method. Normally the developer must return from this method the account information linked to this authentication attempt. This time, we receive all the information from Google and it is presented to the getAuthenticationInfo() method.
The developer can still decide here that the user isn't granted access to the application (because the email address of the user does not contain the domain name of the company, handy for internal apps linked to the Google account of your employees when you are using G Suite / Google apps for Work). In this situation, we need to return null. Octopus interprets this, just as with other authentication methods, that the user is unknown and not allowed access.

There is no validation anymore at the Octopus level of the AuthenticationToken in combination with the AuthenticationInfo because the user is already authenticated by Google. This is reflected by the fact that the AuthenticationToken instance, OAuth2User also implements the ValidatedAuthenticationToken interface.

Now the user is redirected to his original requested page and basic information is shown (include profile image)

9.3 Where to go next

  • Try the alternatives (See 9.5) to use other providers are user can select one of the multiple presented providers
  • Look at the procedure (in 9.4) on how the configure Google for your client application
  • See ??? for the Octopus implementation of the OpenIdConnect protocol (which allows you to create your own OpenIdConnect provider)
  • See ??? for using the access codes as authentication for JAX-RS endpoints.
  • See ??? for using MicroProfile JWT authentication specification as authentication way.

9.4 Client application on Google configuration

In order to integrate with Google OAuth2 / OpenIdConnect, we need to define our application as Client application.

9.5 Alternatives

9.5.1 GitHub as OAuth2 / OpenIdConnect provider

Octopus has also support for GitHub as OAuth2 / OpenIdConnect provider. Since the authentication mechanism is standardized, we only need to change the configuration.
You can start from the main scenario created within 9.1

  • Change the maven dependency for Google with the one for GitHub

<dependency>
    <groupId>be.c4j.ee.security.octopus.authentication</groupId>
    <artifactId>octopus-oauth2-github</artifactId>
    <version>${octopus.version}</version>
</dependency>

  • Change the clientId and clientSecret for the GitHub defined application with the octopusConfiguration.properties

OAuth2.clientId=271f8e3eacb955487e92
OAuth2.clientSecret=26a3030a10e742e4edf4a0496ee707fdfd18cf4b

  • Test the application again

9.5.2 Multiple providers and user selection

Octopus has also support for multiple providers within the application. When there are multiple dependencies are added, we need to display a screen where the user can select which provider (s)he wants to use.
From the developer's point of view, it doesn't matter which provider is used, we only need to know which user is working on our application. We can do this based on the name, or maybe better, based on the email address which each provider returns us.

  • Add multiple maven dependencies for the OAuth2 providers you like to offer to the end user. Octopus has support for 3 of them by default

<dependency>
    <groupId>be.c4j.ee.security.octopus.authentication</groupId>
    <artifactId>octopus-oauth2-google</artifactId>
    <version>${octopus.version}</version>
</dependency>

<dependency>
    <groupId>be.c4j.ee.security.octopus.authentication</groupId>
    <artifactId>octopus-oauth2-github</artifactId>
    <version>${octopus.version}</version>
</dependency>

<dependency>
    <groupId>be.c4j.ee.security.octopus.authentication</groupId>
    <artifactId>octopus-oauth2-linkedin</artifactId>
    <version>${octopus.version}</version>
</dependency>

  • Define for each provider the clientId and clientSecret within octopusConfiguration.properties.

Google.OAuth2.clientId = 208845979122-dgn7s1umrv2ll15ilg7lvmt81e4651si.apps.googleusercontent.com
Google.OAuth2.clientSecret = UHErnUvaQEcKVYYqVBdjutrD

Github.OAuth2.clientId=271f8e3eacb955487e92
Github.OAuth2.clientSecret=26a3030a10e742e4edf4a0496ee707fdfd18cf4b

Linkedin.OAuth2.clientId=771a48ph3b53xt
Linkedin.OAuth2.clientSecret=CM5ekYbsZR6y0smD

  • Create the /webapp/login.xhtml file where the user can define the provider of his choice.

<h:body>
    <h2>Authenticate</h2>
    <h:form id="login">
        Authentication providers supported :<br/>
        <ui:repeat value="#{defaultOauth2ServletInfo.providers}" var="_provider">
            <h:commandLink actionListener="#{defaultOauth2ServletInfo.authenticateWith(_provider)}"
                           value="#{_provider}"/>
            <br/>
        </ui:repeat>
        <h:messages/>
    </h:form>

</h:body>

Additional info

When Octopus detects multiple providers on the classpath, the logic described in 9.2.7 is slightly different when the user needs to authenticate itself.
Instead of redirecting to the provider login page, the normal redirect to the login.xhtml page (or equivalent defined within the configuration) is shown to the end user. This allows him to select the provider of his choice.

The developer doesn't need to hardcode this list, the discovered providers are returned by the DefaultOauth2ServletInfo CDI bean, more in specific, the getProviders() method. This is a list of Strings.

The authentication process can be continued by calling the method DefaultOauth2ServletInfo.authenticateWith(String) where the String parameter is the String which identifies the provider and must be found in the list of all discovered providers.

We need also multiple clientId and clientSecret values, for each provider a set. This is solved by using the provider name as prefix of the parameter name. So we can specify the Google clientId with Google.OAuth2.clientId, Github clientId with Github.OAuth2.clientId and the Linkedin one with Linkedin.OAuth2.clientId.

results matching ""

    No results matching ""