back to all blogsSee all blog posts

Securing Open Liberty applications with Azure Active Directory using OpenID Connect

image of author
Reza Rahman on Sep 4, 2020
Post available in languages:

Long gone are the days when you had to create your own user account management, authentication, and authorization for your web delivered software. Instead, contemporary applications make use of Identity and Access Management (IAM) services from external providers. As a full-featured Java application runtime, Open Liberty has great options for externally provided IAM.

Open Liberty supports IAM mainstays, such as Social Media Login, SAML Web Single Sign-on, and OpenID Connect Client. In Bruce Tiffany’s blog post “Securing Open Liberty apps and micro-services with MicroProfile JWT and Social Media login,” you have a solid example of how to use the Open Liberty Social Media Login feature to authenticate users using their existing social media credentials. In this blog post, let’s take a look at another example of how to configure the Liberty Social Media Login feature as an OpenID Connect client to secure Java applications with Azure Active Directory.

The sample code used in this blog is hosted on this GitHub repository. Feel free to check it out and follow its user guide to run the Java EE demo application before or after reading this blog.

Set up Azure Active Directory

Azure Active Directory (Azure AD) implements OpenID Connect (OIDC), an authentication protocol built on OAuth 2.0, which lets you securely sign in a user from Azure AD to an application. Before going into the sample code, you must first set up an Azure AD tenant and create an application registration with a redirect URL and client secret. The tenant ID, application (client) ID, and client secret are used by Open Liberty to negotiate with Azure AD to complete an OAuth 2.0 authorization code flow.

Learn how to set up Azure AD from these articles:

Configure social media login as OpenID Connect client

The following sample code shows how an application running on an Open Liberty server is configured with the socialLogin-1.0 feature as an OpenID Connect client to authenticate a user from an OpenID Connect Provider, with Azure AD as the designated security provider.

The relevant server configuration in server.xml:

<?xml version="1.0" encoding="UTF-8"?>
<server description="defaultServer">

    <!-- Enable features -->
    <featureManager>
        <feature>cdi-2.0</feature>
        <feature>jaxb-2.2</feature>
        <feature>jpa-2.2</feature>
        <feature>jsf-2.3</feature>
        <feature>jaxrs-2.1</feature>
        <feature>ejbLite-3.2</feature>
        <feature>socialLogin-1.0</feature>
        <feature>transportSecurity-1.0</feature>
        <feature>appSecurity-3.0</feature>
        <feature>jwt-1.0</feature>
        <feature>mpJwt-1.1</feature>
        <feature>mpConfig-1.3</feature>
    </featureManager>

    <!-- trust JDK’s default truststore -->
    <ssl id="defaultSSLConfig"  trustDefaultCerts="true" />

    <oidcLogin
        id="liberty-aad-oidc-javaeecafe" clientId="${client.id}"
        clientSecret="${client.secret}"
        discoveryEndpoint="https://login.microsoftonline.com/${tenant.id}/v2.0/.well-known/openid-configuration"
        signatureAlgorithm="RS256"
        userNameAttribute="preferred_username" />

    <!-- JWT consumer -->
    <mpJwt id="jwtUserConsumer"
        jwksUri="https://login.microsoftonline.com/${tenant.id}/discovery/v2.0/keys"
        issuer="https://login.microsoftonline.com/${tenant.id}/v2.0"
        audiences="${client.id}"
        userNameAttribute="preferred_username"
        authFilterRef="mpJwtAuthFilter" />

    <!-- JWT auth filter -->
    <authFilter id="mpJwtAuthFilter">
        <requestUrl id="myRequestUrl" urlPattern="/rest" matchType="contains"/>
    </authFilter>

    <httpEndpoint id="defaultHttpEndpoint" host="*"
        httpPort="9080" httpsPort="9443" />

    <!-- Automatically expand WAR files and EAR files -->
    <applicationManager autoExpand="true" />

    <dataSource id="JavaEECafeDB"
        jdbcDriverRef="postgresql-driver" jndiName="jdbc/JavaEECafeDB"
        transactional="true" type="javax.sql.ConnectionPoolDataSource">
        <properties databaseName="postgres" portNumber="5432"
            serverName="${postgresql.server.name}" user="${postgresql.user}"
            password="${postgresql.password}" />
    </dataSource>

    <jdbcDriver id="postgresql-driver"
        javax.sql.ConnectionPoolDataSource="org.postgresql.ds.PGConnectionPoolDataSource"
        javax.sql.XADataSource="org.postgresql.xa.PGXADataSource"
        libraryRef="postgresql-library" />

    <library id="postgresql-library">
        <fileset dir="${shared.resource.dir}"
            id="PostgreSQLFileset" includes="postgresql-42.2.4.jar" />
    </library>

    <webApplication id="javaee-cafe"
        location="${server.config.dir}/apps/javaee-cafe.war">
        <application-bnd>
            <security-role name="users">
                <special-subject type="ALL_AUTHENTICATED_USERS" />
            </security-role>
        </application-bnd>
    </webApplication>
