Trying to set up JWT Auth. using Keycloak on .NET 2.2 but getting 401 error

329 Views Asked by At

I'm trying to add Auth to my application using KeyCloak as a Identity Provider.

Now following this tutorial: Security in React and Web API I've managed to make it work, but only with .NET 6. Now the project that I am working on is using .NET 2.2.

Here is Startup.cs:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Swashbuckle.AspNetCore.Swagger;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;

namespace Test
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

            services.ConfigureJWT(Configuration.GetSection("Keycloak")["ServerRealm"], Configuration.GetSection("Keycloak")["PublicKey"]);
            AddSwaggerDoc(services);
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseHsts();
            }
            app.UseEndpointRouting();
            app.UseAuthentication();

            app.UseSwagger();
            app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "Interview Service API"); });
            app.UseHttpsRedirection();
            app.UseMvc();
        }

        private void AddSwaggerDoc(IServiceCollection services)
        {
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new Info { Title = "Test", Version = "v1" });


                c.AddSecurityDefinition("Bearer", new ApiKeyScheme
                {
                    Description = "Please paste JWT token with Bearer prefix. Example: \"Bearer {your token}\"",
                    Name = "Authorization",
                    In = "header",
                    Type = "apiKey"
                });

                c.AddSecurityRequirement(new Dictionary<string, IEnumerable<string>>
                    {{"Bearer", Array.Empty<string>()}});
            });
        }
    }
}

Here's the Extension class:

using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Threading.Tasks;
using System;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Security;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Http;

namespace Test{
    /// <summary>
    /// Used to get the role within the claims structure used by keycloak, then it adds the role(s) in the ClaimsItentity of ClaimsPrincipal.Identity
    /// </summary>
    public class ClaimsTransformer : IClaimsTransformation
    {
        public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
        {
            ClaimsIdentity claimsIdentity = (ClaimsIdentity)principal.Identity;

            // flatten resource_access because Microsoft identity model doesn't support nested claims
            // by map it to Microsoft identity model, because automatic JWT bearer token mapping already processed here
            if (claimsIdentity.IsAuthenticated && claimsIdentity.HasClaim((claim) => claim.Type == "resource_access"))
            {
                var userRole = claimsIdentity.FindFirst((claim) => claim.Type == "resource_access");

                var content = Newtonsoft.Json.Linq.JObject.Parse(userRole.Value);

                foreach (var role in content["roles"])
                {
                    claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, role.ToString()));
                }
            }

            return Task.FromResult(principal);
        }
    }

    public static class ConfigureServiceAuthentificationExtension
    {
        public static void ConfigureJWT(this IServiceCollection services, string serverRealm, string publicKey)
        {
            var AuthenticationBuilder = services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
            });

            AuthenticationBuilder.AddJwtBearer(options =>
            {
                #region == JWT Token Validation ==

                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateAudience = false,
                    ValidateIssuer = false,
                    ValidIssuers = new[] { serverRealm },
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = BuildRSAKey(publicKey),
                    ValidateLifetime = true
                };

                #endregion

                #region === Event Authentification Handlers ===

                options.Events = new JwtBearerEvents()
                {
                    OnTokenValidated = c =>
                    {
                        Console.WriteLine("User successfully authenticated");
                        return Task.CompletedTask;
                    },
                    OnAuthenticationFailed = c =>
                    {
                        c.NoResult();

                        c.Response.StatusCode = 500;
                        c.Response.ContentType = "text/plain";

                        return c.Response.WriteAsync("An error occured processing your authentication.");
                    }
                };

                #endregion
            });
        }

        private static RsaSecurityKey BuildRSAKey(string publicKey)
        {
            byte[] publicKeyBytes = Convert.FromBase64String(publicKey);
            AsymmetricKeyParameter asymmetricKeyParameter = PublicKeyFactory.CreateKey(publicKeyBytes);
            RsaKeyParameters rsaKeyParameters = (RsaKeyParameters)asymmetricKeyParameter;
            RSAParameters rsaParameters = new RSAParameters();
            rsaParameters.Modulus = rsaKeyParameters.Modulus.ToByteArrayUnsigned();
            rsaParameters.Exponent = rsaKeyParameters.Exponent.ToByteArrayUnsigned();
            RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
            rsa.ImportParameters(rsaParameters);

            return new RsaSecurityKey(rsa);
        }
    }
}

Here's appsettings.json

"Keycloak": {
    "ServerRealm": "http://localhost:8080/realms/Interview-System",
    "Metadata": "http://localhost:8080/realms/Interview-System/.well-known/openid-configuration",
    "ClientId": "interview-client",
    "ClientSecret": "xBeOdZAOnWyZhVD8VlpXg8ioVJaZtl7T",
    "PublicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlJnfj5okGexWc7oJH9eh4d0ZcaJuSghOwwxG1VyhXDH60yrmE3SdWwCQXyjUe9/NCQsPncD8ZAsMVfCxoaUmAgQ7E2cQnNcBuoW41c0T6PA1N6izh67tL4i9YnwcVHVWES9yphnW6tQHjOzFCiw9qM+6kr+EWGEtXDxp2r6GpcW9YfgWqC0r4XaNVzTq3yH00hsPy9QnuF5PsJffEFaVmTjMb8ankE9IcGP3nJPmhLUall+ooHhMmCPIWuk1l9rC6K0nY5T0/BP5BoDMIo1J1tO5n0kvqPxOF5I8YehsdwwioyMzXqN0zhc0PCXC01fGUdlvH7rN779gcAYndtHsbQIDAQAB"
  },

Here's the example of Bearer token that I get from KeyCloak:

eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI1MFlCSHQtRXJfSk0tUjRxYUtnTmJ2YUt0VnlxWjJ4TE0wYW1RS0VCbUlZIn0.eyJleHAiOjE2Njg0ODE1MzksImlhdCI6MTY2ODQ4MTIzOSwianRpIjoiNGI2Y2VkZGUtYzc5OC00OGIyLTgzMWItZjVkZWFmNzE0NDY0IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy9JbnRlcnZpZXctU3lzdGVtIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6Ijc3MjM1MGJlLTc4N2YtNDgyNS1hMDhiLTFmZjM3NGY0NmFlZiIsInR5cCI6IkJlYXJlciIsImF6cCI6ImludGVydmlldy1jbGllbnQiLCJzZXNzaW9uX3N0YXRlIjoiMmJhYjI0M2YtYjdlZS00NTU5LWFkZTgtMTZlMGY3ZDVmY2Y0IiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyJodHRwczovL2xvY2FsaG9zdDo0NDMzNS8iXSwicmVzb3VyY2VfYWNjZXNzIjp7ImludGVydmlldy1jbGllbnQiOnsicm9sZXMiOlsiaW50ZXJ2aWV3ZXIiLCJoci1tYW5hZ2VyIiwiZGV2ZWxvcGVyIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwic2lkIjoiMmJhYjI0M2YtYjdlZS00NTU5LWFkZTgtMTZlMGY3ZDVmY2Y0IiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJyb2xlIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiLCJpbnRlcnZpZXdlciIsImhyLW1hbmFnZXIiLCJkZXZlbG9wZXIiXSwibmFtZSI6InRlc3QgdGVzdCIsInByZWZlcnJlZF91c2VybmFtZSI6InRlc3QiLCJnaXZlbl9uYW1lIjoidGVzdCIsImZhbWlseV9uYW1lIjoidGVzdCJ9.UpHKBA_H3KNSelWyjSX_SlH4TxafxGWbwLOI_PHxlpNgkKKLF2wUZbSh8uXNWD4M1MEERAIbBHW8-fK3Gu5_duh8MzuUFhOFNYkx5CavgfyL9aasyCGLLqQxY3IDsY8BstZtUjPqgjeaCwV-YSeZT7iF5wNzk28I4t29eamadDscnrGp5DuhnZ-inT0-QRJZbPq2UUz-_eSFG4F0yCAWMBN0YweZ7TYr4AQlT4z2IZ1XBwwWJpMccBuMBH_tkeauu30sAVgRkc0nV2jg2AiSLtLPfjJJGra66-ffozOGEt1XJ8QBgLlg0KYIhkENxmcg0wFxcLyJfHexFgOUVu3ITQ

And here's the Controller class:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Test.Controllers
{
    [Route("api/[controller]")]
    [Authorize]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        // GET api/values
        [HttpGet]
        public ActionResult<IEnumerable<string>> Get()
        {
            return new string[] { "value1", "value2" };
        }

        // GET api/values/5
        [HttpGet("{id}")]
        public ActionResult<string> Get(int id)
        {
            return "value";
        }

        // POST api/values
        [HttpPost]
        public void Post([FromBody] string value)
        {
        }

        // PUT api/values/5
        [HttpPut("{id}")]
        public void Put(int id, [FromBody] string value)
        {
        }

        // DELETE api/values/5
        [HttpDelete("{id}")]
        public void Delete(int id)
        {
        }
    }
}

Now, using this exact same approach, I've managed to run the WeatherTemplate on .NET 6, with all roles, but trying to do the same on .NET 2.2 I'm always hitting 401.

I'm confident to assume that the problem might be in this part of StartUp.cs, but I'm not sure how to debug it, as every documentation that I've come across shows the bellow snippet:

private void AddSwaggerDoc(IServiceCollection services)
        {
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new Info { Title = "Test", Version = "v1" });


                c.AddSecurityDefinition("Bearer", new ApiKeyScheme
                {
                    Description = "Please paste JWT token with Bearer prefix. Example: \"Bearer {your token}\"",
                    Name = "Authorization",
                    In = "header",
                    Type = "apiKey"
                });

                c.AddSecurityRequirement(new Dictionary<string, IEnumerable<string>>
                    {{"Bearer", Array.Empty<string>()}});
            });
        }

I've tried the exact same approach on .NET 6, but it works like a charm. Although, the only difference on .NET 6 and .NET 2.2 is how I set up my Swagger. Here's example of .NET 6 AddSwagerGen():

builder.Services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "MyWebApi", Version = "v1" });

                //First we define the security scheme
                c.AddSecurityDefinition("Bearer", //Name the security scheme
                    new OpenApiSecurityScheme
                    {
                        Description = "JWT Authorization header using the Bearer scheme.",
                        Type = SecuritySchemeType.Http, //We set the scheme type to http since we're using bearer authentication
                        Scheme = JwtBearerDefaults.AuthenticationScheme //The name of the HTTP Authorization scheme to be used in the Authorization header. In this case "bearer".
                    });

                c.AddSecurityRequirement(new OpenApiSecurityRequirement{
                    {
                        new OpenApiSecurityScheme{
                            Reference = new OpenApiReference{
                                Id = JwtBearerDefaults.AuthenticationScheme, //The name of the previously defined security scheme.
                                Type = ReferenceType.SecurityScheme
                            }
                        },new List<string>()
                    }
                });
            });

Any help is appreciated, thank you!

Following the tutorial on how to set up Auth. for JWT and RS256, I've stumbled across an issue that I'm understanding.

The expected output is to access the Get method from API call.

EDIT 1: Using Postman, I get the results back, but Swagger is still returning 401

0

There are 0 best solutions below