ASP核心:Azure广告验证+自定义JWT +自定义身份存储 [英] Asp Core: Azure Ad Auth + custom JWT + custom Identity store

查看:89
本文介绍了ASP核心:Azure广告验证+自定义JWT +自定义身份存储的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

使用ASP.NET Core 2.0,我尝试实现以下目标:

  1. 通过Azure AD(注册的应用程序)进行身份验证
  2. 使用自定义JWT作为身份验证方案
    • 使Web应用程序身份验证可在服务器/实例之间工作
    • 能够保存承载以使用桌面客户​​端登录
  3. 具有自定义身份存储,以引入自定义角色,策略等.

所有这些部分都有有效的示例,但是在尝试将它们组合时,我偶然发现了一些问题.

"Web Api + Azure广告验证"示例使用JWT令牌进行身份验证,但没有验证或创建令牌的逻辑.它也没有登录/注销的逻辑,但这似乎是合理的,只是Api.

以下是Web Api示例代码的快速提醒:

AzureAdAuthenticationBuilderExtensions.cs

using System;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.Authentication
{
    public static class AzureAdServiceCollectionExtensions
    {
        public static AuthenticationBuilder AddAzureAdBearer(this AuthenticationBuilder builder)
            => builder.AddAzureAdBearer(_ => { });

        public static AuthenticationBuilder AddAzureAdBearer(this AuthenticationBuilder builder, Action<AzureAdOptions> configureOptions)
        {
            builder.Services.Configure(configureOptions);
            builder.Services.AddSingleton<IConfigureOptions<JwtBearerOptions>, ConfigureAzureOptions>();
            builder.AddJwtBearer();
            return builder;
        }

        private class ConfigureAzureOptions: IConfigureNamedOptions<JwtBearerOptions>
        {
            private readonly AzureAdOptions _azureOptions;

            public ConfigureAzureOptions(IOptions<AzureAdOptions> azureOptions)
            {
                _azureOptions = azureOptions.Value;
            }

            public void Configure(string name, JwtBearerOptions options)
            {
                options.Audience = _azureOptions.ClientId;
                options.Authority = $"{_azureOptions.Instance}{_azureOptions.TenantId}";
            }

            public void Configure(JwtBearerOptions options)
            {
                Configure(Options.DefaultName, options);
            }
        }
    }
}

Startup.cs

的摘录

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(sharedOptions =>
    {
        sharedOptions.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddAzureAdBearer(options => Configuration.Bind("AzureAd", options));

    services.AddMvc();
}

另一方面,Web应用程序+ Azure广告示例使用带有Cookie的OpenId,并且确实具有登录/注销逻辑:

AzureAdAuthenticationBuilderExtensions.cs

using System;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.Authentication
{
    public static class AzureAdAuthenticationBuilderExtensions
    {
        public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder)
            => builder.AddAzureAd(_ => { });

        public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder, Action<AzureAdOptions> configureOptions)
        {
            builder.Services.Configure(configureOptions);
            builder.Services.AddSingleton<IConfigureOptions<OpenIdConnectOptions>, ConfigureAzureOptions>();
            builder.AddOpenIdConnect();
            return builder;
        }

        private class ConfigureAzureOptions : IConfigureNamedOptions<OpenIdConnectOptions>
        {
            private readonly AzureAdOptions _azureOptions;

            public ConfigureAzureOptions(IOptions<AzureAdOptions> azureOptions)
            {
                _azureOptions = azureOptions.Value;
            }

            public void Configure(string name, OpenIdConnectOptions options)
            {
                options.ClientId = _azureOptions.ClientId;
                options.Authority = $"{_azureOptions.Instance}{_azureOptions.TenantId}";
                options.UseTokenLifetime = true;
                options.CallbackPath = _azureOptions.CallbackPath;
                options.RequireHttpsMetadata = false;
            }

            public void Configure(OpenIdConnectOptions options)
            {
                Configure(Options.DefaultName, options);
            }
        }
    }
}

Startup.cs

