Office 365 is becoming the attention centre of many companies those days. Integration of Office 365 in their portal become needed to add features like registering to an event in your Calendar, creating a contact directory from Azure AD or showing a message from a specific Teams. The Microsoft documentation explains well how to use the Microsoft Graph API with many technologies, but when it’s time to make it work in a java web application it can be challenging. Adding Liferay portal in the stack become a quest. The most complex part is authenticating your user with OAuth 2.0.
There are several pieces to put together for everything to work well for your users:
- Register your application in Azure (see instruction in the azure doc)
- Use ScribeJava to manage the OAuth 2.0 authentication
- Integrate the authentication process with Liferay
- Use the Microsoft Graph SDK for Java to interact with Office 365
1
2
3
4
5
6 | OAuth20Service azureAuthService = new ServiceBuilder(configuration.apiKey())
.withScope("openid offline_access Calendars.ReadWrite")
.apiKey(configuration.apiKey())
.apiSecret(configuration.apiSecret())
.callback(authentication.getCallBackURL())
.build(MicrosoftAzureActiveDirectory20Api.instance());
|
configuration is a configuration object that contains the definition of our application in Azure
The scope is the requested access to the Microsoft graph api, openid is required to do the authentication, offline_access give access to the api even if the user isn’t active, this gives us access to the refresh token that is useful to reduce the number of redirection the user has to do when he comme back.
The callbackURL is the URL of your application where the user will be redirected after authentication, we will see how to use it later.
Once we have our azureAuthService there are three useful calls that we can make.
Get the authorization URL:
1
2
3
4
5 | Map<String, String> params = new HashMap<>();
params.put("response_mode","form_post");
params.put("state",backURL);
params.put("prompt",prompt);
String authorizationUrl = authService.getAuthorizationUrl(params);
|
We use the response_mode = form_post that will send a POST request to our application callbackURL. This is more secure than using a GET request and also cleaner for the users.
The state will be sent back to our callbackUrl unchanged, we will use it to know where to send the user after the completed authentication.
The prompt let us configure the Microsoft login prompt, more about it soon.
We can then send our users to the authorizationUrl and receive an id_token after a successful authentication.
Get the access token:
1 | OAuth2AccessToken accessToken = authService.getAccessToken(id_token);
|
The ID_token received must then be validated and used to get an accessToken that will be usable to sign our following request to the Graph API.
Get a new access token when it’s expired:
1 | OAuth2AccessToken accessToken = authService.refreshAccessToken(refreshToken);
|
The access token is valid for a short time, usually less than an hour. With the refresh token, we can get a new one without having to send the user to the authorizationUrl again. The refresh token is usually valid for at least a month but can be revoked from the Azure side.
Integrate the OAuth 2.0 Authentication Workflow in a Java Web Application
The next step is to integrate the user redirection flow needed to authenticate the user. There is a good explanation of the flow in the Microsoft documentation. To summarize, we need to :
- Redirect the user to the Microsoft authorizationUrl
- Handle the user being redirected to our callbackURL and validate his token
- Redirect the user to the page where he wanted to go
To process the authentication in a web application, we need to have complete control of the request, using a filter is all designed (this filter could easily be reworked to work outside Liferay). The filter needs to listen to your callbackURL :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52 | protected void processFilter(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws Exception {
try {
String code = ParamUtil.get(request, "code", "");
O365Authentication authentication = authenticationService.getAuthentication(request);
if(Validator.isNull(code)) {
String backURL = ParamUtil.getString(request, AuthenticationService.BACK_URL_PARAM, "/");
// step 0 - test if user is already authenticated
if(authenticationService.isConnected(authentication, authenticatedServiceTracker.getScope())){
String contextUrl = HttpUtil.decodeURL(backURL);
response.sendRedirect(contextUrl);
return;
}
// Step 1 - redirection to o365 authentication
String error = ParamUtil.getString(request, "error");
String prompt = AuthenticationService.PROMPT_NONE;
switch (error) {
case "login_required":
case "interaction_required":
prompt = AuthenticationService.PROMPT_LOGIN;
break;
case "consent_required":
prompt = AuthenticationService.PROMPT_CONSENT;
break;
case "access_denied":
// the user refuse to give access, we return it to the portal page
String state = ParamUtil.getString(request, "state");
String contextUrl = HttpUtil.decodeURL(state);
LOG.debug("Send user to " + contextUrl);
response.sendRedirect(contextUrl);
return;
}
String authenticationURL = authenticationService.getAuthenticationURL(authentication, backURL, prompt);
LOG.debug("Send user to "+authenticationURL);
response.sendRedirect(authenticationURL);
} else {
// Step 2 - token validation
LOG.debug("Received id_token: "+code);
authenticationService.validateIdToken(authentication, code);
String state = ParamUtil.getString(request, "state");
String contextUrl = HttpUtil.decodeURL(state);
LOG.debug("Send user to "+contextUrl);
response.sendRedirect(contextUrl);
}
} catch (Exception e) {
throw new PortalException("Cannot authenticate user to o365", e);
}
}
|
The parameter code contains the id_token when the user successfully authenticate.
We use only one filter to handle the whole process. We need to first send the user to this filter, as no code will be received at that time, we process to step 0.
Step 0
To keep the authentication the less intrusive possible to the user, we check if we already have a valid accessToken. authenticationService.isConnected does this verification and if needed will try to get a new accessToken from the refreshToken automatically.
If we do have a valid accessToken, we directly redirect the user to his destination page. If we do not, we continue to step 1.
Step 1
We need to redirect the user to the authenticationURL. This URL is customized considering the actual user situation. In the best case, the user is already logged in Office 365, which is a high probability in an corporate setup. We first try to authenticate him without prompt on the Microsoft side, this only shows a blank page to the user while he is redirected. If that fail, we receive an interaction_required error, we can then redirect again the user with a login prompt that time, in that case, the user will see a blank page for a short time then the Microsoft login prompt, beside the URL change he wouldn’t notice being redirected 4 times. There are still two particular error cases, consent_required which happen if we ask more authorization than the user already consented, in this case we redirect the user to the authenticationURL with the PROMPT_CONSENT parameter, Microsoft login will directly show the consent form instead of going through the login form. The other case is access_denied, in this case, the user, or his admin, didn’t accept the required authorization, there is nothing we can do about it, except notify the user, so we redirect him to his destination page and handle the feedback from the API usage.
Step 2
The second step happens when the user successfully login to the Microsoft side and accept all authorization requested. We then trigger the token validation which will persist the authentication detail. We then redirect the user to his destination page.
Using the Microsoft Graph SDK for Java
The MsGraph SDK for java already has a good usage documentation. The only missing part is how to provide the authentication coming from ScribeJava to the MsGraph SDK. We have to write an AuthenticationProvider that will be used to add the authentication to every request sent from the SDK. Here is the implementation of the AuthenticationProvider:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | final class ScribejavaAuthenticationProvider implements IAuthenticationProvider {
private final O365Authentication authentication;
private static final String AUTHORIZATION_HEADER_NAME = "Authorization";
private static final String OAUTH_BEARER_PREFIX = "bearer ";
ScribejavaAuthenticationProvider(O365Authentication authentication) {
if(authentication == null || authentication.getAccessToken()==null){
throw new RuntimeException("User need to be logged in Office 365 before calling the API." );
}
this.authentication = authentication;
}
@Override
public void authenticateRequest(IHttpRequest request) {
OAuth2AccessToken accessToken = (OAuth2AccessToken) authentication.getAccessToken();
request.addHeader(AUTHORIZATION_HEADER_NAME, OAUTH_BEARER_PREFIX + accessToken.getAccessToken());
}
}
|
And we can finally register an event in our calendar:
1
2
3
4
5
6
7 | IAuthenticationProvider authenticationProvider = new ScribejavaAuthenticationProvider(authentication);
IGraphServiceClient graphClient = GraphServiceClient.builder().authenticationProvider(authenticationProvider).buildClient();
Event event = new Event();
// I let you create the event as you need
graphClient.me().events().buildRequest().post(event);
|
Final Words
I’ve shown here how to set up an OAuth 2.0 authentication with office 365 in a java web application. This setup could be used to access any OAuth 2.0 available api and although some Liferay tools were used in it, they are easy to replace with more common tool if your use case is outside of Liferay. The full working code is available here : https://github.com/savoirfairelinux/office-365-integration
As I said at the beginning, integrating this workflow in Liferay has some more gotcha. I’ll look at them in a next blog post.