【问题标题】:AngularJs, WebAPI, JWT, with (integrated) Windows authenticationAngularJs、WebAPI、JWT,带有(集成的)Windows 身份验证
【发布时间】:2017-05-08 14:03:46
【问题描述】:

我之前问过question,得到的答案是正确的,但我越深入这个兔子洞,我就越意识到;我不认为我问的是正确的问题。

让我用最简单的术语解释一下……我有一个 AngularJS 单页应用程序(客户端),它指向一个 asp.net webapi (OWIN) 站点(资源服务器?),还有一个单独的asp.net“授权/身份验证”服务器。

身份验证服务器将为多个应用程序提供身份验证和授权。我需要能够在资源服务器中使用 Authorize 属性,以及从 angular 获取令牌。我还需要对所有内容使用 Windows 身份验证(集成),无需用户名或密码。索赔信息存储在数据库中,需要添加到令牌中。

我已经在 asp.net 核心中使用带有 JwtBearerToken 和“密码流”的 openiddict 完成了 SSO 风格的声明授权实现?并想尝试做类似的事情(令牌等)。从我之前的实现中,我对它的工作原理有了基本的了解,但是我完全迷失了试图弄清楚如何让 JWT 与 Windows Auth 一起工作。我上一个问题的答案提供了一些很好的建议,但我很难看到这在这种情况下如何应用。

目前我一直在尝试使用 WindowsAuthentication 扩展来让 IdentityServer3 执行此操作,主要是从示例中提取的。但我真的很难将它与客户联系在一起并真正得到一些工作。当前的客户端和服务器代码如下,请注意,我真的不知道这是否接近正确的解决方案。

客户:

app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
        {
            AuthenticationMode = AuthenticationMode.Passive,
            AuthenticationType = "windows",
            Authority = "http://localhost:21989",
            ClientId = "mvc.owin.implicit",
            ClientSecret = "api-secret",
            RequiredScopes = new[] { "api" }
        });

认证服务器:

app.Map("/windows", ConfigureWindowsTokenProvider);
app.Use(async (context, next) =>
{
     if (context.Request.Uri.AbsolutePath.EndsWith("/token", StringComparison.OrdinalIgnoreCase))
            {
                if (context.Authentication.User == null ||
                    !context.Authentication.User.Identity.IsAuthenticated)
                {
                    context.Response.StatusCode = 401;
                    return;
                }
            }

            await next();
        });
        var factory = new IdentityServerServiceFactory()
           .UseInMemoryClients(Clients.Get())
           .UseInMemoryScopes(Scopes.Get());

        var options = new IdentityServerOptions
        {
            SigningCertificate = Certificate.Load(),
            Factory = factory,
            AuthenticationOptions = new AuthenticationOptions
            {
                EnableLocalLogin = false,
                IdentityProviders = ConfigureIdentityProviders
            },
            RequireSsl = false
        };

        app.UseIdentityServer(options);


private static void ConfigureWindowsTokenProvider(IAppBuilder app)
    {
        app.UseWindowsAuthenticationService(new WindowsAuthenticationOptions
        {
            IdpReplyUrl = "http://localhost:21989",
            SigningCertificate = Certificate.Load(),
            EnableOAuth2Endpoint = false
        });
    }

    private void ConfigureIdentityProviders(IAppBuilder app, string signInAsType)
    {
        var wsFederation = new WsFederationAuthenticationOptions
        {
            AuthenticationType = "windows",
            Caption = "Windows",
            SignInAsAuthenticationType = signInAsType,
            MetadataAddress = "http://localhost:21989",
            Wtrealm = "urn:idsrv3"
        };
        app.UseWsFederationAuthentication(wsFederation);
    }

编辑:我看到“/.well-known/openid-configuration”以及“/.well-known/jwks”的身份验证端点请求,并且我在正在调用的控制器操作上有 Authorize 属性,但我没有看到身份验证方面发生任何其他事情。我还在 usewindowsauthservice WindowsAuthenticationOptions 中添加了一个 ICustomClaimsProvider 实现,但它甚至没有被调用。

