Jump to content
Officeapi

Got message 'HTTP/1.1 401 Unauthorized' when tried to access user profile (https://graph.microsoft.com/v1.0/me)

Recommended Posts

I have a delphi desktop application and would like to use Graph API to get inforamation from Office 365. I registered the app in Entra. I got the access token, and when I tried to use the token to get user profile (https://graph.microsoft.com/v1.0/me), I got the error message 'HTTP/1.1 401 Unauthorized'.

 

When I registered the app in Entra, the permission 'User.Read' is enabled under Graph. And also when the token is generated, the scope 'User.Read' is also set.

 

When I run test in Postman with the token I get, I have the response with 200 OK.

 

Here is the code to call Graph api:

  RESTClient.BaseURL := FConnection.RESTEndPoint; // https://graph.microsoft.com
  RESTListResGroupRequest.Resource := 'v1.0/me';
  RESTListResGroupRequest.Params.Clear;
  RESTListResGroupRequest.Params.AddItem('Authorization', 'Bearer ' + FConnection.AuthToken, TRESTRequestParameterKind.pkHTTPHEADER, [poDoNotEncode]);
  RESTListResGroupRequest.Params.AddItem('Content-Type', 'application/json', TRESTRequestParameterKind.pkHTTPHEADER);

  RESTListResGroupRequest.ExecuteAsync;

image.thumb.png.ca9967477e45d9b5877f2ff1f8e17556.png

 

image.thumb.png.09b850bb9ad8bdad9e25d1b4d0eea021.png

 

What could be the problem?

Share this post


Link to post

Permissions, always, they are complex to set-up in Azure and use in code.  In particular, consumer and business accounts are different, the latter need a Microsoft User Authority to be passed and enabled, And the scopes specified when you got the bearer token must match those set-up for the account.

 

Angus

 

Share this post


Link to post

Thanks for the reply.

I got the authorization code first, then used the code to get the token with the parameters 'tenant', 'client_id', 'grant_type', 'code', 'scope', 'redirect_uri' and 'response_type'.

Here is the code for the scope:

 

RESTTokenRequest.Params.AddItem('scope', 'openid profile offline_access Mail.Read Mail.Send User.Read', TRestRequestParameterKind.pkREQUESTBODY); 
 

Share this post


Link to post

i have this,

maybe you can the with the scope i use,

i only use Application type, not Delegated:
 


    LRequest.AddAuthParameter('scope', 'https://graph.microsoft.com/.default', TRESTRequestParameterKind.pkREQUESTBODY);

 

i read that it works with postman, so it would not be the scope then.  

i'm trying to remember because i've had problems in the beginning also with authorization with the msgraphapi  (i needed the poDoNotEncode)

 

The RestRequest method = rmGet i assume?

 

 

 

Edited by mvanrijnen

Share this post


Link to post

Thanks for the reply.

I checked during debug, if the method is not given, it will be rmGet.

I also tried the scope 'https://graph.microsoft.com/.default', same result.

Share this post


Link to post

maybe another tip, try somethings like https://mitmproxy.org/ to spy on the request you send.

if  the request works with Postman but not with your code, something has to be different.

 

 

[edit]

Can something go wrong because you use the ExecuteAsync, can't see that with the code you proviced ? (try it with Execute and see if if works then?)

 

Edited by mvanrijnen

Share this post


Link to post

I tried with 'Execute', and it didn't work (still 401 Unauthorized).

I also tried to use Mitmweb to capture the actual request. However, it only captured the request for the first step (log in and get authorization code). The requests for second step (use the code to get access token), and the third step (use access token to get user profile) were not captured. Although I did get the access token from the second step.

I set the proxy server address http://localhost, and port 8080. Did I miss something?

Share this post


Link to post

https://www.charlesproxy.com/ is invaluable for debugging authentication sequences, well, technically any kind of web request.

I've used it to find the difference between a working OAuth2 authentication .NET app, and a non-working Delphi app and solve the Delphi problem.

Share this post


Link to post
5 hours ago, Officeapi said:

I tried with 'Execute', and it didn't work (still 401 Unauthorized).

I also tried to use Mitmweb to capture the actual request. However, it only captured the request for the first step (log in and get authorization code). The requests for second step (use the code to get access token), and the third step (use access token to get user profile) were not captured. Although I did get the access token from the second step.

I set the proxy server address http://localhost, and port 8080. Did I miss something?

They are all using the same TRestClient ?

( i always set these things manually)

 


  restClient.ProxyServer := Self.ProxyHost;
  restClient.ProxyPort := Self.ProxyPort;

 

Could also be the case that you do not use the 127.0.0.1 address, but your non-localhost address (eg 192.168.1.44). 

 

Edited by mvanrijnen

Share this post


Link to post

Universal solution is to compare working and non-working dumps line by line. If you can't use proxy, try with Wireshark

  • Like 1

Share this post


Link to post

Did you check https://learn.microsoft.com/en-us/graph/api/user-get?view=graph-rest-1.0&tabs=http#permissions?

 

A potential reasons for the 401 is the missing scope User.ReadAll.

 

You wrote that you use Application type. For the Application permission type, the table says that the "Least privileged permission" is User.Read.All, however, yor code uses User.Read:

RESTTokenRequest.Params.AddItem('scope', 'openid profile offline_access Mail.Read Mail.Send User.Read', TRestRequestParameterKind.pkREQUESTBODY);  

Also, for testing user profile access, there is no need to include the scopes profile, offline_access, Mail.Read and Mail.Send.

 

Try this:

RESTTokenRequest.Params.AddItem('scope', 'openid User.ReadAll', TRestRequestParameterKind.pkREQUESTBODY);  

 

The API documentation also explains that with Application permission type, only the admin can consent. Calling the API endpoint https://graph.microsoft.com/v1.0/me may return the admin user profile only.

 

If you need the user profile of other users, use the HTTP request described in https://learn.microsoft.com/en-us/graph/api/user-get?view=graph-rest-1.0&tabs=http#http-request

/users/{id | userPrincipalName}

 

Hope this helps

Edited by mjustin

Share this post


Link to post

I did check Microsoft Ignite on how to get user profile. What I need is a sign-in user getting his own profile, not the application type. If you check my first post, all permissions are delegated. Maybe my code used it wrong? 

It is a desktop application. I did get the access token. I copied the token to Postman, it worked with GET https://graph.microsoft.com/v1.0/me

Here is how I get access token (for the scope, I just list all permissions in Entra setting, I believe the one actually needed is User.Read):

 

  RestClient.BaseURL := FConnection.TokenEndPoint;  //'https://login.microsoftonline.com/' + FConnection.TenantId + '/oauth2/v2.0/token'

  RESTTokenRequest.Method := TRESTRequestMethod.rmPOST;
  RESTTokenRequest.Params.Clear;
  RESTTokenRequest.Params.AddItem('tenant',  FConnection.TenantId,  TRestRequestParameterKind.pkQUERY);
  RESTTokenRequest.Params.AddItem('client_id', FConnection.ClientId, TRestRequestParameterKind.pkQUERY);

  RESTTokenRequest.Params.AddItem('grant_type', 'authorization_code', TRestRequestParameterKind.pkREQUESTBODY);
  RESTTokenRequest.Params.AddItem('client_id', FConnection.ClientId, TRestRequestParameterKind.pkREQUESTBODY);
  RESTTokenRequest.Params.AddItem('code', FConnection.AuthCode, TRestRequestParameterKind.pkREQUESTBODY);
  RESTTokenRequest.Params.AddItem('scope', 'IMAP.AccessAsUser.All Mail.Read Mail.ReadWrite Mail.Send offline_access openid POP.AccessAsUser.All profile User.Read Mail.Read', TRestRequestParameterKind.pkREQUESTBODY); 
  RESTTokenRequest.Params.AddItem('redirect_uri', FConnection.RedirectURL, TRestRequestParameterKind.pkREQUESTBODY);  //'https://login.microsoftonline.com/common/oauth2/nativeclient'
  RESTTokenRequest.Params.AddItem('response_type', 'code', TRestRequestParameterKind.pkREQUESTBODY);

 

RESTTokenRequest.ExecuteAsync;
 

Share this post


Link to post

Here is the code to call Graph api:

 

  RESTClient.BaseURL := FConnection.RESTEndPoint; // https://graph.microsoft.com
  RESTProfileRequest.Method := TRESTRequestMethod.rmGet;
  RESTProfileRequest.Resource := 'v1.0/me';
  RESTProfileRequest.Params.Clear;
  RESTProfileRequest.Params.AddItem('Authorization', 'Bearer ' + FConnection.AuthToken, TRESTRequestParameterKind.pkHTTPHEADER, [poDoNotEncode]);
  RESTProfileRequest.Params.AddItem('Content-Type', 'application/json', TRESTRequestParameterKind.pkHTTPHEADER);

  RESTProfileRequest.ExecuteAsync;


I am stuck here for several days. Any suggestion will be appreciated.

Share this post


Link to post

Sorry for my mistake, I mistook a post of a different user as being from you.

 

If the postman call succeeds, I'd check every single HTTP element and modify the Delphi request to be the same. Postman shows all headers and payloads. 

 

One header, which is frequently said to be causing errors, is the User-Agent. So I'd check and use the same header that Postman uses.

Edited by mjustin

Share this post


Link to post

I have tried to mimic what Postman sent. But still got the same error. Here is my code

 

  RESTClient.BaseURL := FConnection.RESTEndPoint; // https://graph.microsoft.com
  RESTProfileRequest.Method := TRESTRequestMethod.rmGet;
  RESTProfileRequest.Resource := 'v1.0/me';
  RESTProfileRequest.Params.Clear;
  RESTProfileRequest.Params.AddItem('Authorization', 'Bearer ' + FConnection.AuthToken, TRESTRequestParameterKind.pkHTTPHEADER, [poDoNotEncode]);
  RESTProfileRequest.Params.AddItem('Content-Type', 'application/json', TRESTRequestParameterKind.pkHTTPHEADER);


  RESTProfileRequest.Params.AddItem('User-Agent', 'PostmanRuntime/7.34.0', TRESTRequestParameterKind.pkHTTPHEADER);
  RESTProfileRequest.Params.AddItem('Accept', '*/*', TRESTRequestParameterKind.pkHTTPHEADER);
//  RESTProfileRequest.Params.AddItem('Postman-Token', 'e08693a1-7ca4-463f-b83f-bd3c7878c798', TRESTRequestParameterKind.pkHTTPHEADER);
  RESTProfileRequest.Params.AddItem('Host', 'graph.microsoft.com', TRESTRequestParameterKind.pkHTTPHEADER);
  RESTProfileRequest.Params.AddItem('Accept-Encoding', 'gzip, deflate, br', TRESTRequestParameterKind.pkHTTPHEADER);
  RESTProfileRequest.Params.AddItem('Connection', 'keep-alive', TRESTRequestParameterKind.pkHTTPHEADER);

 

  RESTProfileRequest.ExecuteAsync;

Share this post


Link to post

I also tried to use Charles Proxy to check the request/response for Graph API (GET https://graph.microsoft.com/v1.0/me), I got the follow error in Charles Proxy window:

{
    "error": {
        "code": "InvalidAuthenticationToken",
        "message": "CompactToken parsing failed with error code: 80049217",
        "innerError": {
            "date": "2023-11-06T19:13:00",
            "request-id": "65e9c9d2-68ca-4081-bcfa-63a7cc1a1fa4",
            "client-request-id": "65e9c9d2-68ca-4081-bcfa-63a7cc1a1fa4"
        }
    }
}

 

I also copied the access token, and used it in Postman, it still worked. 

I am using Delphi 11. What could be the problem?

Share this post


Link to post

 

 

type change this:

  RESTProfileRequest.Method := TRESTRequestMethod.rmGet;
  RESTProfileRequest.Resource := 'v1.0/me';
  RESTProfileRequest.Params.Clear;

 

to

  RESTProfileRequest.Method := TRESTRequestMethod.rmGet;
  RESTProfileRequest.Params.Clear;
  RESTProfileRequest.Resource := 'v1.0/me';

 

i think somethings get cleared with the params clear. 

 

 

second also instead of .Params. use .AddParameter 

 

 

 

with the Access token you mean the value in:  FConnection.AuthToken

 

Edited by mvanrijnen

Share this post


Link to post

I switched the 'Clear' and 'Resource' lines, and used 'AddParameter' as well. I saved the access token in 'FConnection.AuthToken'. Still the same result (401 unauthorized).

Share this post


Link to post

very strange, try this 🙂

 


   FRestClient.AddAuthParameter('Authorization','Bearer ' + AccessToken, pkHTTPHEADER, [TRESTRequestParameterOption.poDoNotEncode]);

so, add the param to the client instead of the request, and use AddAuthParameter for this. 

(maybe try with settings this in the request also).

 

tried all kind of variants, even pasted your code line, here it always works., you tried it with Execute instead of ExecuteAsync 

 

i think you should try a bit harder to get a proxy running for debugging this kind of stuf, works the best to find out differences.

 

 

the different variants i tried, the last one is copied from your code posted here:


//    req.Resource := cExtensionResource;
//  FRestClient.AddAuthParameter('Authorization','Bearer ' + AccessToken, pkHTTPHEADER, [TRESTRequestParameterOption.poDoNotEncode]);
//  req.AddParameter('Authorization','Bearer ' + AccessToken, pkHTTPHEADER, [TRESTRequestParameterOption.poDoNotEncode]);
//    req.Params.AddItem('Authorization','Bearer ' + AccessToken, pkHTTPHEADER, [TRESTRequestParameterOption.poDoNotEncode]);
    req.Params.AddItem('Authorization', 'Bearer ' + AccessToken, TRESTRequestParameterKind.pkHTTPHEADER, [poDoNotEncode]);
    req.Resource := cExtensionResource;
    req.Execute();

 

Edited by mvanrijnen

Share this post


Link to post

Thanks for the update.

I tried with RestClient.AddAuthParameter, now the error became 'HTTP1.1 400 Bad Request'.

Share this post


Link to post
19 minutes ago, Officeapi said:

Thanks for the update.

I tried with RestClient.AddAuthParameter, now the error became 'HTTP1.1 400 Bad Request'.

 

Thats very strange, 

You have assigned the correct TRestclient to the Request?  

 

I use 2 seperate (3 actually), TRestclients:

* 1 for the requests

* 1 for the Authorization

* 1 for uploads (large files)

So i can keep them seperate because they differ a little bit somesitimes (BaseURL especially)

 

Preperation:

function THSMSGraphAPI.NewRestClient(const ABaseURI: string): TRESTClient;
begin
  Result := TRESTClient.Create(ABaseURI);
  Result.SecureProtocols := [THTTPSecureProtocol.TLS12, THTTPSecureProtocol.TLS13];
  if not UserAgent.IsEmpty then
     Result.UserAgent := UserAgent;
  Result.ProxyServer := ProxyServer;
  Result.ProxyPort := ProxyPort;
end;
 

procedure THSMSGraphAPI.CheckClients;
begin
  if not Assigned(FRestClient) then
     FRestClient := NewRestClient(CNST_MSGRAPH_BASEURL);
  if not Assigned(FAuthClient) then
     FAuthClient := NewRestClient(CNST_MICROSOFT_AccessTokenEndpoint.Replace(CNST_TENANTNAME, TenantName, [rfReplaceAll, rfIgnoreCase]));
  if not Assigned(FUplClient) then
     FUplClient := NewRestClient('');
end;

procedure THSMSGraphAPI.CheckCreds;
begin
  CheckClients();
  if not DoCheckAuthorisation() then
     raise Exception.Create('Authorization fail.');
  FRestClient.AddAuthParameter('Authorization','Bearer ' + AccessToken, pkHTTPHEADER, [TRESTRequestParameterOption.poDoNotEncode]);
end;
[code]

 

warning, i only use client_credentials  as grant_type

 

a simple routine

[code]

function THSMSGraphAPI.FindCalendar(const ACalendarName: string; var ACalendarID: string; const ASkipCreds :Boolean): Boolean;
var
  cal : TMSGraphCalendarListItem;
  cals : TMSGraphCalendarListItems;
  req : TRESTRequest;
begin
  Result := False;
  ACalendarID := '';
  if not ASkipCreds then
     CheckCreds();
  req := TRESTRequest.Create(nil);
  try
    req.Client := FRestClient;
    req.Method := TRESTRequestMethod.rmGET;
    req.Resource := CNST_RESOURCE_LISTCALENDARS.Replace(CNST_SENDASEMAILADDRESS, AccountEmailAddress, [rfReplaceAll, rfIgnoreCase]);
    req.Execute();
    if not LogInvalidResponse(req.Response, True, 'THSMSGraphAPI.FindCalendar') then
    begin
      TgoBsonSerializer.Deserialize(req.Response.Content, cals);
      for cal in cals.Value do
          if (cal.name=ACalendarName) then
          begin
            ACalendarID := cal.id;
            break;
          end;
    end;
  finally
    Result := not ACalendarID.IsEmpty;
    req.Free;
  end;
end;

(ps this code was just a quick way to get the calendar i needed, have to expand to the search/filter possibilities from MSGRaph API)

 

i always create the clients&requests by hand in my final code.

 

Edited by mvanrijnen

Share this post


Link to post

Thanks for the reply.

After I used seperate TRESTClient for Graph api call, I don't have 401 error anymore. Now the response has StatusCode 200. However, the JSONValue of the response is nil. I only want to get login user profile (https://graph.microsoft.com/v1.0/me), and the delegated permissions are in my first post. The scope for Access Token is:

  RESTTokenRequest.Params.AddItem('scope', 'IMAP.AccessAsUser.All Mail.Read Mail.ReadWrite Mail.Send offline_access openid POP.AccessAsUser.All profile User.Read Mail.Read', TRestRequestParameterKind.pkREQUESTBODY); 

I believe 'User.Read' is the only permission actually needed. What we have are more than enough. When I used the same token in Postman, I can get my profile.  The response is also in my first post.
 

Share this post


Link to post

I used Charles Proxy to check the request/response for the GraphAPI call from my code (/v1.0/me), and it actually got my profile. But in the Delphi code, the he JSONValue of the response is nil. Here is the code to get the response:

   

FieldRawResponse.Text := TJSON.Format(RESTListResGroupResponse.JSONValue);      
 

I also check that RESTListResGroupResponse.content is empty too.

Here is the info I get from Charles Proxy

image.thumb.png.b1ed05d748b6d61d9f828f9535271df1.png

  • Like 1

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×