简体   繁体   中英

Framework.net service, using ConfidentialClientApplication and the Outlook Tasks REST API, got StatusCode: 401, ReasonPhrase: 'Unauthorized'

Hello World !

Hello to everyone ! I'm new to StackOverflow, so I'll do my best to explain my problem better.

The Issue

I'm tring to interact with Outlook Calendar and Tasks REST API from ac# .net framework cli program, that will became a background Windows service.

I have created a dedicated Outlook account, registered the app on the Azure portal and adapted the rest-sender project.

With PublicClientApplicationBuilder.AcquireTokenInteractive I can get a valid, even though effimeral (1 hour), access token and listing my tasks. It works for other API calls too, but the problem is that every time I require a token it shows and interactive web browser window for picking up an account. The app will became a Windows background service in a Production Headless Server, so no interactive window is possible.

I discovered ConfidentialClientApplication.AcquireTokenForClient that needs a client secret. I have created one from the Azure portal. I can get a token, but It seems that it isn't authorized.

The Portal

the app This is a screen of the app in the Azure's page.

auth These are the authentications settings.

auths These are the authorizations settings.

secret This is the secret page.

mail From the mail I can see these permission (granted with the GetInteractiveToken )

token This is the decoded token.


This is an illustrative code:


static async Task<string> GetSecretAccessToken()
{
    var client =
        ConfidentialClientApplicationBuilder
            .Create("<applicationId>")
            .WithClientSecret("<clientSecret>")
            .Build();

    var result = client.AcquireTokenForClient(new []{
        "https://outlook.office.com/.default",
    }).ExecuteAsync();

    var r = result.Result.AccessToken;
    return r;
}

static async Task<string> GetInteractiveAccessToken()
{
    var client =
        PublicClientApplicationBuilder
            .Create("<applicationId>")
            .Build();

    var result = client.AcquireTokenInteractive(new []{
        "https://outlook.office.com/Tasks.ReadWrite"
    }).ExecuteAsync();

    var r = result.Result.AccessToken;
    return r;
}

static async Task<string> GetAccessFromPasswordToken(string[] scopes){
            try
            {
                var b =
                    PublicClientApplicationBuilder.Create(ConfigurationManager.AppSettings.Get("applicationId"))
                        .Build();
                var result = b.AcquireTokenByUsernamePassword(scopes, "CodeGen.Preventizzatore@Outlook.it", GetSecureString()).ExecuteAsync();
                return result.Result.AccessToken;
            }
            catch (MsalException ex)
            {
                Output.WriteLine(Output.Error, "Could not acquire access token: Error code: {0}, Error message: ",
                    ex.ErrorCode, ex.Message);
                return string.Empty;
            }
}

static async Task ListTasks()
{
    string token = await GetAccessToken();

    var request = new HttpRequestMessage(
        new HttpMethod("GET"),
        new UriBuilder("https://outlook.office.com/api/v2.0/users/CodeGen.Preventizzatore@Outlook.it/taskfolders").Uri
    );
    request.Headers.Authorization =
        new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
    request.Headers.UserAgent.Add(
        new System.Net.Http.Headers.ProductInfoHeaderValue("rest-sender", "1.0"));
    request.Headers.Accept.Add(
        new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));

    var result = await new HttpClient().SendAsync(request);
    if (result.StatusCode == HttpStatusCode.Unauthorized)
        return; // when GetSecretAccessToken
    // when GetInteractiveAccessToken
    string response = await result.Content.ReadAsStringAsync();
}

And this is the manifest on Azure:

{
    "id": "a752666c-11a9-425e-a382-c63ebcbc8bb2",
    "acceptMappedClaims": null,
    "accessTokenAcceptedVersion": 2,
    "addIns": [],
    "allowPublicClient": null,
    "appId": "608bd039-6bc9-4a7e-ad86-2b6a3e20584b",
    "appRoles": [],
    "oauth2AllowUrlPathMatching": false,
    "createdDateTime": "2019-06-21T08:18:56Z",
    "groupMembershipClaims": null,
    "identifierUris": [
        "api://608bd039-6bc9-4a7e-ad86-2b6a3e20584b"
    ],
    "informationalUrls": {
        "termsOfService": null,
        "support": null,
        "privacy": null,
        "marketing": null
    },
    "keyCredentials": [],
    "knownClientApplications": [],
    "logoUrl": null,
    "logoutUrl": null,
    "name": "CodeGen.Preventizzatore",
    "oauth2AllowIdTokenImplicitFlow": false,
    "oauth2AllowImplicitFlow": true,
    "oauth2Permissions": [],
    "oauth2RequirePostResponse": false,
    "optionalClaims": null,
    "orgRestrictions": [],
    "parentalControlSettings": {
        "countriesBlockedForMinors": [],
        "legalAgeGroupRule": "Allow"
    },
    "passwordCredentials": [
        {
            "customKeyIdentifier": null,
            "endDate": "2299-12-30T23:00:00Z",
            "keyId": "c4a2fe3a-a09c-41ff-9115-40a2a2ef1a89",
            "startDate": "2019-06-21T11:46:06.306Z",
            "value": null,
            "createdOn": "2019-06-21T11:46:08.2116173Z",
            "hint": "nR?",
            "displayName": "Preventizzatore"
        }
    ],
    "preAuthorizedApplications": [],
    "publisherDomain": null,
    "replyUrlsWithType": [
        {
            "url": "urn:ietf:wg:oauth:2.0:oob",
            "type": "InstalledClient"
        }
    ],
    "requiredResourceAccess": [
        {
            "resourceAppId": "00000002-0000-0ff1-ce00-000000000000",
            "resourceAccess": [
                {
                    "id": "bbd1ca91-75e0-4814-ad94-9c5dbbae3415",
                    "type": "Scope"
                },
                {
                    "id": "6b49b74d-642f-4417-a6b4-820576845707",
                    "type": "Scope"
                },
                {
                    "id": "bf24470f-10c1-436d-8d53-7b997eb473be",
                    "type": "Role"
                },
                {
                    "id": "77e65b5a-ceae-48b3-9490-50a86a038a48",
                    "type": "Role"
                },
                {
                    "id": "dc890d15-9560-4a4c-9b7f-a736ec74ec40",
                    "type": "Role"
                },
                {
                    "id": "798ee544-9d2d-430c-a058-570e29e34338",
                    "type": "Role"
                },
                {
                    "id": "c1b0de0a-1de9-455d-919f-eca451053141",
                    "type": "Role"
                },
                {
                    "id": "2c6a42ca-0d4d-49ad-bc0e-21222c449a65",
                    "type": "Role"
                },
                {
                    "id": "ef54d2bf-783f-4e0f-bca1-3210c0444d99",
                    "type": "Role"
                },
                {
                    "id": "2dfdc6dc-2fa7-4a2c-a922-dbd4f85d17be",
                    "type": "Role"
                }
            ]
        }
    ],
    "samlMetadataUrl": null,
    "signInUrl": null,
    "signInAudience": "AzureADandPersonalMicrosoftAccount",
    "tags": [],
    "tokenEncryptionKeyId": null
}