【问题讨论】:

  • 如果您使用集成的 Windows 身份验证,为什么还需要令牌?使用 Windows 身份验证,您的域控制器就是您的“身份提供者”。在您的“资源服务器”上,您可以访问当前用户的身份,并且可以使用授权属性来限制访问等......也许我遗漏了一些东西,但您似乎添加了很多需要注意的安全基础设施由 Windows 身份验证。
  • 因为我需要增加对来自数据库的自定义声明提供的令牌的声明。基本上,我需要匹配一些 AD 组成员资格并使用 AD 组从数据库中添加一些声明。有一个 WebAPI 端点需要授权某些操作,但我还需要在应用程序的客户端(角度)端使用令牌和声明。我有一些 js 代码可以解码 JWT 令牌以获取声明,因此我可以交互地提供一些表面级状态(角度 ui-router)授权,如果这有意义的话?
  • 我还有一个自定义 AuthorizeAttribute,它接受一个字符串,其中包含允许访问该端点的声明的名称。那里的 cliams 校长似乎是正确的。
  • 我可能没有听懂你所说的一切,但如果你的 web api 是由 windows auth 保护的,它可以知道当前用户,它的组成员身份,也可以去你的数据库进行其他决定。对我来说,这负责服务器端。从那里,我希望前端应用程序会从服务器获取授权信息(仅用于此目的的 api?),而不是尝试解码令牌等......
  • 这实际上是它以前的工作方式,也是我试图取代的。我不能让 WebAPI 直接与安全数据库对话。身份验证服务器将被其他服务使用,基本上是单点登录系统,只使用用户当前的 Windows 帐户,而不需要用户名/密码。客户端(WebAPI 应用程序)是完全独立的应用程序,只需要检查 ClaimsPrincipal 上的声明是否存在。

标签: c# asp.net angularjs authentication asp.net-web-api


【解决方案1】:

我已经在 asp.net 核心中使用带有 JwtBearerToken 和“密码流”的 openiddict 完成了 SSO 样式声明授权实现?

如果您将 OpenIddict 与 Windows 身份验证一起使用,则使用 OAuth2/OpenID Connect 隐式流(这是最适合 JS 应用程序的流)很容易实现,而无需任何 WS-Federation 代理:

启动配置:

public void ConfigureServices(IServiceCollection services)
{
    // Register the OpenIddict services.
    services.AddOpenIddict(options =>
    {
        // Register the Entity Framework stores.
        options.AddEntityFrameworkCoreStores<ApplicationDbContext>();

        // Register the ASP.NET Core MVC binder used by OpenIddict.
        // Note: if you don't call this method, you won't be able to
        // bind OpenIdConnectRequest or OpenIdConnectResponse parameters.
        options.AddMvcBinders();

        // Enable the authorization endpoint.
        options.EnableAuthorizationEndpoint("/connect/authorize");

        // Enable the implicit flow.
        options.AllowImplicitFlow();

        // During development, you can disable the HTTPS requirement.
        options.DisableHttpsRequirement();

        // Register a new ephemeral key, that is discarded when the application
        // shuts down. Tokens signed using this key are automatically invalidated.
        // This method should only be used during development.
        options.AddEphemeralSigningKey();
    });

    // Note: when using WebListener instead of IIS/Kestrel, the following lines must be uncommented:
    //
    // services.Configure<WebListenerOptions>(options =>
    // {
    //     options.ListenerSettings.Authentication.AllowAnonymous = true;
    //     options.ListenerSettings.Authentication.Schemes = AuthenticationSchemes.Negotiate;
    // });
}

授权控制器:

