In my previous article , I showed you how to modify our great Graph Client for Java sample to add some additional options for things like filtering, setting the max retries for 429 errors, etc.  That sample uses the Oauth2 Device Code flow.  In this article, I will show you how to convert that and use the Client Credentials Flow.  Although, you will not be able to retrieve the same information as in the previous example, since the client credentials relies on application permissions instead of delegated permissions ( see this great article by Bac Hoang for the differences between the two ). I have modified it to show some examples using this flow.  If you haven’t already downloaded and gotten the sample project working, please do so here.

Update the App Registration

The first step you will need to make is go to the azure portal and to the app registration you created for the sample app.  You will need to add a client secret to the app registration like so:

Be sure to copy the secret that is generated and store that somewhere secure as you will not be able to retrieve that later if you don’t ( see how my value is masked with ****** ). Next, we need to add some application permissions to the app registration.  For this change, let’s add the Microsoft Graph Application Permission “User.Read.All” and “Calendars.ReadWrite”.  You can remove the delegated permissions if you like or leave them if you want the previous sample to still work.  Be sure to provide Admin Consent.

Now, back to the project to make our code changes…

Modify the java code

You will need to modify the file src\main\resources\com\contoso\oAuth.properties to change the scopes value ( I just commented out my old version ) and add the client secret.  For this, I am using the short hand notation of /.default for the Microsoft Graph endpoint:  https://graph.microsoft.com/.default otherwise, I would have to list each one separately like so:  https://graph.microsoft.com/Calendars.ReadWrite https://graph.microsoft.com/User.Read.All
As you can see, it is easier to just use /.default and only have to write the resource one time.

Next, let’s modify the file Graph.java under src\mail\java\com\contoso

Add the import: import com.microsoft.graph.models.extensions.Calendar;  at the top.

Let’s modify the method getUser.  We will add an additional parameter to specify the user since we are doing a client credentials flow, there is no user context so we need this method to look up a user based on a User Principal Name (upn) we are going to pass in.  You can see where I just commented out the old “me” code:

    public static User getUser(String accessToken, String upn) {
        ensureGraphClient(accessToken);

        // GET /me to get authenticated user
        /*
        User me = graphClient
            .me()
            .buildRequest()
            .get();
        */

        try{
            User user = graphClient
                .users(upn)
                .buildRequest()
                .get();
            return user;       
        } catch ( Exception ex ) {
            System.out.println("Error getting user " + ex.getMessage());
            return null;
        }
    }

We will need to create a GetCalendar method:

    public static Calendar GetCalendar(String accessToken, String upn){
        ensureGraphClient(accessToken);
        try {
            Calendar cal = graphClient
                .users(upn)
                .calendar()
                .buildRequest()
                .get();
            return cal;
        } catch (Exception ex){
            System.out.println("Error getting calendar " + ex.getMessage());
            return null;
        }
    }

Next, let’s modify Authentication.java to use the new client credentials.

Some new imports that need to be added:
import com.microsoft.aad.msal4j.ClientCredentialFactory;
import com.microsoft.aad.msal4j.ClientCredentialParameters;
import com.microsoft.aad.msal4j.ConfidentialClientApplication;
import com.microsoft.aad.msal4j.IClientCredential;
import com.microsoft.aad.msal4j.SilentParameters;
import java.net.MalformedURLException;

In public class Authentication, add a new static variable:  clientSecret

    private static String applicationId;
    // Set authority to allow only organizational accounts
    // Device code flow only supports organizational accounts
    private static String authority;
    private static String clientSecret;

Modify the initialize method like so:

    public static void initialize(String applicationId, String authority, String clientSecret) {
        Authentication.authority = authority;
        Authentication.applicationId = applicationId;
        Authentication.clientSecret = clientSecret;
    }

You can next comment out all of the devicecode flow code like so:

        /*
        PublicClientApplication app;
        try {
            // Build the MSAL application object with
            // app ID and authority
            app = PublicClientApplication.builder(applicationId)
                .authority(authority)
                .build();
        } catch (MalformedURLException e) {
            return null;
        }

        
        // Create consumer to receive the DeviceCode object
        // This method gets executed during the flow and provides
        // the URL the user logs into and the device code to enter
        Consumer<DeviceCode> deviceCodeConsumer = (DeviceCode deviceCode) -> {
            // Print the login information to the console
            System.out.println(deviceCode.message());
        };

        // Request a token, passing the requested permission scopes
        IAuthenticationResult result = app.acquireToken(
            DeviceCodeFlowParameters
                .builder(scopeSet, deviceCodeConsumer)
                .build()
        ).exceptionally(ex -> {
            System.out.println("Unable to authenticate - " + ex.getMessage());
            return null;
        }).join();
        */

