javaazuremicrosoft-graph-apimsaloutlook-api

401 code return when read mail by msal Credential Grant, the access token is correct and /users is success


I need read email from my hotmail account,my app is a daemon, so I use client secret mode to get tokens, yes, its success got token, and get /users is success also, however when i try to get /users/{id}/messages, code 401 response with empty body.

Account type: Accounts in any organizational directory (Any Microsoft Entra ID tenant - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)

Access tokens (used for implicit flows)  ----- checked
ID tokens (used for implicit and hybrid flows)----- checked

Api permissions

I use {tenant} as my Tenant Id, not /common and others

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import com.microsoft.aad.msal4j.ClientCredentialFactory;
import com.microsoft.aad.msal4j.ClientCredentialParameters;
import com.microsoft.aad.msal4j.ConfidentialClientApplication;
import com.microsoft.aad.msal4j.IAuthenticationResult;
import com.nimbusds.oauth2.sdk.http.HTTPResponse;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Collections;
import java.util.Properties;
import java.util.concurrent.CompletableFuture;

class ClientCredentialGrant {

private static String authority;
private static String clientId;
private static String secret;
private static String scope;
private static ConfidentialClientApplication app;

public static void main(String args[]) throws Exception{

    setUpSampleData();

    try {
        BuildConfidentialClientObject();
        IAuthenticationResult result = getAccessTokenByClientCredentialGrant();
        System.out.println(result.accessToken());
        String usersListFromGraph = getUsersListFromGraph(result.accessToken());
        System.out.println("Users in the Tenant = " + usersListFromGraph);

        String mailsListFromGraph = getEmailsFromGraph(result.accessToken());
        System.out.println("Mails in the Tenant = " + mailsListFromGraph);

    } catch(Exception ex){
        System.out.println("Oops! We have an exception of type - " + ex.getClass());
        System.out.println("Exception message - " + ex.getMessage());
        throw ex;
    }
}
private static void BuildConfidentialClientObject() throws Exception {
    
    // Load properties file and set properties used throughout the sample
    app = ConfidentialClientApplication.builder(
            clientId,
            ClientCredentialFactory.createFromSecret(secret))
            .authority(authority)
            .build();               
}

private static IAuthenticationResult getAccessTokenByClientCredentialGrant() throws Exception {
    
    // With client credentials flows the scope is ALWAYS of the shape "resource/.default", as the
    // application permissions need to be set statically (in the portal), and then granted by a tenant administrator
    ClientCredentialParameters clientCredentialParam = ClientCredentialParameters.builder(
            Collections.singleton(scope))
            .build();
    
    CompletableFuture<IAuthenticationResult> future = app.acquireToken(clientCredentialParam);
    return future.get();
}

private static String getUsersListFromGraph(String accessToken) throws IOException {
    URL url = new URL("https://graph.microsoft.com/v1.0/users");
    HttpURLConnection conn = (HttpURLConnection) url.openConnection();

    conn.setRequestMethod("GET");
    conn.setRequestProperty("Authorization", "Bearer " + accessToken);
    conn.setRequestProperty("Accept","application/json");

    int httpResponseCode = conn.getResponseCode();
    if(httpResponseCode == HTTPResponse.SC_OK) {

        StringBuilder response;
        try(BufferedReader in = new BufferedReader(
                new InputStreamReader(conn.getInputStream()))){

            String inputLine;
            response = new StringBuilder();
            while (( inputLine = in.readLine()) != null) {
                response.append(inputLine);
            }
        }
        return response.toString();
    } else {
        return String.format("Connection returned HTTP code: %s with message: %s",
                httpResponseCode, conn.getResponseMessage());
    }
}


private static String getEmailsFromGraph(String accessToken) throws IOException {
    URL url = new URL("https://graph.microsoft.com/v1.0/users/9dfb85a5-b8bb-4e7d-8e05-a60d418ca16d/messages?$select=sender,subject");
    HttpURLConnection conn = (HttpURLConnection) url.openConnection();

    conn.setRequestMethod("GET");
    conn.setRequestProperty("Authorization", "Bearer " + accessToken);
    conn.setRequestProperty("Accept","application/json");

    int httpResponseCode = conn.getResponseCode();
    if(httpResponseCode == HTTPResponse.SC_OK) {

        StringBuilder response;
        try(BufferedReader in = new BufferedReader(
                new InputStreamReader(conn.getInputStream()))){

            String inputLine;
            response = new StringBuilder();
            while (( inputLine = in.readLine()) != null) {
                response.append(inputLine);
            }
        }
        return response.toString();
    } else {
        return String.format("Connection returned HTTP code: %s with message: %s",
                httpResponseCode, conn.getResponseMessage());
    }
}

/**
 * Helper function unique to this sample setting. In a real application these wouldn't be so hardcoded, for example
 * different users may need different authority endpoints or scopes
 */
private static void setUpSampleData() throws IOException {
    // Load properties file and set properties used throughout the sample
    Properties properties = new Properties();
    properties.load(Thread.currentThread().getContextClassLoader().getResourceAsStream("application.properties"));
    authority = properties.getProperty("AUTHORITY");
    clientId = properties.getProperty("CLIENT_ID");
    secret = properties.getProperty("SECRET");
    scope = properties.getProperty("SCOPE");
}

}