public class AuthorizationController : Controller
{
    // Warning: extreme caution must be taken to ensure the authorization endpoint is not included in a CORS policy
    // that would allow an attacker to force a victim to silently authenticate with his Windows credentials
    // and retrieve an access token using a cross-domain AJAX call. Avoiding CORS is strongly recommended.

    [HttpGet("~/connect/authorize")]
    public async Task<IActionResult> Authorize(OpenIdConnectRequest request)
    {
        // Retrieve the Windows principal: if a null value is returned, apply an HTTP challenge
        // to allow IIS/WebListener to initiate the unmanaged integrated authentication dance.
        var principal = await HttpContext.Authentication.AuthenticateAsync(IISDefaults.Negotiate);
        if (principal == null)
        {
            return Challenge(IISDefaults.Negotiate);
        }

        // Note: while the principal is always a WindowsPrincipal object when using Kestrel behind IIS,
        // a WindowsPrincipal instance must be manually created from the WindowsIdentity with WebListener.
        var ticket = CreateTicket(request, principal as WindowsPrincipal ?? new WindowsPrincipal((WindowsIdentity) principal.Identity));

        // Immediately return an authorization response without displaying a consent screen.
        return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
    }

    private AuthenticationTicket CreateTicket(OpenIdConnectRequest request, WindowsPrincipal principal)
    {
        // Create a new ClaimsIdentity containing the claims that
        // will be used to create an id_token, a token or a code.
        var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme);

        // Note: the JWT/OIDC "sub" claim is required by OpenIddict
        // but is not automatically added to the Windows principal, so
        // the primary security identifier is used as a fallback value.
        identity.AddClaim(OpenIdConnectConstants.Claims.Subject, principal.GetClaim(ClaimTypes.PrimarySid));

        // Note: by default, claims are NOT automatically included in the access and identity tokens.
        // To allow OpenIddict to serialize them, you must attach them a destination, that specifies
        // whether they should be included in access tokens, in identity tokens or in both.

        foreach (var claim in principal.Claims)
        {
            // In this sample, every claim is serialized in both the access and the identity tokens.
            // In a real world application, you'd probably want to exclude confidential claims
            // or apply a claims policy based on the scopes requested by the client application.
            claim.SetDestinations(OpenIdConnectConstants.Destinations.AccessToken,
                                  OpenIdConnectConstants.Destinations.IdentityToken);

            // Copy the claim from the Windows principal to the new identity.
            identity.AddClaim(claim);
        }

        // Create a new authentication ticket holding the user identity.
        return new AuthenticationTicket(
            new ClaimsPrincipal(identity),
            new AuthenticationProperties(),
            OpenIdConnectServerDefaults.AuthenticationScheme);
    }
}

