AngularJs, WebAPI, JWT, с (интегрированной) аутентификацией Windows

Я спросил question раньше, и ответ, который был дан, был правильным, но чем дальше я спускаюсь с этой кроличьей дыры, тем больше я понимаю; Я не думаю, что задал правильный вопрос.

Позвольте мне объяснить это самым простым термином, я могу... У меня есть одностраничное приложение AngularJS (клиент), которое указывает на сайт asp.net webapi (OWIN) (сервер ресурсов?) и отдельный Сервер авторизации/аутентификации asp.net.

Сервер auth обеспечит аутентификацию и авторизацию для нескольких приложений. Мне нужно иметь возможность использовать атрибут Authorize на сервере ресурсов, а также получить токен из angular. Мне также нужно использовать проверку подлинности Windows (интегрированную) для всего, без имен пользователей или паролей. Информация о претензиях хранится в базе данных и должна быть добавлена ​​в токен.

Я выполнил реализацию авторизации в стиле SSO в ядре asp.net, используя openiddict с JwtBearerToken и "поток паролей"? И хотел попытаться сделать что-то подобное (токен и т.д.). У меня есть базовое понимание того, как это работает с моего предыдущего внедрения, но я полностью потерялся, пытаясь понять, как заставить JWT работать с Windows Auth. Ответ на мой предыдущий вопрос дал несколько хороших предложений, но мне нелегко видеть, как это применимо в этом сценарии.

В настоящее время я пытаюсь получить IdentityServer3 для этого, используя расширения WindowsAuthentication, в основном извлекаемые из образцов. Но я действительно пытаюсь связать это вместе с клиентом и фактически получить что-то работающее. Текущий код клиента и сервера ниже, помните, что я действительно не знаю, насколько это близко к правильному решению.

Клиент:

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

AuthServer:

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

EDIT: я вижу запросы конечных точек auth для "/.well-known/openid-configuration", а также "/.well-known/jwks", и у меня есть атрибут Authorize при вызове контроллера, который вызывается, но я не вижу ничего другого, что происходит на стороне авторизации. Я также добавил внедрение ICustomClaimsProvider в usewindowsauthservice WindowsAuthenticationOptions, но это даже не вызвано.

Ответ 1

Таким образом, в конечном итоге весь смысл здесь заключался в том, чтобы увеличить требования к существующей ClaimsPrincipal с претензиями из базы данных и, мы надеемся, сможет использовать JWT в javascript. Мне не удалось заставить это работать с помощью IdentityServer3. Я закончил свое собственное рудиментарное решение, выполнив IAuthenticationFilter и IAuthorizationFilter, используя атрибут действий для предоставления имени заявки.

Сначала атрибут authorize ничего не делает, кроме как принять имя требования, что пользователю нужно будет получить доступ к действию.

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

Затем фильтр авторизации, который ничего не делает, кроме проверки, чтобы узнать, имеет ли пользователь претензию от этого атрибута.

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

Это работает для моих целей, а конечная точка web api потребляется как в экземпляре WebAPI, так и в приложении AngularJS. Однако он, очевидно, НЕ идеален. Я бы предпочел использовать "реальные" процессы аутентификации/авторизации. Я стесняюсь сказать, что это ответ на вопрос, но это единственное решение, которое я мог придумать, когда мне пришлось что-то сделать.

Ответ 2

Я выполнил реализацию авторизации в стиле SSO в ядре asp.net, используя openiddict с JwtBearerToken и паролем?

Если вы использовали 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);
    }
}

Аналогичный сценарий может быть реализован в старых приложениях ASP.NET с использованием версии ASOS OWIN/Katana, промежуточного ПО сервера OpenID Connect позади OpenIddict:

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, использующего неявный поток. Вы можете взглянуть на этот образец, чтобы узнать, как его реализовать с помощью библиотеки OID-клиента JS: https://github.com/openiddict/openiddict-samples/tree/master/samples/ImplicitFlow/AureliaApp