Here we’ll create an authentication provider that lets the Graph SDK that lets our Java app call the Graph API on behalf of the user. This article will focus on creating the authentication provider as part of a Spring Boot web app using the Microsoft Authentication Library for Java and Azure AD.
The Microsoft Graph API provides access to a rich collection of data relating to users and their documents, emails, contacts, meetings, and much more. All of this data is made available through an HTTP API and the JSON objects it returns. But parsing raw JSON is error-prone. Plus, it often involves tedious traversals through maps — creating classes to represent the returned objects would be incredibly time-consuming.
In our first series, we created a basic Graph application that read the signed-in user’s calendar data. There, we called the Graph API directly to keep our introduction simple. But when we’re building more complex Graph applications, we’d much rather use a refined, type-checked method to access the API.
Fortunately, Microsoft provides typed libraries with class mapping to the entities exposed by the Graph API. Using libraries like those in the Microsoft Graph SDK for Java allows our app to navigate the Graph API in a type-safe and IDE-friendly manner.
First, though, we’ll need to create an authentication provider that lets our Java app call the Graph API on behalf of the user. So, in this article, we’ll create an authentication provider and look at how to use the Graph API client from a Spring Boot web application.
Add the Graph Client Dependency
The first step is to add the Graph client dependency. This is provided through the Microsoft Graph Maven dependency.
<dependency>
<groupId>com.microsoft.graph</groupId>
<artifactId>microsoft-graph</artifactId>
<version>5.4.0</version>
</dependency>
Query the Graph API
To use the Graph API client, we call GraphServiceClient
, which exposes a builder interface to all the available endpoints. Here, we call the me
endpoint, which returns information about the currently logged-in user:
GraphServiceClient.builder
.authenticationProvider (new SomeAuthenticationProviderGoesHere)
.buildClient
.me
.buildRequest
.get;
Note that we’ve used a placeholder class when calling the authenticationProvider
method.
To call the Graph API, we need to pass a valid access token. This token is generated by an implementation of the IAuthenticationProvider
interface and passed to the Graph client via the authenticationProvider
method. The SDK provides many such implementations out of the box but doesn’t provide an implementation for Java applications requiring an on-behalf-of (OBO) token typically generated by an OAuth2 resource server.
Fortunately, we can create our own authentication provider.
Generate OBO Tokens
An OBO token is generated by passing the JSON Web Token (JWT) received by a resource server to an OAuth2 authorization server as part of a specially formatted HTTP request. The Microsoft documentation provides a detailed explanation of the HTTP request.
In addition to raw HTTP calls, the OBO flow can be completed using the classes available in the MSAL library. We can see an example of the OBO flow implemented in the AADOBOOAuth2AuthorizedClientProvider class. Using this existing code as inspiration, we can generate our own authentication provider called OboAuthenticationProvider
. To follow along, have a look at the complete code for this class, in the following package:
package com.matthewcasperson.onenotebackend.providers;
Let’s break this code down.
Our class extends the BaseAuthenticationProvider
class. This in turn implements the IAuthenticationProvider
interface.
public class OboAuthenticationProvider extends BaseAuthenticationProvider {
We require details of the Azure AD application to complete the request, including the client ID, client secret, tenant ID, and the scopes we’re requesting access to:
private final String tenantId;
private final String clientId;
private final String clientSecret;
private final Set<String> scopes;
public OboAuthenticationProvider(
final Set<String> scopes,
final String tenantId,
final String clientId,
final String clientSecret) {
this.scopes = scopes;
this.tenantId = tenantId;
this.clientId = clientId;
this.clientSecret = clientSecret;
}
The getAuthorizationTokenAsync
method returns a future containing null
if no token is required for the supplied URL or if the OBO token returned from the authorization server:
@Nonnull
@Override
public CompletableFuture<String> getAuthorizationTokenAsync(@Nonnull final URL url) {
Not all requests require a token. The shouldAuthenticateRequestWithUrl
method will return false
if the requested URL doesn’t require a token. In this case, we return null
to the caller.
if (!shouldAuthenticateRequestWithUrl(url)) {
return CompletableFuture.completedFuture(null);
}
For requests that do require a token, we start by reading the access token that was passed to the resource server by the caller.
final AbstractOAuth2TokenAuthenticationToken oauth2Token =
(AbstractOAuth2TokenAuthenticationToken) SecurityContextHolder
.getContext()
.getAuthentication();
final String accessToken = oauth2Token.getToken().getTokenValue();
The parameters to pass to the authorization server are captured by the OnBehalfOfParameters
class:
final OnBehalfOfParameters parameters = OnBehalfOfParameters
.builder(scopes, new UserAssertion(accessToken))
.build();
These parameters are then used to acquire a token from a ConfidentialClientApplication
, and the resulting token is returned to the caller:
return createApp()
.map(a -> a.acquireToken(parameters).thenApply(IAuthenticationResult::accessToken))
.orElse(CompletableFuture.failedFuture(new Exception("Failed to generate obo token.")));
}
We create a ConfidentialClientApplication
with the createApp
method.
private Optional<ConfidentialClientApplication> createApp() {
The client requires an authority value — which is based on the tenant ID — as well as the client secret:
final String authority = "https://login.microsoftonline.com/" + tenantId;
final IClientSecret clientCredential = ClientCredentialFactory.createFromSecret(clientSecret);
We then create a ConfidentialClientApplication
through the builder interface.
try {
return Optional
.of(ConfidentialClientApplication.builder(clientId, clientCredential)
.authority(authority)
.build());
} catch (final MalformedURLException e) {
LOGGER.error("Failed to create ConfidentialClientApplication", e);
}
return Optional.empty();
}
Access the Azure AD Application Details
To create a new instance of the OboAuthenticationProvider
class, we need access to the Azure AD application client ID, client secret, and tenant ID.
In a typical Azure AD-enabled Spring application, these values are defined in the application.properties file or application.yml file. Here’s an example of an application.yml:
azure:
activedirectory:
client-id: ${CLIENT_ID}
client-secret: ${CLIENT_SECRET}
app-id-uri: ${APP_URI}
tenant-id: ${TENANT_ID}
We can inject an instance of AADAuthenticationProperties
into our Spring beans to access these properties:
@Autowired
AADAuthenticationProperties azureAd;
We then use the AADAuthenticationProperties
getters to access the required properties, allowing us to create a new instance of the OboAuthenticationProvider
class.
GraphServiceClient.builder()
.authenticationProvider(new OboAuthenticationProvider(
Set.of("https://graph.microsoft.com/user.read"),
azureAd.getTenantId(),
azureAd.getClientId(),
azureAd.getClientSecret()))
.buildClient()
.me()
.buildRequest()
.get();
And with that, we now can query the Graph API from our Spring resource server through the Graph API client.
Conclusion
In this article, we learned how to add the Microsoft Graph API client to our Spring application, and how to build an authentication provider that generates OBO tokens from the access token passed in by a caller. This gives us the foundation we need to create resource servers acting as a gateway between our front-end application and the Graph API.
In the second article of this series, we’ll use the Graph API client to consume OneNote documents through a microservice that allows them to be converted into Markdown format.