4

We have a .net core application which uses Azure AD for authentication (MSAL/ v2.0). We want a linux application to access an API from the first application. The second application has no user context and will interact exactly as curl script would.

From reading the documentation I believe that I should register a second application with azure ad. I can get so far as a JWT token that has the target application as audience but I cannot get access to the api. We've been able to access the (real) api from scripts with an access token captured from a logged in user.

I created a Test environment to find the solution.

Created a .net core project authenticated by azure ad. It is running locally from my workstation not deployed to azure. Azure Authentication is working for interactive users.

Registered a second application and created a secret for it.

The first application is configured with both id and access tokens for implicit grant, it is not set as a public client.

I defined an approle 'access_as_application' in the manifest.

Under Expose an API I Created a scope 'api' and added the other application as an authorized client application.

Under API permissions I added a permisison, chose application permission and checked the approle I created earlier.

I can run a curl script and retrieve a bearer token that when decoded shows an audience matching my application. When I use that token in a curl script it is redirected to sign in.

Manifest:

{
    "id": "33b*******************************",
    "acceptMappedClaims": null,
    "accessTokenAcceptedVersion": 2,
    "addIns": [],
    "allowPublicClient": null,
    "appId": "3d8*******************************",
    "appRoles": [
        {
            "allowedMemberTypes": [
                "Application"
            ],
            "description": "Access webapp as an application.",
            "displayName": "access_as_application",
            "id": "ff5ea9b2*******************************",",
            "isEnabled": true,
            "lang": null,
            "origin": "Application",
            "value": "access_as_application"
        }
    ],
    "oauth2AllowUrlPathMatching": false,
    "createdDateTime": "2019-10-29T16:49:37Z",
    "groupMembershipClaims": null,
    "identifierUris": [
        "api://3d8*******************************"
    ],
    "informationalUrls": {
        "termsOfService": null,
        "support": null,
        "privacy": null,
        "marketing": null
    },
    "keyCredentials": [],
    "knownClientApplications": [],
    "logoUrl": null,
    "logoutUrl": "https://localhost:44321/signout-callback-oidc",
    "name": "WebApp",
    "oauth2AllowIdTokenImplicitFlow": true,
    "oauth2AllowImplicitFlow": true,
    "oauth2Permissions": [
        {
            "adminConsentDescription": "consent for api",
            "adminConsentDisplayName": "consent for api",
            "id": "a4b2*******************************",",
            "isEnabled": true,
            "lang": null,
            "origin": "Application",
            "type": "Admin",
            "userConsentDescription": null,
            "userConsentDisplayName": null,
            "value": "api"
        }
    ],
    "oauth2RequirePostResponse": false,
    "optionalClaims": null,
    "orgRestrictions": [],
    "parentalControlSettings": {
        "countriesBlockedForMinors": [],
        "legalAgeGroupRule": "Allow"
    },
    "passwordCredentials": [
        {
            "customKeyIdentifier": null,
            "endDate": "2299-12-31T05:00:00Z",
            "keyId": "e03c4*******************************",",
            "startDate": "2019-10-31T20:05:42.56Z",
            "value": null,
            "createdOn": "2019-10-31T20:05:42.7555795Z",
            "hint": "00_",
            "displayName": "webappsecret"
        }
    ],
    "preAuthorizedApplications": [
        {
            "appId": "a4b*******************************",
            "permissionIds": [
                "23b*******************************"
            ]
        },
        {
            "appId": "3d8*******************************",
            "permissionIds": [
                "23b*******************************"
            ]
        }
    ],
    "publisherDomain": "brainbuzgmail.onmicrosoft.com",
    "replyUrlsWithType": [
        {
            "url": "https://localhost:44321/signin-oidc",
            "type": "Web"
        },
        {
            "url": "https://localhost:44321/",
            "type": "Web"
        }
    ],
    "requiredResourceAccess": [
        {
            "resourceAppId": "3d8*******************************",
            "resourceAccess": [
                {
                    "id": "23b*******************************",
                    "type": "Scope"
                },
                {
                    "id": "ff5ea*******************************",",
                    "type": "Role"
                }
            ]
        },
        {
            "resourceAppId": "a4b*******************************",
            "resourceAccess": [
                {
                    "id": "a37a*******************************",",
                    "type": "Scope"
                },
                {
                    "id": "ccf78*******************************",",
                    "type": "Role"
                }
            ]
        },
        {
            "resourceAppId": "00000003-0000-0000-c000-000000000000",
            "resourceAccess": [
                {
                    "id": "e1fe*******************************",",
                    "type": "Scope"
                }
            ]
        }
    ],
    "samlMetadataUrl": null,
    "signInUrl": null,
    "signInAudience": "AzureADMyOrg",
    "tags": [],
    "tokenEncryptionKeyId": null
}