Then, below that comment block but before the if(result != null) block, add this code:

		IClientCredential cred = ClientCredentialFactory.createFromSecret(clientSecret);
        ConfidentialClientApplication app;
        try {
            // Build the MSAL application object for a client credential flow
            app = ConfidentialClientApplication.builder(applicationId, cred ).authority(authority).build();
        } catch (MalformedURLException e) {
            System.out.println("Error creating confidential client: " + e.getMessage());
            return null;
        }
        
        IAuthenticationResult result;
        try{
            SilentParameters silentParameters = SilentParameters.builder(scopeSet).build();
            result= app.acquireTokenSilently(silentParameters).join();
        } catch (Exception ex ){
            if (ex.getCause() instanceof MsalException) {

                ClientCredentialParameters parameters =
                        ClientCredentialParameters
                                .builder(scopeSet)
                                .build();
   
                // Try to acquire a token. If successful, you should see
                // the token information printed out to console
                result = app.acquireToken(parameters).join();
            } else {
                // Handle other exceptions accordingly
                System.out.println("Unable to authenticate = " + ex.getMessage());
                return null;
            }
        }

My Full file looks like this:

package com.contoso;

import java.net.MalformedURLException;
// import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Consumer;

import javax.security.auth.login.Configuration.Parameters;

import com.microsoft.aad.msal4j.AuthorizationCodeParameters;
import com.microsoft.aad.msal4j.ClientCredentialFactory;
import com.microsoft.aad.msal4j.ClientCredentialParameters;
import com.microsoft.aad.msal4j.ConfidentialClientApplication;
import com.microsoft.aad.msal4j.DeviceCode;
import com.microsoft.aad.msal4j.DeviceCodeFlowParameters;
import com.microsoft.aad.msal4j.IAuthenticationResult;
import com.microsoft.aad.msal4j.IClientCredential;
import com.microsoft.aad.msal4j.MsalException;
import com.microsoft.aad.msal4j.PublicClientApplication;
import com.microsoft.aad.msal4j.SilentParameters;


/**
 * Authentication
 */
public class Authentication {

    private static String applicationId;
    // Set authority to allow only organizational accounts
    // Device code flow only supports organizational accounts
    private static String authority;
    private static String clientSecret;

    public static void initialize(String applicationId, String authority, String clientSecret) {
        Authentication.authority = authority;
        Authentication.applicationId = applicationId;
        Authentication.clientSecret = clientSecret;
    }

    public static String getUserAccessToken(String[] scopes) {
        if (applicationId == null) {
            System.out.println("You must initialize Authentication before calling getUserAccessToken");
            return null;
        }

         Set<String> scopeSet = new HashSet<>();
        
        Collections.addAll(scopeSet, scopes);
        /*
        PublicClientApplication app;
        try {
            // Build the MSAL application object with
            // app ID and authority
            app = PublicClientApplication.builder(applicationId)
                .authority(authority)
                .build();
        } catch (MalformedURLException e) {
            return null;
        }

        
        // Create consumer to receive the DeviceCode object
        // This method gets executed during the flow and provides
        // the URL the user logs into and the device code to enter
        Consumer<DeviceCode> deviceCodeConsumer = (DeviceCode deviceCode) -> {
            // Print the login information to the console
            System.out.println(deviceCode.message());
        };

        // Request a token, passing the requested permission scopes
        IAuthenticationResult result = app.acquireToken(
            DeviceCodeFlowParameters
                .builder(scopeSet, deviceCodeConsumer)
                .build()
        ).exceptionally(ex -> {
            System.out.println("Unable to authenticate - " + ex.getMessage());
            return null;
        }).join();
        */

        
        IClientCredential cred = ClientCredentialFactory.createFromSecret(clientSecret);
        ConfidentialClientApplication app;
        try {
            // Build the MSAL application object for a client credential flow
            app = ConfidentialClientApplication.builder(applicationId, cred ).authority(authority).build();
        } catch (MalformedURLException e) {
            System.out.println("Error creating confidential client: " + e.getMessage());
            return null;
        }
        
        IAuthenticationResult result;
        try{
            SilentParameters silentParameters = SilentParameters.builder(scopeSet).build();
            result= app.acquireTokenSilently(silentParameters).join();
        } catch (Exception ex ){
            if (ex.getCause() instanceof MsalException) {

                ClientCredentialParameters parameters =
                        ClientCredentialParameters
                                .builder(scopeSet)
                                .build();
   
                // Try to acquire a token. If successful, you should see
                // the token information printed out to console
                result = app.acquireToken(parameters).join();
            } else {
                // Handle other exceptions accordingly
                System.out.println("Unable to authenticate = " + ex.getMessage());
                return null;
            }
        }


        if (result != null) {
            // System.out.println("Access Token - " + result.accessToken());
            return result.accessToken();
        }

        return null;
    }
}

Finally, let’s modify the App.java file located in the same folder.

You will need to add a final String for the clientSecret like so:
        final String clientSecret = oAuthProperties.getProperty(“app.clientSecret”);

