13

I've asked a question before and the answer that was given was correct but the farther I go down this rabbit hole the more I realize; I don't think I was asking the right question.

Let me just explain this in the most simple terms I can... I have a AngularJS single page app (client), that points at an asp.net webapi (OWIN) site (Resource server?), and a separate asp.net "authorization/authentiation" server.

The auth server will provide authentication and authorization for multiple applications. I need to be able to use the Authorize attribute in the resource server, as well as get a token from from angular. I also need to use windows authentication (integrated) for everything, no usernames or passwords. The claims information is stored in a database and needs to be added to the token.

I've done a SSO style claims authoriztion implementation in asp.net core using openiddict with JwtBearerToken and 'password flow?' And wanted to try to do something similar (token, etc). I have a basic understanding of how that works from my previous implmentation, but I am completely lost trying to figure out how to get JWT working with Windows Auth. The answer to my previous question provided some good suggestions but I am having a hard time seeing how that applies in this scenario.

Currently I have been trying to get IdentityServer3 to do this, using the WindowsAuthentication extensions, mainly pulled from the samples. But I am really struggling to tie this together with the client and actually get something working. The current client and server code is below, mind you I really don't know if this is even close to the correct solution.

Client:

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: I see the auth endpoints requests for "/.well-known/openid-configuration" as well as "/.well-known/jwks" and I have the Authorize attribute on a controller action which is being called, but I dont see anything else happening on the auth side. I also added a ICustomClaimsProvider implmentation to the usewindowsauthservice WindowsAuthenticationOptions but that doesnt even get called.

Community
  • 1
  • 1
Brandon
  • 830
  • 1
  • 15
  • 35
  • If you are using integrated windows authentication, why do you even need the token? With windows auth, your domain controller is your "identity provider". On your "resource server", you have access to the current user's identity and can use authorize attributes to limit access, etc... Maybe I'm missing something, but you seem to be adding a lot of security infrastructure that is taken care of by windows auth. – snow_FFFFFF Dec 27 '16 at 15:23
  • Because I need to augment claims on the token provided with custom claims from a database. Basically, I need to match on some AD group memberships and add some claims from the db using the AD groups. There is a WebAPI endpoint that needs to authroize some actions, but I also need to consume the tokens and the claims in the client (angular) side of the app. I have some js code that decodes JWT tokens to get the claims for so I can interactively provide some surface level state (angular ui-router) authorization if that makes any sense? – Brandon Dec 27 '16 at 15:30
  • I also have a custom AuthorizeAttribute that takes a string with contains the name of the claims allowed to access that endpoint. The cliams principal seems to be correct in there. – Brandon Dec 27 '16 at 15:38
  • I may not be following everything you are saying, but if your web api is secured by windows auth, it can be aware of the current user, its group memberships and can also go to your database to make other decisions. To me, that takes care of the server-side. From there, I would expect that the front-end app would get authorization information from the server (an api just for that purpose?) as opposed to trying to decode tokens, etc... – snow_FFFFFF Dec 27 '16 at 15:39
  • That is actually the way it was working before, and is what Im trying to replace. I can't have the WebAPI talking directly to the security database. The auth server is going to be used by other services, basically a single sign on system just using the users current windows accounts without requiring username/password. The clients (WebAPI applications) are completely separate applications and just need to check for existence of claims on the ClaimsPrincipal. – Brandon Dec 27 '16 at 16:17
  • I am currently using the sample provided here: https://github.com/IdentityServer/IdentityServer3.Samples/tree/master/source/WebHost%20(Windows%20Auth%20All-in-One) for guidance. I was using the client code above, but I dont see any example clients provided for that flow, so Im not really sure if what I did above was the correct solution. – Brandon Dec 27 '16 at 21:32

2 Answers2

8

I've done a SSO style claims authoriztion implementation in asp.net core using openiddict with JwtBearerToken and 'password flow?'

If you were to use OpenIddict with Windows authentication, it would be quite easy to implement using the OAuth2/OpenID Connect implicit flow (which is the most appropriate flow for a JS app), without needing any WS-Federation proxy:

Startup configuration:

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

Authorization controller:

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

A similar scenario can be implemented in legacy ASP.NET apps using the OWIN/Katana version of ASOS, the OpenID Connect server middleware behind 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);
            };
        });
    }
}

The client-side code shouldn't be different from any other JS application using the implicit flow. You can take a look at this sample to see how you can implement it with the oidc-client JS library: https://github.com/openiddict/openiddict-samples/tree/master/samples/ImplicitFlow/AureliaApp

Kévin Chalet
  • 39,509
  • 7
  • 121
  • 131
  • I do appreciate the effort you put into that answer, but OpenIddict is unfortunately only for .NET Core, which I currently don't have the luxury of using. :( – Brandon Dec 28 '16 at 14:54
  • 1
    @Brandon woops, sorry. I read that your authorization server was a separate application and I mistakenly concluded you could use ASP.NET Core for it. Though I'd definitely recommend going with ASP.NET Core as it offers a **much better** story for Windows authentication, you can implement something similar with the OWIN/Katana version of ASOS, the OpenID Connect server middleware behind OpenIddict: https://www.nuget.org/packages/Owin.Security.OpenIdConnect.Server. I updated my answer to include a tiny sample of how to do that. – Kévin Chalet Dec 28 '16 at 15:47
  • I wish I could. Ive used ASP.NET Core for several other projects and I love it. Out of my control in this case unfortunately. – Brandon Dec 28 '16 at 15:58
  • For windows authentication to work with web api should this go in a separate project like an Auth Server with windows authentication enable or can this be in the same project as the web api? I'm using .net 4.6 so will I lose any features or will I run into any snags using ASOS compare to Openiddict? How does this compare to using Identity Server? I can't believe it looks that easy to use! I've been working with Identity Server 3 for almost a week and took days to try to figure out how to get it to work and I'm still lost with bits and pieces of it. – Fab May 04 '17 at 12:32
2

So ultimately the whole point here was to augment claims on the existing ClaimsPrincipal with claims from the database and hopefully be able to use JWT's in the javascript. I was unable to get that to work using IdentityServer3. I ended up rolling my own rudimentary solution by implementing IAuthenticationFilter and IAuthorizationFilter using an attribute on the actions to supply the claim name.

First the authorize attribute does nothing but take the name of the claim that the user should have to access the action.

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

Then the Authorize filter which does nothing but check to see if the user has the claim from the attribute.

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

}

The Authentication filter which calls the webapi endpoint (which is using windows authentication) to get the users list of custom "claims" from the database. The WebAPI is just a standard webapi instance, nothing special at all.

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

The filters are bound to the attribute using my DI framework (ninject in this case).

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

This works for my purposes, and the web api endpoint consumable both in the WebAPI instance and in the AngularJS app. However it is obviously NOT ideal. I really would have preferred to use 'real' authentication/authorization processes. I hesitate to say this is the answer to the question, but it is the only solution I could come up with the time that I had to make something work.

Brandon
  • 830
  • 1
  • 15
  • 35