如何将 Azure OpenIdConnect OWIN 中间件 Cookie Auth 转换为 SPA 应用程序的 JavaScript JWT?

Posted

技术标签:

【中文标题】如何将 Azure OpenIdConnect OWIN 中间件 Cookie Auth 转换为 SPA 应用程序的 JavaScript JWT?【英文标题】:How to convert Azure OpenIdConnect OWIN Middleware Cookie Auth to JavaScript JWT for SPA application? 【发布时间】: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 中间件执行一系列任务,包括

    通过 Azure Graph API 获取 Azure AD 组和角色 从数据库中获取用户配置文件数据 从第 1 步和第 2 步创建声明 发布cookie 中间件自动处理刷新令牌 中间件将令牌缓存在数据库中,并且能够通过 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;
    

【问题讨论】:

你现在解决这个问题了吗? 【参考方案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 应用程序中添加自定义声明,可以在验证令牌后添加它们。请参考原始帖子的代码示例以添加自定义声明。

以上是关于如何将 Azure OpenIdConnect OWIN 中间件 Cookie Auth 转换为 SPA 应用程序的 JavaScript JWT?的主要内容,如果未能解决你的问题,请参考以下文章

Microsoft OpenIdConnect Owin 响应

当用户的 AD 身份验证失败时如何导航到自定义访问被拒绝页面(.net 3.1 核心与 OpenIDConnect Azure AD 身份验证)

在 Dynamics CRM Online 中的 iFrame 中托管的 OpenIDConnect Azure 网站

Azure AD B2C OpenIdConnect ConfigurationManager 错误

Liferay 7.4 OpenID Connect 作为 Azure B2C 的 SP

教程:如何借助Azure AD和PCF的单点登录(SSO)简化云原生身份管理