Variables $ are set in environment. Token is captured from successful request and set as $TOKEN. -k insecure flag is used in curl due to local app using self signed certificate.

curl -X POST -d "grant_type=client_credentials&client_id=$CLIENTID&client_secret=$SECRET&resource=$SCOPE" https://login.microsoftonline.com/$TENANT/oauth2/token

curl -k 'https://localhost:44321/api/' \
-H 'Accept: application/json' \
-H "Authorization: Bearer $TOKEN" \
-H 'Sec-Fetch-Mode: cors' -H 'Content-Type: application/json' --compressed

Partial response redirecting to login page, it looks like the client is being asked to get an id token, even though it has a valid access token:

< HTTP/2 302
< location: https://login.microsoftonline.com/****/oauth2/v2.0/authorize?client_id=***&redirect_uri=https%3A%2F%2Flocalhost%3A44321%2Fsignin-oidc&response_type=id_token
brainbuz
  • 384
  • 1
  • 3
  • 12
  • I have a similar case and I get 401 unauthorized error. As you have worked with sch case, I was wondering if you can take a look at my question here and help with it? https://stackoverflow.com/questions/62654557/use-curl-command-to-access-elasticcloud-kibana-api-secured-by-azure-ad – Matrix Jul 01 '20 at 12:41

1 Answers1

1

If you just want your Linux app to call APIs of your .net core application which protected by Azure AD,this is a service to service call flow and there is no need to redirect to /authorize endpoint as generally this endpoint is one of the steps of users login.

Based on your description, you have obtained access token successfully , and you can use this token as a Authorization Bearer header in your API request to call your .net core application directly.

TodoListServicepeoject of this demo is a sample API side demo which will be helpful for you. In your case you should do some modify to make service to service call work .

1.Replace content of Controllers/TodoListController.cs in TodoListService with code below :

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Identity.Web.Resource;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Claims;
using TodoListService.Models;

namespace TodoListService.Controllers
{
    [Authorize]
    [Route("api/[controller]")]
    public class TodoListController : Controller
    {
        static readonly ConcurrentBag<TodoItem> TodoStore = new ConcurrentBag<TodoItem>();

        /// <summary>
        /// The Web API will only accept tokens 1) for users, and 
        /// 2) having the access_as_user scope for this API
        /// </summary>
        static readonly string[] scopeRequiredByApi = new string[] { "access_as_application" };

        // GET: api/values
        [HttpGet]
        public IEnumerable<TodoItem> Get()
        {
         //   HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);
            string owner = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
            return TodoStore.Where(t => t.Owner == owner).ToList();
        }

        // POST api/values
        [HttpPost]
        public void Post([FromBody]TodoItem todo)
        {
            //check roles claim in token start
            Claim scopeClaim = HttpContext.User?.FindFirst("http://schemas.microsoft.com/ws/2008/06/identity/claims/role");
            if (scopeClaim == null || !scopeClaim.Value.Split(' ').Intersect(scopeRequiredByApi).Any())
            {
                HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
                string message = $"The 'roles' claim does not contain scopes '{string.Join(",", scopeRequiredByApi)}' or was not found";
                throw new HttpRequestException(message);
            }
            //check roles claim end
            string owner = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
            TodoStore.Add(new TodoItem { Owner = owner, Title = "test!!" });
        }
    }
}

enter image description here

  1. In WebApiServiceCollectionExtensions.cs of Microsoft.Identity.Web project,line 80 , replace with code below to make sure your role claim will be checked :

    && !context.Principal.Claims.Any(y => y.Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"))

enter image description here

Have a test here , post a request : enter image description here

get records : enter image description here

Hope it helps .

Stanley Gong
  • 11,522
  • 1
  • 8
  • 16
  • The request with the bearer token is redirected to login/authorize. – brainbuz Nov 13 '19 at 06:27
  • The example you referenced is for a v 1.0 endpoint using msgraph I'm using v 2.0 with an api/scope coming from my web application. – brainbuz Nov 13 '19 at 19:39
  • Hi @brainbuz , if you are using Azure AD V2.0 endpoint , you should obtain tokens from V2.0 endpoint too . However in your curl reuqest , you are using v1,0 endpoint . The V2.0 endpoint should be : 'https://login.microsoftonline.com//oauth2/v2.0/token' . If you use v2.0 endpoint , you should use scope to replace resource param in your request , details see here : https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow#get-a-token – Stanley Gong Nov 15 '19 at 03:42