Arzu Suleymanov
Arzu Suleymanov

Reputation: 691

How to use keycloak in .net core 5.0 with AspNet Core Identity?

I am using aspnet core 5.0 webapi with CQRS in my project and already have jwt implementation. Not using role management from aspnet core but manually added for aspnet users table role field and it is using everywhere. In internet I can't find any article to implement keycloak for existing authentication and authorization. My point is for now users login with their email+password, idea is not for all but for some users which they already stored in keycloak, or for some users we will store there, give option login to our app using keycloak as well.

Scenario 1:

I have [email protected] in both in my db and in keycloak and both are they in admin role, I need give access for both to login my app, first scenario already working needs implement 2nd scenarion beside first.

Found only this article which implements securing app (as we have already and not trying to replace but extend)

Medium keycloak

My jwt configuration looks like:

public static IServiceCollection AddCustomAuthentication(this IServiceCollection services,
            IConfiguration configuration)
        {
            var key = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(configuration.GetSection("AppSettings:Token").Value));
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(opt =>
                {
                    opt.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuerSigningKey = true,
                        IssuerSigningKey = key,
                        ValidateAudience = false,
                        ValidateIssuer = false,
                        ClockSkew = TimeSpan.Zero
                    };
                    opt.Events = new JwtBearerEvents
                    {
                        OnAuthenticationFailed = context =>
                        {
                            if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
                            {
                                context.Response.Headers.Add("Token-Expired", "true");
                            }

                            return Task.CompletedTask;
                        }
                    };
                });
           

         return services;
      }

My jwt service looks like:

public JwtGenerator(IConfiguration config)
        {
            _key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config.GetSection("AppSettings:Token").Value));
        }
        public string CreateToken(User user)
        {
            var claims = new List<Claim>
            {
                new(ClaimTypes.NameIdentifier, user.Id),
                new(ClaimTypes.Email, user.Email),
                new(ClaimTypes.Name, user.UserName),
                new(ClaimTypes.Role, user.Role.ToString("G").ToLower())
            };
            
            var creds = new SigningCredentials(_key, SecurityAlgorithms.HmacSha512);
            var tokenDescriptor = new SecurityTokenDescriptor
            {
                Subject = new ClaimsIdentity(claims),
                Expires = DateTime.UtcNow.AddMinutes(15),
                SigningCredentials = creds
            };
            var tokenHandler = new JwtSecurityTokenHandler();
            var token = tokenHandler.CreateToken(tokenDescriptor);
            return tokenHandler.WriteToken(token);
        }

My login method looks like:

 public async Task<GetToken> Handle(LoginCommand request, CancellationToken cancellationToken)
        {
            var user = await _userManager.FindByEmailAsync(request.Email);
            if (user == null)
                throw new BadRequestException("User not found");
            
            UserManagement.ForbiddenForLoginUser(user);

            var result = await _signInManager.CheckPasswordSignInAsync(user, request.Password, false);

            if (result.Succeeded)
            {
                user.IsRoleChanged = false;
                
                RefreshToken refreshToken = new RefreshToken
                {
                    Name = _jwtGenerator.GenerateRefreshToken(),
                    DeviceName = $"{user.UserName}---{_jwtGenerator.GenerateRefreshToken()}",
                    User = user,
                    Expiration = DateTime.UtcNow.AddHours(4)
                };
                await _context.RefreshTokens.AddAsync(refreshToken, cancellationToken);
                await _context.SaveChangesAsync(cancellationToken);

                return new GetToken(_jwtGenerator.CreateToken(user),refreshToken.Name);
            }

            throw new BadRequestException("Bad credentials");
        }

My authorization handler:

public static IServiceCollection AddCustomMvc(this IServiceCollection services)
        {
            services.AddMvc(opt =>
            {
                var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
                opt.Filters.Add(new AuthorizeFilter(policy));
                // Build the intermediate service provider
                opt.Filters.Add<CustomAuthorizationAttribute>();
            }).AddFluentValidation(cfg => cfg.RegisterValidatorsFromAssemblyContaining<CreateProjectCommand>());

            return services;
        }

What is best practise to implement keycloak authentiaction+authorization beside my current approach and give users to login with two scenarios, normal and keycloak login.

P.S. Ui is different and we are using angular this one just webapi for backend.

Upvotes: 4

Views: 13482

Answers (1)

Random12b3
Random12b3

Reputation: 169

Since your login method returns a jwt, you could configure multiple bearer tokens by chaining .AddJwtBearer(), one for your normal login and one for keycloak.

Here is a link to a question that might solve your problem: Use multiple jwt bearer authentication.

Keycloak configuration:

  • Go to Roles -> Realm Roles and create a corresponding role.
  • Go to Clients -> Your client -> Mappers.
  • Create a new role mapper and select "User Realm Role" for Mapper Type, "roles" for Token Claim Name and "String" for Claim JSON Type. Without the mapping the role configured before would be nested somewhere else in the jwt.

You can use the debugger at jwt.io to check if your token is correct. The result should look like this:

{
  "exp": 1627565901,
  "iat": 1627564101,
  "jti": "a99ccef1-afa9-4a62-965b-15e8d33de7de",

  // [...]
  // roles nested in realm_access :(
  "realm_access": {
    "roles": [
      "offline_access",
      "uma_authorization",
      "Admin"
    ]
  },

  // [...]
  // your mapped roles in your custom claim
  "roles": [
    "offline_access",
    "uma_authorization",
    "Admin"
  ]

  // [...]
}

Upvotes: 2

Related Questions