</server>

(Find this code sample in GitHub.)

The oidcLogin element has a large number of available configuration options in Open Liberty. With Azure AD, most of them are not required and you can use only the few options used in the code example. This is because Azure AD supports discovery endpoints as is shown in the code example. Discovery endpoints allow for most OpenID Connect configuration to be automatically retrieved by the client, significantly simplifying configuration. In addition, Azure AD instances follow a known pattern for discovery endpoint URLs, allowing us to parameterize the URL using a tenant ID. In addition to that, a client ID and secret are needed. RS256 must be used as the signature algorithm with Azure AD.

The userNameAttribute parameter is used to map a token value from Azure AD to a unique subject identity in Liberty. There are a number of Azure AD token values you can use. Do be cautious, as the required tokens that exist for v1.0 and v2.0 differ (with v2.0 not supporting some v1.0 tokens). Either preferred_username or oid can be safely used, although in most cases you will probably want to use the preferred_username.

Using Azure AD allows your application to use a certificate with a root CA signed by Microsoft’s public certificate. This certificate is added to the default cacerts of the JVM. Trusting the JVM default cacerts ensures a successful SSL handshake between the OIDC Client and Azure AD (i.e., setting the defaultSSLConfig trustDefaultCerts value to true).

In our case, we assign all users authenticated via Azure AD the users role. More complex role mappings are possible with Liberty if desired.

Use OpenID Connect to authenticate users

The sample application exposes a JSF client, which defines a Java EE security constraint that only users with the role users can access.

