3

I tried to implement IdentityServer as it is explained at https://aspnetboilerplate.com/Pages/Documents/Zero/Identity-Server

But the sample does not work.

I started a Core 2.0 Angular project from ASP.NET Boilerplate. Is there any updated working sample based on the documentation?

There is more than one problem, but one of them is with AuthConfigurer.cs.

The API caller (client) cannot pass the token validation.

Actually, there is a token generation code in TokenAuthController.cs:

private string CreateAccessToken(IEnumerable<Claim> claims, TimeSpan? expiration = null)
{
    var now = DateTime.UtcNow;
    var jwtSecurityToken = new JwtSecurityToken(
        issuer: _configuration.Issuer,
        audience: _configuration.Audience,
        claims: claims,
        notBefore: now,
        expires: now.Add(expiration ?? _configuration.Expiration),
        signingCredentials: _configuration.SigningCredentials
    );
    return new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
}

But in the Startup class, AddIdentity and AddAuthentication create different token values and validation rules.

services.AddIdentityServer()
        .AddDeveloperSigningCredential()
        .AddInMemoryIdentityResources(IdentityServerConfig.GetIdentityResources())
        .AddInMemoryApiResources(IdentityServerConfig.GetApiResources())
        .AddInMemoryClients(IdentityServerConfig.GetClients())
        .AddAbpPersistedGrants<IAbpPersistedGrantDbContext>()
        .AddAbpIdentityServer<User>(); ;

services.AddAuthentication().AddIdentityServerAuthentication("IdentityBearer", options =>
{
    options.Authority = "http://localhost:62114/";
    options.RequireHttpsMetadata = false;
});

The token can be generated by both sides. CreateAccessToken is called by the Angular client and by the API client as shown below:

var disco = await DiscoveryClient.GetAsync("http://localhost:21021");

var httpHandler = new HttpClientHandler();
httpHandler.CookieContainer.Add(new Uri("http://localhost:21021/"), new Cookie(MultiTenancyConsts.TenantIdResolveKey, "1")); //Set TenantId
var tokenClient = new TokenClient(disco.TokenEndpoint, "AngularSPA", "secret", httpHandler);
var tokenResponse = await tokenClient.RequestResourceOwnerPasswordAsync("admin", "123qwe", "default-api"); //RequestClientCredentialsAsync("default-api");

But just one of them (according to the Authentication part) cannot pass authentication.

I need both the API client authentication and the Angular client authentication to work.

I have some clue from a link about dual authentication:
https://wildermuth.com/2017/08/19/Two-AuthorizationSchemes-in-ASP-NET-Core-2

But I could not solve this. Any comment is very valuable to solve the problem.

Nkosi
  • 235,767
  • 35
  • 427
  • 472

1 Answers1

5

At the and I managed to solve problem here are the needed modifications;

1- in the TokenAuthController there is a token creation code as shown below;

private static List<Claim> CreateJwtClaims(ClaimsIdentity identity)
        {
            var claims = identity.Claims.ToList();
            var nameIdClaim = claims.First(c => c.Type == ClaimTypes.NameIdentifier);

            // Specifically add the jti (random nonce), iat (issued timestamp), and sub (subject/user) claims.
            claims.AddRange(new[]
            {
                new Claim(JwtRegisteredClaimNames.Sub, nameIdClaim.Value),
                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.Now.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64)
            });

            return claims;
        }

If you start using Identityserver Claims coming from login is totally different from current implementation and "sub" claim is already added to claims. So it is not necessary to add seperately. So please update this as shown below

 private static List<Claim> CreateJwtClaims(ClaimsIdentity identity)
        {
            var claims = identity.Claims.ToList();

            // Specifically add the jti (random nonce), iat (issued timestamp), and sub (subject/user) claims.
            claims.AddRange(new[]
            {
                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.Now.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64)
            });

            return claims;
        }

2- Add Authentcation to startup class as shown below; The most important part is authenticationSchemaName "IdentityBearer" do not forget adding it.

services.AddAuthentication().AddIdentityServerAuthentication("IdentityBearer", options =>
            {
                options.Authority = "http://localhost:21021/";
                options.RequireHttpsMetadata = false;
            });

3- But this is not enough. because if you look at configure methon in startup authontication is registered as

app.UseJwtTokenMiddleware(); 

if you check it it uses "bearer" schema not IdentityBearer as we added above. So we also need anpther authenticaiton registration. Add this line too (have both of them)

    app.UseJwtTokenMiddleware("IdentityBearer");

4- But as you can see there is no method taking string parameter to add UseJwtTokenMiddleware so it is needed to update that class to. please change your class as shown below;

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;

namespace MyProject.Authentication.JwtBearer
{
    public static class JwtTokenMiddleware
    {
        public static IApplicationBuilder UseJwtTokenMiddleware(this IApplicationBuilder app)
        {
            return UseJwtTokenMiddleware(app, JwtBearerDefaults.AuthenticationScheme);
        }

        public static IApplicationBuilder UseJwtTokenMiddleware(this IApplicationBuilder app, string authenticationScheme)
        {
            return app.Use(async (ctx, next) =>
            {
                if (ctx.User.Identity?.IsAuthenticated != true)
                {
                    var result = await ctx.AuthenticateAsync(authenticationScheme);
                    if (result.Succeeded && result.Principal != null)
                    {
                        ctx.User = result.Principal;
                    }
                }

                await next();
            });
        }        
    }
}

Now you have now two different token type and two different validator. YOu can have API client using basic token info and JWT tokens are created by login from angular client. if you debug each request tries to pass two of them but just one of them succeed which is enough for you.

if aspnetboilerplate team updates sample according to this requirement it would be great.

  • By *sample*, do you mean the documentation or the template? – aaron Jan 16 '18 at 15:29
  • Actually it is not easy to see it is needed to add app.UseJwtTokenMiddleware("IdentityBearer"); to have secondary valdation. And that method does nto accept string parameter as schema name. This part can be changed at template. At the end of the document the other parts can be added as explanation because as I I said before both token validaiton is not working at the same time in the given example. Api client or angular client token validaiton work properly (just one of them). – Tuğrul Karakaya Jan 16 '18 at 16:37
  • And adding "sub" claim during login is not necessary and throws error if you start using IdentityServer – Tuğrul Karakaya Jan 16 '18 at 16:47
  • You can [open an issue](https://github.com/aspnetboilerplate/aspnetboilerplate/issues/new) or, even better, a PR on the repo. – aaron Jan 16 '18 at 16:47
  • OK thx. Just a note for readers I followed steps at http://docs.identityserver.io/en/release/topics/apis.html to implement . – Tuğrul Karakaya Jan 16 '18 at 17:03
  • I faced same issue and got it resolved, great answer! – Nitin Sawant Aug 15 '20 at 10:40