的摘录

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(sharedOptions =>
    {
        sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddAzureAd(options => Configuration.Bind("AzureAd", options))
    .AddCookie();

    services.AddMvc(options =>
    {
        var policy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .Build();
        options.Filters.Add(new AuthorizeFilter(policy));
    })
    .AddRazorPagesOptions(options =>
    {
        options.Conventions.AllowAnonymousToFolder("/Account");
    });
}

AccountController.cs

public class AccountController : Controller
{
    [HttpGet]
    public IActionResult SignIn()
    {
        var redirectUrl = Url.Page("/Index");
        return Challenge(
            new AuthenticationProperties { RedirectUri = redirectUrl },
            OpenIdConnectDefaults.AuthenticationScheme
        );
    }

    [HttpGet]
    public IActionResult SignOut()
    {
        var callbackUrl = Url.Page("/Account/SignedOut", pageHandler: null, values: null, protocol: Request.Scheme);
        return SignOut(
            new AuthenticationProperties { RedirectUri = callbackUrl },
            CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme
        );
    }
}

我以某种方式合并了这两个变体,但显然不起作用.在登录方法中,我当然用JwtBearerDefaults替换了CookieAuthenticationDefault.

Startup.cs

的摘录

public void ConfigureServices(IServiceCollection services)
{
    services.AddEntityFrameworkSqlServer().AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    services.AddAuthentication(sharedOptions =>
    {
        sharedOptions.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
        sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddAzureAd(options => Configuration.Bind("AzureAd", options))
    .AddJwtBearer(options =>
    {
        options.IncludeErrorDetails = true;

        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = "localhost",
            ValidAudience = "localhost",
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("test"))
        };
    });

    services.AddMvc(options =>
    {
        var policy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .Build();
        options.Filters.Add(new AuthorizeFilter(policy));
    })
    .AddRazorPagesOptions(options =>
    {
        options.Conventions.AllowAnonymousToFolder("/Account");
    });
}

我不完全了解不同的身份验证是如何链接或相互依赖的.我了解,OpenId在内部使用某种JWT,仍然存在以下问题:

  • 为什么Web Api示例仅使用JWT,而其他示例使用带有Cookie的OpenId?
  • 为什么OpenId示例首先不使用JWT?
  • 自定义JWT是否可以与OpenId一起使用?
  • 是否可以引入自定义身份存储,但保留Azure AD进行登录(和登录名)?

如果您能为我提供一些指导,那就太好了,不需要完全可行的示例(即使这很好)

解决方案

如果要在ASP.NET Core Web应用程序中结合Cookie和Bearer授权,可以遵循以下代码片段:

services.AddAuthentication(sharedOptions =>
{
    sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddJwtBearer(jwtOptions=> {
    jwtOptions.IncludeErrorDetails = true;
    jwtOptions.Authority = "{Authority}";
    jwtOptions.Audience = "{Audience}";
    jwtOptions.TokenValidationParameters = new TokenValidationParameters()
    {
        ValidIssuer = "{ValidIssuer}",
        ValidAudience = "{ValidAudience}"
    };
    jwtOptions.Events = new JwtBearerEvents()
    {
        OnAuthenticationFailed = context => {
            //TODO:
            return Task.FromResult(0);
        },
        OnTokenValidated = context => {
            //At this point, the security token has been validated successfully and a ClaimsIdentity has been created
            var claimsIdentity = (ClaimsIdentity)context.Principal.Identity;
            //add your custom claims here
            claimsIdentity.AddClaim(new Claim("test", "helloworld!!!"));

            return Task.FromResult(0);
        }
    };
})
.AddAzureAd(options => Configuration.Bind("AzureAd", options))
.AddCookie();

我注意到您添加了一个全局AuthorizeFilter,这时您需要确保匿名动作需要使用AllowAnonymous属性进行修饰.

services.AddMvc(options =>
{
    var policy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .AddAuthenticationSchemes(CookieAuthenticationDefaults.AuthenticationScheme, JwtBearerDefaults.AuthenticationScheme)
        .Build();
    options.Filters.Add(new AuthorizeFilter(policy));
})

或者您可以使用Authorize属性装饰控制器动作,如下所示:

[Authorize(AuthenticationSchemes = "Cookies,Bearer")]
public IActionResult UserInfo()
{
    return Json(User.Claims.Select(c => new { key = c.Type, value = c.Value }));
}

对于OpenID Connect中间件,您可以修改AzureAdAuthenticationBuilderExtensions.cs文件下的Configure(string name, OpenIdConnectOptions options)方法以添加自定义声明(例如角色等),如下所示:

public void Configure(string name, OpenIdConnectOptions options)
{
    options.ClientId = _azureOptions.ClientId;
    options.Authority = $"{_azureOptions.Instance}{_azureOptions.TenantId}";
    options.UseTokenLifetime = true;
    options.CallbackPath = _azureOptions.CallbackPath;
    options.RequireHttpsMetadata = false;
    //the new code
    options.Events = new OpenIdConnectEvents
    {
        OnTokenValidated = context =>
        {   
            var claimsIdentity = (ClaimsIdentity)context.Principal.Identity;
            //add your custom claims here
            claimsIdentity.AddClaim(new Claim("test", "helloworld!!!"));

            return Task.FromResult(0);
        }
    };
}

总而言之,在配置后,您可以结合使用Cookies和Bearer身份验证,并且在验证令牌之后,您可以检索用户标识符并从数据库中获取其他用户信息,然后将其附加到ClaimsIdentity.

对于桌面客户端,您可以利用 MSAL (对于AD App v2.0)登录并检索access_tokenid_token,然后将其用作承载令牌来访问您的Web应用程序.

With ASP.NET Core 2.0 I try to achieve the following:

  1. Authentication via Azure AD (registered App)
  2. Custom JWT as the authentication scheme to
    • make the the web app auth work across servers/instances
    • be able to save the bearer to login with desktop clients
  3. Have a custom identity store to introduce custom roles, policies, and other.

All these parts have working examples, but while trying to combine them I stumbled over some problems.

The Web Api + Azure Ad Auth example uses JWT Tokens for authentication, but doesn't have logic for validating or creating tokens. Neither does it have logic for login/logout, but this seems reasonable, its just Api.

Here is a quick reminder of the code of the Web Api example:

AzureAdAuthenticationBuilderExtensions.cs

using System;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.Authentication
{
    public static class AzureAdServiceCollectionExtensions
    {
        public static AuthenticationBuilder AddAzureAdBearer(this AuthenticationBuilder builder)
            => builder.AddAzureAdBearer(_ => { });

        public static AuthenticationBuilder AddAzureAdBearer(this AuthenticationBuilder builder, Action<AzureAdOptions> configureOptions)
        {
            builder.Services.Configure(configureOptions);
            builder.Services.AddSingleton<IConfigureOptions<JwtBearerOptions>, ConfigureAzureOptions>();
            builder.AddJwtBearer();
            return builder;
        }

        private class ConfigureAzureOptions: IConfigureNamedOptions<JwtBearerOptions>
        {
            private readonly AzureAdOptions _azureOptions;

            public ConfigureAzureOptions(IOptions<AzureAdOptions> azureOptions)
            {
                _azureOptions = azureOptions.Value;
            }

            public void Configure(string name, JwtBearerOptions options)
            {
                options.Audience = _azureOptions.ClientId;
                options.Authority = $"{_azureOptions.Instance}{_azureOptions.TenantId}";
            }

            public void Configure(JwtBearerOptions options)
            {
                Configure(Options.DefaultName, options);
            }
        }
    }
}

Excerpt of Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(sharedOptions =>
    {
        sharedOptions.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddAzureAdBearer(options => Configuration.Bind("AzureAd", options));

    services.AddMvc();
}

The Web Application + Azure Ad example on the other hand uses OpenId with cookies and does have login/logout logic:

AzureAdAuthenticationBuilderExtensions.cs

using System;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.Authentication
{
    public static class AzureAdAuthenticationBuilderExtensions
    {
        public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder)
            => builder.AddAzureAd(_ => { });

        public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder, Action<AzureAdOptions> configureOptions)
        {
            builder.Services.Configure(configureOptions);
            builder.Services.AddSingleton<IConfigureOptions<OpenIdConnectOptions>, ConfigureAzureOptions>();
            builder.AddOpenIdConnect();
            return builder;
        }

        private class ConfigureAzureOptions : IConfigureNamedOptions<OpenIdConnectOptions>
        {
            private readonly AzureAdOptions _azureOptions;

            public ConfigureAzureOptions(IOptions<AzureAdOptions> azureOptions)
            {
                _azureOptions = azureOptions.Value;
            }

            public void Configure(string name, OpenIdConnectOptions options)
            {
                options.ClientId = _azureOptions.ClientId;
                options.Authority = $"{_azureOptions.Instance}{_azureOptions.TenantId}";
                options.UseTokenLifetime = true;
                options.CallbackPath = _azureOptions.CallbackPath;
                options.RequireHttpsMetadata = false;
            }

            public void Configure(OpenIdConnectOptions options)
            {
                Configure(Options.DefaultName, options);
            }
        }
    }
}

Excerpt of Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(sharedOptions =>
    {
        sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddAzureAd(options => Configuration.Bind("AzureAd", options))
    .AddCookie();

    services.AddMvc(options =>
    {
        var policy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .Build();
        options.Filters.Add(new AuthorizeFilter(policy));
    })
    .AddRazorPagesOptions(options =>
    {
        options.Conventions.AllowAnonymousToFolder("/Account");
    });
}

AccountController.cs

public class AccountController : Controller
{
    [HttpGet]
    public IActionResult SignIn()
    {
        var redirectUrl = Url.Page("/Index");
        return Challenge(
            new AuthenticationProperties { RedirectUri = redirectUrl },
            OpenIdConnectDefaults.AuthenticationScheme
        );
    }

    [HttpGet]
    public IActionResult SignOut()
    {
        var callbackUrl = Url.Page("/Account/SignedOut", pageHandler: null, values: null, protocol: Request.Scheme);
        return SignOut(
            new AuthenticationProperties { RedirectUri = callbackUrl },
            CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme
        );
    }
}

I have merged somehow both variants, but apparently it doesn't work. I replaced of course CookieAuthenticationDefault with JwtBearerDefaults in the login method.

Excerpt of Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddEntityFrameworkSqlServer().AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    services.AddAuthentication(sharedOptions =>
    {
        sharedOptions.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
        sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddAzureAd(options => Configuration.Bind("AzureAd", options))
    .AddJwtBearer(options =>
    {
        options.IncludeErrorDetails = true;

        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = "localhost",
            ValidAudience = "localhost",
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("test"))
        };
    });

    services.AddMvc(options =>
    {
        var policy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .Build();
        options.Filters.Add(new AuthorizeFilter(policy));
    })
    .AddRazorPagesOptions(options =>
    {
        options.Conventions.AllowAnonymousToFolder("/Account");
    });
}

I don't fully understand how the different authentications are chained or depend on each other. I understand, that OpenId uses internally some sort of JWT, still the following questions remain:

  • Why does the Web Api example just uses JWT, but the other uses OpenId with cookies ?
  • Why does the OpenId example doesn't use JWT in the first place ?
  • Does custom JWT work with OpenId ?
  • Is it possible to introduce a custom identity store, but keep the Azure AD for login (and the login name) ?

It would be great if you could offer me some guidance, no fully working example needed (even though this would be great)

解决方案

If you want combine Cookies and Bearer authorization in your ASP.NET Core web application, you could follow the code snippet below:

services.AddAuthentication(sharedOptions =>
{
    sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddJwtBearer(jwtOptions=> {
    jwtOptions.IncludeErrorDetails = true;
    jwtOptions.Authority = "{Authority}";
    jwtOptions.Audience = "{Audience}";
    jwtOptions.TokenValidationParameters = new TokenValidationParameters()
    {
        ValidIssuer = "{ValidIssuer}",
        ValidAudience = "{ValidAudience}"
    };
    jwtOptions.Events = new JwtBearerEvents()
    {
        OnAuthenticationFailed = context => {
            //TODO:
            return Task.FromResult(0);
        },
        OnTokenValidated = context => {
            //At this point, the security token has been validated successfully and a ClaimsIdentity has been created
            var claimsIdentity = (ClaimsIdentity)context.Principal.Identity;
            //add your custom claims here
            claimsIdentity.AddClaim(new Claim("test", "helloworld!!!"));

            return Task.FromResult(0);
        }
    };
})
.AddAzureAd(options => Configuration.Bind("AzureAd", options))
.AddCookie();

I noticed that you added a global AuthorizeFilter, at this time you need to make sure the anonymous action(s) need to decorate with AllowAnonymous attribute.

services.AddMvc(options =>
{
    var policy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .AddAuthenticationSchemes(CookieAuthenticationDefaults.AuthenticationScheme, JwtBearerDefaults.AuthenticationScheme)
        .Build();
    options.Filters.Add(new AuthorizeFilter(policy));
})

Or you could decorate the controller action with Authorize attribute as follows:

[Authorize(AuthenticationSchemes = "Cookies,Bearer")]
public IActionResult UserInfo()
{
    return Json(User.Claims.Select(c => new { key = c.Type, value = c.Value }));
}

For the OpenID Connect middleware, you could modify the Configure(string name, OpenIdConnectOptions options) method under AzureAdAuthenticationBuilderExtensions.cs file to add your custom claims (e.g. role,etc.) as follows:

public void Configure(string name, OpenIdConnectOptions options)
{
    options.ClientId = _azureOptions.ClientId;
    options.Authority = $"{_azureOptions.Instance}{_azureOptions.TenantId}";
    options.UseTokenLifetime = true;
    options.CallbackPath = _azureOptions.CallbackPath;
    options.RequireHttpsMetadata = false;
    //the new code
    options.Events = new OpenIdConnectEvents
    {
        OnTokenValidated = context =>
        {   
            var claimsIdentity = (ClaimsIdentity)context.Principal.Identity;
            //add your custom claims here
            claimsIdentity.AddClaim(new Claim("test", "helloworld!!!"));

            return Task.FromResult(0);
        }
    };
}

In summary, upon the configuration, you could combine Cookies and Bearer authentication, and after the token has been validated, you could retrieve the user identifier and get additional user info from your database, then attach them to ClaimsIdentity.

For desktop clients, you could leverage ADAL (for AD app v1.0) or MSAL (for AD app v2.0) to log in and retrieve the access_token or id_token, then use it as the bearer token to access your web application.

这篇关于ASP核心:Azure广告验证+自定义JWT +自定义身份存储的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