token ... CXlTBW8OAh4HMED7UKSFqvuD_YSalGZ1tiSZwWRyNB-_Cn-jhAO1fDBuPPOKjrz8XOWAdQSa7ipGj4gTDiPwNeNdw4d2Qg7G4lny5D2Hav9BROxzQSx82eilA5w-qJOMaOuSLRHJTj1usFTnlCqaPoZWUQoF9v2AxqRBBMe105-nnZmSuoeiOobjFFtx8X8l4FaKACj4zwncW8igLsHSslU9YeQ_gXXVUiYgTEGhokMKj5IFSOMngiU2GuY4pVBWsHpHXKAilHXl5D2_pA
Users in the Tenant = {"@odata.context":"https://graph.microsoft.com/v1.0/$metadata#users","value":[{"businessPhones":[],"displayName":"caron_liu", .... "d418ca16d"}]}
Mails in the Tenant = Connection returned HTTP code: 401 with message: Unauthorized

I donwload the sample code form github: https://github.com/Azure-Samples/ms-identity-java-daemon

Is there somebody help me? thanks.


Solution

  • Note that, you need Delegated type permissions to read mails of personal Microsoft accounts like Hotmail but client credential flow uses Application type permissions that won't work in your scenario.

    To resolve the error, you need to switch to delegated flow like interactive flow or authorization code flow by granting Delegated type permissions and use /common token endpoint as authority.

    Initially, I registered one application and granted Mail.Read permission of Delegated type with consent:

    enter image description here

    Now, I added redirect URI as http://localhost in Mobile and desktop applications platform like this:

    enter image description here

    Make sure to enable public client flow option that is required while using interactive flow:

    enter image description here

    In my case, I used below code files that invokes interactive flow for token generation with /common endpoint and retrieves mail:

    application.properties:

    CLIENT_ID=yourAppId
    AUTHORITY=https://login.microsoftonline.com/common/  
    REDIRECT_URI=http://localhost  
    SCOPE=Mail.Read
    

    Main.java:

    package com.example;
    
    import com.microsoft.aad.msal4j.*;
    
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.net.HttpURLConnection;
    import java.net.URI;
    import java.net.URL;
    import java.util.*;
    import java.util.concurrent.*;
    
    public class Main {
    
        private static String clientId;
        private static String authority;
        private static String redirectUri;
        private static String scope;
    
        public static void main(String[] args) throws Exception {
            loadConfig();
    
            PublicClientApplication app = PublicClientApplication.builder(clientId)
                    .authority(authority)
                    .build();
    
            Set<String> scopes = Collections.singleton(scope);
            IAuthenticationResult result;
    
            Optional<IAccount> firstAccount = app.getAccounts().get().stream().findFirst();
            if (firstAccount.isPresent()) {
                try {
                    SilentParameters silentParams = SilentParameters.builder(scopes, firstAccount.get()).build();
                    result = app.acquireTokenSilently(silentParams).get();
                    System.out.println("Token acquired silently.");
                } catch (ExecutionException ex) {
                    System.out.println("Silent token acquisition failed: " + ex.getCause().getMessage());
                    result = acquireInteractive(app, scopes);
                }
            } else {
                result = acquireInteractive(app, scopes);
            }
    
            System.out.println("Access Token:\n" + result.accessToken());
    
            String emailsJson = getEmailsFromGraph(result.accessToken());
            System.out.println("\nInbox Messages:\n");
    
            String[] items = emailsJson.split("\"subject\"");
            for (int i = 1; i < items.length; i++) {
                if (!items[i].contains("\"name\"") || !items[i].contains("\"address\"")) {
                    continue;
                }
    
                String subject = items[i].split(",")[0].split(":")[1].replace("\"", "");
                String senderName = items[i].split("\"name\":\"")[1].split("\"")[0];
                String senderEmail = items[i].split("\"address\":\"")[1].split("\"")[0];
    
                System.out.println("Subject : " + subject);
                System.out.println("From    : " + senderName + " <" + senderEmail + ">");
                System.out.println();
            }
        }
    
        private static IAuthenticationResult acquireInteractive(PublicClientApplication app, Set<String> scopes) throws Exception {
            InteractiveRequestParameters parameters = InteractiveRequestParameters.builder(new URI(redirectUri)).scopes(scopes).build();
            return app.acquireToken(parameters).get();
        }
    
        private static String getEmailsFromGraph(String accessToken) throws IOException {
            URL url = new URL("https://graph.microsoft.com/v1.0/me/messages?$select=subject,sender");  
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("GET");
            conn.setRequestProperty("Authorization", "Bearer " + accessToken);
            conn.setRequestProperty("Accept", "application/json");
    
            int responseCode = conn.getResponseCode();
            if (responseCode == 200) {
                BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
                StringBuilder content = new StringBuilder();
                String line;
                while ((line = in.readLine()) != null) {
                    content.append(line);
                }
                in.close();
                return content.toString();
            } else {
                return "Error: " + responseCode + " - " + conn.getResponseMessage();
            }
        }
    
        private static void loadConfig() throws IOException {
            Properties props = new Properties();
            try (InputStream input = Thread.currentThread().getContextClassLoader().getResourceAsStream("application.properties")) {
                props.load(input);
            }
            clientId = props.getProperty("CLIENT_ID");
            authority = props.getProperty("AUTHORITY");
            redirectUri = props.getProperty("REDIRECT_URI");
            scope = props.getProperty("SCOPE");
        }
    }
    

    Response:

    enter image description here

    Reference:

    OAuth 2.0 Token Endpoints - Microsoft identity platform