使用 OWIN/Katana 版本的 ASOS(OpenIddict 背后的 OpenID Connect 服务器中间件)可以在旧版 ASP.NET 应用程序中实现类似的场景:

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.UseOpenIdConnectServer(options =>
        {
            // Register a new ephemeral key, that is discarded when the application
            // shuts down. Tokens signed using this key are automatically invalidated.
            // This method should only be used during development.
            options.SigningCredentials.AddEphemeralKey();

            // Enable the authorization endpoint.
            options.AuthorizationEndpointPath = new PathString("/connect/authorize");

            // During development, you can disable the HTTPS requirement.
            options.AllowInsecureHttp = true;

            // Implement the ValidateAuthorizationRequest event to validate the response_type,
            // the client_id and the redirect_uri provided by the client application.
            options.Provider.OnValidateAuthorizationRequest = context =>
            {
                if (!context.Request.IsImplicitFlow())
                {
                    context.Reject(
                        error: OpenIdConnectConstants.Errors.UnsupportedResponseType,
                        description: "The provided response_type is invalid.");

                    return Task.FromResult(0);
                }

                if (!string.Equals(context.ClientId, "spa-application", StringComparison.Ordinal))
                {
                    context.Reject(
                        error: OpenIdConnectConstants.Errors.InvalidClient,
                        description: "The provided client_id is invalid.");

                    return Task.FromResult(0);
                }

                if (!string.Equals(context.RedirectUri, "http://spa-app.com/redirect_uri", StringComparison.Ordinal))
                {
                    context.Reject(
                        error: OpenIdConnectConstants.Errors.InvalidClient,
                        description: "The provided redirect_uri is invalid.");

                    return Task.FromResult(0);
                }

                context.Validate();

                return Task.FromResult(0);
            };

            // Implement the HandleAuthorizationRequest event to return an implicit authorization response.
            options.Provider.OnHandleAuthorizationRequest = context =>
            {
                // Retrieve the Windows principal: if a null value is returned, apply an HTTP challenge
                // to allow IIS/SystemWeb to initiate the unmanaged integrated authentication dance.
                var principal = context.OwinContext.Authentication.User as WindowsPrincipal;
                if (principal == null)
                {
                    context.OwinContext.Authentication.Challenge();
                    return Task.FromResult(0);
                }

                // Create a new ClaimsIdentity containing the claims that
                // will be used to create an id_token, a token or a code.
                var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationType);

                // Note: the JWT/OIDC "sub" claim is required by OpenIddict
                // but is not automatically added to the Windows principal, so
                // the primary security identifier is used as a fallback value.
                identity.AddClaim(OpenIdConnectConstants.Claims.Subject, principal.GetClaim(ClaimTypes.PrimarySid));

                // Note: by default, claims are NOT automatically included in the access and identity tokens.
                // To allow OpenIddict to serialize them, you must attach them a destination, that specifies
                // whether they should be included in access tokens, in identity tokens or in both.

                foreach (var claim in principal.Claims)
                {
                    // In this sample, every claim is serialized in both the access and the identity tokens.
                    // In a real world application, you'd probably want to exclude confidential claims
                    // or apply a claims policy based on the scopes requested by the client application.
                    claim.SetDestinations(OpenIdConnectConstants.Destinations.AccessToken,
                                          OpenIdConnectConstants.Destinations.IdentityToken);

                    // Copy the claim from the Windows principal to the new identity.
                    identity.AddClaim(claim);
                }

                context.Validate(identity);

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

客户端代码不应与任何其他使用隐式流的 JS 应用程序不同。您可以查看此示例,了解如何使用 oidc-client JS 库实现它:https://github.com/openiddict/openiddict-samples/tree/master/samples/ImplicitFlow/AureliaApp

【讨论】:

  • 我非常感谢您为该答案付出的努力,但不幸的是,OpenIddict 仅适用于 .NET Core,我目前还没有使用的奢侈。 :(
  • @Brandon 哎呀,对不起。我读到您的授权服务器是一个单独的应用程序,我错误地认为您可以使用 ASP.NET Core。虽然我绝对推荐使用 ASP.NET Core,因为它为 Windows 身份验证提供了更好的故事,但您可以使用 ASOS 的 OWIN/Katana 版本(背后的 OpenID Connect 服务器中间件)实现类似的东西OpenIddict:nuget.org/packages/Owin.Security.OpenIdConnect.Server。我更新了我的答案,以包括如何做到这一点的小样本。
  • 我希望可以。我已经将 ASP.NET Core 用于其他几个项目,我喜欢它。不幸的是,在这种情况下我无法控制。
  • 为了让 windows 身份验证与 web api 一起工作,这应该放在一个单独的项目中,比如启用 windows 身份验证的 Auth Server,还是可以与 web api 在同一个项目中?我使用的是 .net 4.6,所以与 Openiddict 相比,我会失去任何功能还是会遇到使用 ASOS 的任何障碍?这与使用 Identity Server 相比如何?我不敢相信它看起来那么容易使用!我已经使用 Identity Server 3 将近一周了,花了几天时间试图弄清楚如何让它工作,但我仍然对它的零碎工作感到迷茫。
【解决方案2】:

所以最终这里的重点是使用来自数据库的声明来增加对现有 ClaimsPrincipal 的声明,并希望能够在 javascript 中使用 JWT。我无法使用 IdentityServer3 让它工作。我最终通过使用操作上的属性来提供声明名称来实现 IAuthenticationFilter 和 IAuthorizationFilter,从而推出了自己的基本解决方案。

首先,authorize 属性只取用户访问该操作的声明名称。

public class AuthorizeClaimAttribute : Attribute
{
    public string ClaimValue;
    public AuthorizeClaimAttribute(string value)
    {
        ClaimValue = value;
    }
}

然后是 Authorize 过滤器,它什么都不做,只是检查用户是否拥有来自属性的声明。

public class AuthorizeClaimFilter : AuthorizeAttribute, IAuthorizationFilter
{
    private readonly string _claimValue;

    public AuthorizeClaimFilter(string claimValue)
    {
        _claimValue = claimValue;
    }

    public override async Task OnAuthorizationAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
    {            
        var p = actionContext.RequestContext.Principal as ClaimsPrincipal;

        if(!p.HasClaim("process", _claimValue))
            HandleUnauthorizedRequest(actionContext);

        await Task.FromResult(0);
    }

    protected override void HandleUnauthorizedRequest(HttpActionContext actionContext)
    {
        actionContext.Response = new HttpResponseMessage(HttpStatusCode.Forbidden);
    }

}

身份验证过滤器调用 webapi 端点(使用 Windows 身份验证)从数据库中获取自定义“声明”的用户列表。 WebAPI 只是一个标准的 webapi 实例,没什么特别的。

public class ClaimAuthenticationFilter : ActionFilterAttribute, IAuthenticationFilter
{
    public ClaimAuthenticationFilter()
    {
    }

    public async Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
    {

        if (context.Principal != null && context.Principal.Identity.IsAuthenticated)
        {
            var windowsPrincipal = context.Principal as WindowsPrincipal;
            var handler = new HttpClientHandler()
            {
                UseDefaultCredentials = true
            };

            HttpClient client = new HttpClient(handler);
            client.BaseAddress = new Uri("http://localhost:21989");// to be stored in config

            var response = await client.GetAsync("/Security");
            var contents = await response.Content.ReadAsStringAsync();
            var claimsmodel = JsonConvert.DeserializeObject<List<ClaimsModel>>(contents);

            if (windowsPrincipal != null)
            {
                var name = windowsPrincipal.Identity.Name;
                var identity = new ClaimsIdentity();


                foreach (var claim in claimsmodel)
                {
                    identity.AddClaim(new Claim("process", claim.ClaimName));
                }

                var claimsPrincipal = new ClaimsPrincipal(identity);
                context.Principal = claimsPrincipal;
            }
        }
        await Task.FromResult(0);
    }

    public async Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
    {
        var challenge = new AuthenticationHeaderValue("Negotiate");
        context.Result = new ResultWithChallenge(challenge, context.Result);
        await Task.FromResult(0);
    }
}

过滤器使用我的 DI 框架(在本例中为 ninject)绑定到属性。

 this.BindHttpFilter<AuthorizeClaimFilter>(FilterScope.Action)
             .WhenActionMethodHas<AuthorizeClaimAttribute>()
         .WithConstructorArgumentFromActionAttribute<AuthorizeClaimAttribute>("claimValue", o => o.ClaimValue);

这适用于我的目的,WebAPI 端点在 WebAPI 实例和 AngularJS 应用程序中都可以使用。然而,这显然不是理想的。我真的更愿意使用“真正的”身份验证/授权过程。我不敢说这是问题的答案,但它是我能想出的唯一解决方案,我不得不做一些事情。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-01-28
    • 2018-09-02
    • 1970-01-01
    • 1970-01-01
    • 2014-07-27
    • 2018-04-19
    • 2014-08-19
    • 1970-01-01
    相关资源
    最近更新 更多