When using the non-interactive token I get:

{StatusCode: 401, ReasonPhrase: 'Unauthorized', Version: 1.1, Content: System.Net.Http.StreamContent, Headers:
{
  Transfer-Encoding: chunked
  Cache-Control: private
  WWW-Authenticate: Bearer client_id="00000002-0000-0ff1-ce00-000000000000", trusted_issuers="00000001-0000-0000-c000-000000000000@*", token_types="app_asserted_user_v1 service_asserted_app_v1", authorization_uri="https://login.windows.net/common/oauth2/authorize", error="invalid_token"
  WWW-Authenticate: Basic Realm=""
  request-id: 8a2ccebc-e8c3-4ec2-b8cb-64d1005a5bf7
  X-CalculatedBETarget: MR2P264MB0817.FRAP264.PROD.OUTLOOK.COM
  X-BackEndHttpStatus: 401
  X-RUM-Validated: 1
  x-ms-diagnostics: 2000008;reason="The token contains no permissions, or permissions can not be understood.";error_category="invalid_grant"
  X-AspNet-Version: 4.0.30319
  X-BeSku: WCS5
  X-DiagInfo: MR2P264MB0817
  X-BEServer: MR2P264MB0817
  X-Powered-By: ASP.NET
  X-FEServer: MRXP264CA0011
  X-MSEdge-Ref: Ref A: 25DDA6AB2E72497199D611B555DACB60 Ref B: MIL30EDGE0414 Ref C: 2019-06-24T06:10:44Z
  Date: Mon, 24 Jun 2019 06:10:43 GMT
  Content-Type: text/html; charset=utf-8
}}

Both GetSecretAccessToken and GetInteractiveAccessToken provide a token, but only the interactive one works in the ListTasks method. I need the other one for working with Win service.

I read that https://outlook.office.com/.default make the app use the static app permissions, which I set on the Azure portal. But I can't get this method working.

I just don't know what kind of authorization it needs to work in this mode.

It's almost a week that I'm tring to make this working, without success. Maybe I'm missing something trivial... So I seek help on the best place on the planet.

I also found an example in .net core that register a device code, but I'll prefere to stay with base .net framework, because I already have a running project on my customer system.

I hope I have clarified my issue and that is feasible. It would be very nice to have an Outlook integration.

Thanks in advance.

==============

Tl;Dr;

The Microsoft identity platform doesn't support personal accounts, so if you want to access outlook365.com REST API from a background service/daemon you'll need to be under Azure AD and to be approved by an admin (He'll press the "Grant admin consent for [yourself]" Button). Otherwise you won't be able to perform any request, because your non-interactive token won't contain any scope permission.

Thanks Jason Johnston for the big help !

You are using the client credentials flow to get an application token. This token has no user context to it, so you cannot use the /me segment in your request URL. Replace that with /users/{user-id} . {user-id} is either the user's object ID (obtained by doing a GET /users call), or their UPN (typically their email address).

I would also recommend copying the token and parsing it at https://jwt.ms . You want to look in the token and make sure it contains the Tasks.Read permission.

Also make sure that you've obtained admin consent for the application permissions you've configured on your app registration. If you are the administrator, you should see a Grant consent section just under the API permissions section with a button you can click to grant consent.

在此处输入图片说明

Personal accounts

If you're writing this to access a personal Outlook.com account, you cannot use client credentials flow. That particular flow is only available to work or school accounts on Office 365.

Azure OAuth currently does not support non-interactive flows for personal accounts. The closest you could get is to implement token cache serialization , then do an interactive login with the app once (to seed the cache with the user's refresh token). Then on subsequent runs you could call AcquireTokenSilent .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM