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
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.
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:
Now, I added redirect URI as http://localhost in Mobile and desktop applications platform like this:
Make sure to enable public client flow option that is required while using interactive flow:
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:
Reference: