2
votes

My ASP.NET MVC Core application uses OWIN Middleware along with the following modules to perform OpenIdConnect authentication against Azure AD:

using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using Microsoft.Azure.ActiveDirectory.GraphClient;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Azure.ActiveDirectory.GraphClient.Extensions;

The OWIN Middleware performs a bunch of tasks including

  1. Fetching Azure AD Groups and Roles via Azure Graph API
  2. Fetching User Profile Data from Database
  3. Creating Claims from steps 1 & 2
  4. Issuing cookie
  5. The Middleware automatically handles Refresh tokens
  6. The Middleware caches the token in the database and able to retrieve via a mechanism AcquireTokenSilentAsync for Graph client.

The MVC application serves a single Razor view and from that point onward, I am using Aurelia JavasScript framework (could easily be Angular, Knockout, React, not important) which only performs API requests to my Api Controller via AJAX.

So my question is how to convert all these authentication and authorization steps handled on the server to JWT based authentication on the client against Azure AD?

Admittedly, my question is fairly naive as there is substantial work being performed by OWIN Middleware components in the code below. So I am looking for a starting point, helper libraries and feasibility. I don't feel confident removing all the middleware code and server side authentication until I am confident this flow can be replicated using AJAX and JWT authentication.

I have done some research and the answer may involve the following

  • adal.js
  • JWT middleware in ASP.NET Core
  • HTML Web Storage
  • Azure AD Graph REST API (instead of C# Graph Client)

Here is the current OWIN Middleware code performing OpenIdConnect authentication against Azure AD on the server:

        app.UseCookieAuthentication();

        app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
        {
            ClientId = Configuration["Authentication:AzureAd:ClientId"],
            ClientSecret = Configuration["Authentication:AzureAd:ClientSecret"],
            Authority = Configuration["Authentication:AzureAd:AADInstance"] + Configuration["Authentication:AzureAd:TenantId"],
            CallbackPath = Configuration["Authentication:AzureAd:CallbackPath"],
            ResponseType = OpenIdConnectResponseType.CodeIdToken,

            Events = new OpenIdConnectEvents()
            {
                OnAuthorizationCodeReceived = async (context) =>
                {
                    var code = context.TokenEndpointRequest.Code;
                    var identity = context.Ticket.Principal.Identity as ClaimsIdentity;
                    userObjectID = identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
                    signedInUserID = identity.FindFirst(ClaimTypes.NameIdentifier).Value;

                    ClientCredential credential =
                    new ClientCredential(
                        Configuration["Authentication:AzureAd:ClientId"],
                        Configuration["Authentication:AzureAd:ClientSecret"]);

                    var authority = Configuration["Authentication:AzureAd:AADInstance"]
                    + Configuration["Authentication:AzureAd:TenantId"];

                    AuthenticationContext authContext =
                    new AuthenticationContext(authority, new ADALTokenCacheService(signedInUserID, Configuration));



                    await authContext.AcquireTokenByAuthorizationCodeAsync(
                        context.TokenEndpointRequest.Code,
                        new Uri(context.TokenEndpointRequest.RedirectUri, UriKind.RelativeOrAbsolute),
                        credential,
                         Configuration["Authentication:AzureAd:GraphResource"]);

                    context.HandleCodeRedemption();

                    ActiveDirectoryClient activeDirectoryClient = GetActiveDirectoryClient();

                    // Get currently logged in User from Graph
                    IPagedCollection<IUser> users = await activeDirectoryClient.Users.Where(u => u.ObjectId.Equals(userObjectID)).ExecuteAsync();
                    IUser user = users.CurrentPage.ToList().First();

                    // Get User's AD Groups
                    IEnumerable<string> userGroupIds = await user.GetMemberGroupsAsync(false);
                    List<string> userGroupIdList = userGroupIds.ToList();


                    // Transform User's AD Groups into Claims
                    foreach (var groupObjectId in userGroupIdList)
                    {
                        var group = await activeDirectoryClient.Groups.GetByObjectId(groupObjectId).ExecuteAsync();

                        Claim newClaim = new Claim(
                           CustomClaimValueTypes.ADGroup,
                            group.DisplayName,
                            ClaimValueTypes.String,
                            "AAD GRAPH");

                        ((ClaimsIdentity)(context.Ticket.Principal.Identity)).AddClaim(newClaim);
                    }

                    // Get User's Application permissions from Database
                    upn = identity.FindFirst(ClaimTypes.Upn).Value;

                    DbContext db =
                   new DbContext(Configuration["ConnectionStrings:DefaultConnection"]);

                    if (db.PortalUsers.FirstOrDefault(b => (b.UPN == upn)) == null)
                    {
                        throw new System.IdentityModel.Tokens.SecurityTokenValidationException("You are not registered to use this application.");
                    }

                    var applications = from permissions in db.PortalPermissions
                                       where permissions.PortalUser.UPN == upn
                                       //orderby permissions.Application.SortOrder ascending
                                       select permissions.PortalApplication;

                    // Transform User's Application permissions into Claims
                    foreach (var application in applications)
                    {
                        Claim newClaim = new Claim(
                           CustomClaimValueTypes.Application,
                            application.Name,
                            ClaimValueTypes.String,
                            "DATABASE");

                        ((ClaimsIdentity)(context.Ticket.Principal.Identity)).AddClaim(newClaim);
                    }
                },
                OnRemoteFailure = (context) =>
                {
                    if (context.Failure.Message == "You are not registered to use this application.")
                    {
                        context.Response.Redirect("/AuthenticationError");
                    }
                    else
                    {
                        context.Response.Redirect("/Error");
                    }
                    context.HandleResponse();
                    return Task.FromResult(0);
                }
            }

        });

        app.UseFileServer(new FileServerOptions
        {
            EnableDefaultFiles = true,
            EnableDirectoryBrowsing = false
        });

        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Start}/{id?}");
        });
    }


    private ActiveDirectoryClient GetActiveDirectoryClient()
    {
        Uri servicePointUri = new Uri(Configuration["Authentication:AzureAd:GraphResource"]);
        Uri serviceRoot = new Uri(servicePointUri, Configuration["Authentication:AzureAd:TenantId"]);

        ActiveDirectoryClient activeDirectoryClient = new ActiveDirectoryClient(
            serviceRoot, async () => await GetTokenForApplicationAsync());

        return activeDirectoryClient;

    }


    private async Task<string> GetTokenForApplicationAsync()
    {
        ClientCredential clientCredential =
            new ClientCredential(
                Configuration["Authentication:AzureAd:ClientId"],
                Configuration["Authentication:AzureAd:ClientSecret"]);

        AuthenticationContext authenticationContext =
            new AuthenticationContext(
                Configuration["Authentication:AzureAd:AADInstance"] +
                Configuration["Authentication:AzureAd:TenantId"],
                new ADALTokenCacheService(signedInUserID, Configuration));

        AuthenticationResult authenticationResult = await authenticationContext.AcquireTokenSilentAsync(
                 Configuration["Authentication:AzureAd:GraphResource"],
                clientCredential,
                new UserIdentifier(userObjectID, UserIdentifierType.UniqueId));

        return authenticationResult.AccessToken;
    }
1
Have you fixed this issue now?Fei Xue - MSFT

1 Answers

2
votes

The MVC application serves a single Razor view and from that point onward, I am using Aurelia JavasScript framework (could easily be Angular, Knockout, React, not important) which only performs API requests to my Api Controller via AJAX.

Did you mean that the ASP.NET MVC Core application will protect the the API controller by both cookies and bearer token? And the Aurelia JavasScript framework will perform the AJAX request to the API control using the bearer token?

If I understood correctly, you need to register another native app on the Azure portal for authentication for the app which using Aurelia JavaScript framework(as same as the SPA call web API which protected by Azure AD here).

And for the existing ASP.NET MVC Core application to support the token authentication, we need to add the JWT token middler ware.

And if the web API which publish for your SPA application want to call other resource, we also need to check authentciation methoed.

For example, if we call the web API with token(the audince of token should be the app id uri of you ASP.Net MVC core application), and the web API need to exhcange this token for the target resource using the flow described Delegated User Identity with OAuth 2.0 On-Behalf-Of Draft Specification to call the another web API.

Update

app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
         ClientId = ClientId,
         Authority = Authority,
         PostLogoutRedirectUri = Configuration["AzureAd:PostLogoutRedirectUri"],
         ResponseType = OpenIdConnectResponseType.CodeIdToken,
         GetClaimsFromUserInfoEndpoint = false,

         Events = new OpenIdConnectEvents
         {
             OnRemoteFailure = OnAuthenticationFailed,
             OnAuthorizationCodeReceived = OnAuthorizationCodeReceived,
             OnTokenValidated= context => {
                 (context.Ticket.Principal.Identity as ClaimsIdentity).AddClaim(new Claim("AddByMyWebApp", "ClaimValue"));
                    return Task.FromResult(0);
             }
         }                            
});