【问题标题】:How to convert Azure OpenIdConnect OWIN Middleware Cookie Auth to JavaScript JWT for SPA application?如何将 Azure OpenIdConnect OWIN 中间件 Cookie Auth 转换为 SPA 应用程序的 JavaScript JWT?
【发布时间】:2017-04-23 04:15:17
【问题描述】:

我的 ASP.NET MVC Core 应用程序使用 OWIN 中间件以及以下模块对 Azure AD 执行 OpenIdConnect 身份验证:

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;

OWIN 中间件执行一系列任务,包括

  1. 通过 Azure Graph API 获取 Azure AD 组和角色
  2. 从数据库中获取用户配置文件数据
  3. 从第 1 步和第 2 步创建声明
  4. 发布cookie
  5. 中间件自动处理刷新令牌
  6. 中间件将令牌缓存在数据库中,并且能够通过 Graph 客户端的机制AcquireTokenSilentAsync 进行检索。

MVC 应用程序提供单个 Razor 视图,从那时起,我使用 Aurelia JavaScript 框架(很可能是 Angular、Knockout、React,并不重要),它只通过 AJAX 向我的 Api 控制器执行 API 请求。

所以我的问题是如何将服务器上处理的所有这些身份验证和授权步骤转换为客户端上针对 Azure AD 的基于 JWT 的身份验证?

诚然,我的问题相当幼稚,因为在下面的代码中 OWIN 中间件组件正在执行大量工作。所以我正在寻找一个起点、辅助库和可行性。在我确信可以使用 AJAX 和 JWT 身份验证复制此流程之前,我没有信心删除所有中间件代码和服务器端身份验证。

我做了一些研究,答案可能涉及以下内容

  • adal.js
  • ASP.NET Core 中的 JWT 中间件
  • HTML 网络存储
  • Azure AD Graph REST API(而不是 C# Graph 客户端)

以下是当前 OWIN 中间件代码,该代码在服务器上针对 Azure AD 执行 OpenIdConnect 身份验证:

        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;
    }

【问题讨论】:

  • 你现在解决这个问题了吗?

标签: authentication asp.net-core jwt openid-connect azure-active-directory


【解决方案1】:

MVC 应用程序提供单个 Razor 视图,从那时起,我使用 Aurelia JavaScript 框架(很可能是 Angular、Knockout、React,并不重要),它只通过 AJAX 向我的 Api 控制器执行 API 请求。

您的意思是说 ASP.NET MVC Core 应用程序将通过 cookie 和不记名令牌来保护 API 控制器吗?而 Aurelia JavaScript 框架会使用承载令牌对 API 控件执行 AJAX 请求吗?

如果我理解正确,您需要在 Azure 门户上注册另一个本机应用程序,以对使用 Aurelia JavaScript 框架的应用程序进行身份验证(与受 Azure AD here 保护的 SPA 调用 Web API 相同)。

而为了让现有的 ASP.NET MVC Core 应用程序支持令牌认证,我们需要添加 JWT 令牌中间件。

如果为您的 SPA 应用程序发布的 Web API 想要调用其他资源,我们还需要检查身份验证方法。

例如,如果我们使用令牌调用 Web API(令牌的受众应该是您的 ASP.Net MVC 核心应用程序的 app id uri),并且 Web API 需要使用此令牌交换目标资源flow 描述了Delegated User Identity with OAuth 2.0 On-Behalf-Of Draft Specification 来调用另一个 Web API。

更新

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);
             }
         }                            
});

【讨论】:

  • 这是一个很好的起点。如何使用 JWT 中间件以及如何通过 JavaScript 对 AD 进行身份验证?另外,从 SPA 进行身份验证时,我应该在身份验证管道的哪个位置添加自定义声明?
  • 从SPA调用受Azure AD保护的Web API,可以参考代码示例here。如果要在 Web 应用程序中添加自定义声明,可以在验证令牌后添加它们。请参考原始帖子的代码示例以添加自定义声明。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-06-11
  • 2021-10-11
  • 2020-10-04
  • 2015-08-19
  • 2020-04-10
  • 2018-03-23
  • 2015-09-25
相关资源
最近更新 更多