The relevant configuration in web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.1"
    xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd">
    <context-param>
        <param-name>javax.faces.PROJECT_STAGE</param-name>
        <param-value>Production</param-value>
    </context-param>
    <servlet>
        <servlet-name>Faces Servlet</servlet-name>
        <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>Faces Servlet</servlet-name>
        <url-pattern>*.xhtml</url-pattern>
    </servlet-mapping>

    <servlet>
        <servlet-name>javax.ws.rs.core.Application</servlet-name>
    </servlet>
    <servlet-mapping>
        <servlet-name>javax.ws.rs.core.Application</servlet-name>
        <url-pattern>/rest/*</url-pattern>
    </servlet-mapping>

    <session-config>
        <session-timeout>
            30
        </session-timeout>
    </session-config>

    <welcome-file-list>
        <welcome-file>index.xhtml</welcome-file>
    </welcome-file-list>

    <security-role>
        <role-name>users</role-name>
    </security-role>

    <security-constraint>
        <web-resource-collection>
            <web-resource-name>javaee-cafe</web-resource-name>
            <url-pattern>/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>users</role-name>
        </auth-constraint>
    </security-constraint>
</web-app>

(Find this code sample in GitHub.)

Workflow

The following diagram illustrates the OpenID Connect sign-in and token acquisition flow from Microsoft identity platform and OpenID Connect protocol:

OpenID Connect sign-in and token acquisition flow

This is standard Java EE security. When an unauthenticated user attempts to access the JSF client, they are redirected to Microsoft to provide their Azure AD credentials. Upon success, the browser gets redirected back to the client with an authorization code. The client then contacts Microsoft again with the authorization code, client ID and secret to obtain an ID token and access token, and finally create an authenticated user on the client, which then gets access to the JSF client.

To get authenticated user information, use the @Inject annotation to obtain a reference to the javax.security.enterprise.SecurityContext and call its method getCallerPrincipal():

@Named
@SessionScoped
public class Cafe implements Serializable {

    @Inject
    private transient SecurityContext securityContext;

    public String getLoggedOnUser() {
        return securityContext.getCallerPrincipal().getName();
    }
}

Find this code sample in GitHub.)

Secure internal REST calls using JWT RBAC

The Cafe bean depends on CafeResource, a REST service built with JAX-RS, to create, read, update and delete coffees. The CafeResource service implements RBAC (role-based access control) using MicroProfile JWT to verify the groups claim of the token.

@Path("coffees")
public class CafeResource {

    private static final Logger logger = Logger.getLogger(MethodHandles.lookup().lookupClass().getName());

    @Inject
    private CafeRepository cafeRepository;

    @Inject
    @ConfigProperty(name = "admin.group.id")
    private String ADMIN_GROUP_ID;

    @Inject
    private JsonWebToken jwtPrincipal;

    @DELETE
    @Path("{id}")
    public void deleteCoffee(@PathParam("id") Long coffeeId) {
        if (!this.jwtPrincipal.getGroups().contains(ADMIN_GROUP_ID)) {
            throw new WebApplicationException(Response.Status.FORBIDDEN);
        }

        try {
            this.cafeRepository.removeCoffeeById(coffeeId);
        } catch (IllegalArgumentException ex) {
            logger.log(Level.SEVERE, "Error calling deleteCoffee() for coffeeId {0}: {1}.",
                    new Object[] { coffeeId, ex });
            throw new WebApplicationException(Response.Status.NOT_FOUND);
        }
    }

    @GET
    @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
    public List<Coffee> getAllCoffees() {
        return this.cafeRepository.getAllCoffees();
    }

    @POST
    @Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
    @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
    public Coffee createCoffee(Coffee coffee) {
        try {
            return this.cafeRepository.persistCoffee(coffee);
        } catch (PersistenceException e) {
            logger.log(Level.SEVERE, "Error creating coffee {0}: {1}.", new Object[] { coffee, e });
            throw new WebApplicationException(e, Response.Status.INTERNAL_SERVER_ERROR);
        }
    }
}

(Find this code sample in GitHub.)

The admin.group.id is injected into the application using MicroProfile Config at the application startup using the ConfigProperty annotation. MicroProfile JWT enables you to @Inject the JWT (JSON Web Token). The CafeResource REST endpoint receives the JWT with the preferred_username and groups claims from the ID Token issued by Azure AD in the OpenID Connect authorization workflow. The ID Token can be retrieved using the com.ibm.websphere.security.social.UserProfileManager and com.ibm.websphere.security.social.UserProfile APIs.

Here is the relevant configuration snippet in server.xml:

<?xml version="1.0" encoding="UTF-8"?>
<server description="defaultServer">

    <!-- Enable features -->
    <featureManager>
        <feature>jwt-1.0</feature>
        <feature>mpJwt-1.1</feature>
        <feature>mpConfig-1.3</feature>
    </featureManager>

    <!-- JWT consumer -->
    <mpJwt id="jwtUserConsumer"
        jwksUri="https://login.microsoftonline.com/${tenant.id}/discovery/v2.0/keys"
        issuer="https://login.microsoftonline.com/${tenant.id}/v2.0"
        audiences="${client.id}"
        userNameAttribute="preferred_username"
        authFilterRef="mpJwtAuthFilter" />

    <!-- JWT auth filter -->
    <authFilter id="mpJwtAuthFilter">
        <requestUrl id="myRequestUrl" urlPattern="/rest" matchType="contains"/>
    </authFilter>
</server>

(Find this code sample in GitHub.)

Note, the groups claim is not propagated by default and requires additional Azure AD configuration. To add a groups claim into the ID token, you need to create a group with type as Security and add one or more members to it in Azure AD. In the application registration created as part of Azure AD configuration, you also need to find Token configuration, select Add groups claim, select Security groups as group types to include in ID token, then expand ID and select Group ID in the Customize token properties by type section. Learn more details from these articles:

Summary

In this blog entry, we demonstrated how to effectively secure an Open Liberty application using OpenID Connect and Azure Active Directory. This write-up and the underlying official Azure sample should also easily work for WebSphere Liberty. This effort is part of a broader collaboration between Microsoft and IBM to provide better guidance and tools for developers using Java EE, Jakarta EE (Java EE has been transferred to the Eclipse Foundation as Jakarta EE under vendor-neutral open source governance), and MicroProfile (MicroProfile is a set of open source specifications that build upon Java EE technologies and target the microservices domain) on Azure.

We would like to hear from you as to what kind of tools and guidance you need. If possible, please fill out a five-minute survey on this topic and share your invaluable feedback—especially if you are interested in working closely with us (for free) on a cloud migration case.

Reza Rahman is Principal Program Manager for Java on Azure at Microsoft.