Then, for authentication to occur, you can initialize authentication with this block:

        Authentication.initialize(appId, authority, clientSecret);
        final String accessToken = Authentication.getUserAccessToken(appScopes);         System.out.println(“Access token = ” + accessToken

The rest of the code is to add some additional functionality to work with the new permissions we have setup.  I have added a promptForUPN method so we can change the upn of the user we are working with and a couple of new choice options in the menu.  The full file is here:

package com.contoso;

import java.util.InputMismatchException;
import java.util.Scanner;

import com.microsoft.graph.models.extensions.DateTimeTimeZone;
import com.microsoft.graph.models.extensions.Event;
import com.microsoft.graph.models.extensions.User;
import com.microsoft.graph.models.extensions.Calendar;

import java.io.Console;
import java.io.IOException;
import java.util.Properties;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.List;

/**
 * Graph Tutorial
 *
 */
public class App {
    public static void main(String[] args) {
        System.out.println("Java Graph Tutorial");
        System.out.println();

        // Load OAuth settings
        final Properties oAuthProperties = new Properties();
        try {
            oAuthProperties.load(App.class.getResourceAsStream("oAuth.properties"));
        } catch (IOException e) {
            System.out.println("Unable to read OAuth configuration. Make sure you have a properly formatted oAuth.properties file. See README for details.");
            return;
        }

        final String appId = oAuthProperties.getProperty("app.id");
        final String[] appScopes = oAuthProperties.getProperty("app.scopes").split(",");
        final String authority = oAuthProperties.getProperty("app.authority");
        final String clientSecret = oAuthProperties.getProperty("app.clientSecret");

        // Get an access token
        //Authentication.initialize(appId, authority);j
        Authentication.initialize(appId, authority, clientSecret);
        final String accessToken = Authentication.getUserAccessToken(appScopes);
        System.out.println("Access token = " + accessToken);
        
        String upn = promptForUPN();
        
        // Greet the user
        User user = Graph.getUser(accessToken,upn);
        if(user!=null){
            System.out.println("You have select user " + user.displayName);
        }
        
        Scanner input = new Scanner(System.in);

        int choice = -1;

        while (choice != 0) {
            System.out.println();
            System.out.println("Please choose one of the following options:");
            System.out.println("0. Exit");
            System.out.println("1. Display access token");
            System.out.println("2. Input upn to work with");
            System.out.println("3. Get this users info");
            System.out.println("4. Get this users calender");

            try {
                choice = input.nextInt();
            } catch (InputMismatchException ex) {
                // Skip over non-integer input
                input.nextLine();
            }

            // Process user choice
            switch(choice) {
                case 0:
                    // Exit the program
                    System.out.println("Goodbye...");
                    break;
                case 1:
                    // Display access token
                    System.out.println("Access token: " + accessToken);
                    break;
                case 2:
                    upn = promptForUPN();
        
                    // Greet the user
                    user = Graph.getUser(accessToken,upn);
                    if(user!=null){
                        System.out.println("You have selected user " + user.displayName);
                    }
                    break;

                case 3:
                    if(user!=null){
                        System.out.println("User info:");
                        System.out.println("    id= " + user.id);
                        System.out.println("    mail= " + user.mail);
                    } else {
                        System.out.println("*** No user selected ***");
                    }
                    break;
                case 4:
                    if(user!=null){
                        Calendar cal = Graph.GetCalendar(accessToken,upn);
                        System.out.println("Calendar info:");
                        System.out.println("    id= " + cal.id );                        
                    } else {
                        System.out.println("*** No user selected ***");
                    }
                    break;
                default:
                    System.out.println("Invalid choice");
            }
        }

        input.close();
    }

    private static String formatDateTimeTimeZone(DateTimeTimeZone date) {
        LocalDateTime dateTime = LocalDateTime.parse(date.dateTime);
    
        return dateTime.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT)) + " (" + date.timeZone + ")";
    }

    private static String promptForUPN(){
        String upn;
        Console console = System.console();
        upn = console.readLine("Enter user upn: ");
        return upn;
    }
  }

Reference for the client credentials provider for java:  https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-daemon-acquire-token?tabs=java#acquiretokenforclient-api

Run the code

You will notice now when you run the project, you are not prompted for any kind of authentication.  Instead, right away, you will see the access token come back in the console window, and then we will be prompted to enter a upn so we can get details about this user:

Looking at the access token in http://jwt.ms you can see we have an application only token for Microsoft graph:

After the access token gets displayed and the initial user information for the upn you entered, you will get the menu of actions you can use for this app, including being prompted for a new upn if desired.

Press 3 to get this users info and you will get the id and mail attribute ( if set ).

Summary

As you can see, we are now able to look up users using the client credentials flow.  This is only recommended for scenarios where the application is running in a secure environment.  I do not recommend this flow being used in any kind of distributed application as the client secret is easily retrieved.  This should be server side scripts that users do not have access to.  Also, be careful when assigning application permissions because you can potentially give the application far more permissions than is needed which may put you in a compromised state.

0 0 vote
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments