Keycloak client for ASP.NET Core - asp.net

Is there any existing Keycloak client for Asp.net Core? I have found a NuGet package for .net but it doesn't work with Core. Do you have any ideas how to easily integrate with this security server (or maybe using any other alternatives)?

I've played a bit with this today. The most straightforward way is too use OpenId standard.
In Startup.cs I used OpenIdConnect Authentication:
public void Configure(...)
{ (...)
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme,
AutomaticAuthenticate = true,
CookieHttpOnly = true,
CookieSecure = CookieSecurePolicy.SameAsRequest
});
app.UseOpenIdConnectAuthentication(CreateKeycloakOpenIdConnectOptions());`(...)
}`
OpenIdConnectOptions method:
private OpenIdConnectOptions CreateKeycloakOpenIdConnectOptions()
{
var options = new OpenIdConnectOptions
{
AuthenticationScheme = "oidc",
SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme,
Authority = Configuration["Authentication:KeycloakAuthentication:ServerAddress"]+"/auth/realms/"+ Configuration["Authentication:KeycloakAuthentication:Realm"],
RequireHttpsMetadata = false, //only in development
PostLogoutRedirectUri = Configuration["Authentication:KeycloakAuthentication:PostLogoutRedirectUri"],
ClientId = Configuration["Authentication:KeycloakAuthentication:ClientId"],
ClientSecret = Configuration["Authentication:KeycloakAuthentication:ClientSecret"],
ResponseType = OpenIdConnectResponseType.Code,
GetClaimsFromUserInfoEndpoint = true,
SaveTokens = true
};
options.Scope.Add("openid");
return options;
}
In appsettings.json add configuration for Keycloak:
{
(...),
"Authentication": {
"KeycloakAuthentication": {
"ServerAddress": "http://localhost:8180",
"Realm": "demo",
"PostLogoutRedirectUri": "http://localhost:57630/",
"ClientId": "KeycloakASPNETCore",
"ClientSecret": "secret-get-it-in-keycloakConsole-client-credentials"
}
}
}
Keycloak client is configuerd as followed:
Client settings,
I've added 'accounting' role for test,
I added mapper 'member_of' of type 'User Client Role' for roles so that roles are added in the claims
If I want to Authorize user by role I do something like this:
Add authorization by claims in ConfigureServices method:
public void ConfigureServices(IServiceCollection services)
{
(...)
services.AddAuthorization(options =>
{
options.AddPolicy("Accounting", policy =>
policy.RequireClaim("member_of", "[accounting]")); //this claim value is an array. Any suggestions how to extract just single role? This still works.
});
}
I've edited get method in ValuesController (Default Web API template):
[Authorize(Policy = "Accounting")]
[Route("api/[controller]")]
public class ValuesController : Controller
{
// GET api/values
[HttpGet]
public Dictionary<string,string> Get()
{
var userPrinciple = User as ClaimsPrincipal;
var claims = new Dictionary<string, string>();
foreach (var claim in userPrinciple.Claims)
{
var key = claim.Type;
var value = claim.Value;
claims.Add(key, value);
}
return claims;
}
If I login with user that has accounting role or is in group that has accounting role, it should display my user claims on address localhost:57630/api/values.
I hope this works for you.
Edit: .NET Core 2
Hi everyone! The way my app works changed quite a bit and I have not fully tested .NET Core 2 yet, but you can still try connecting to Keycloak like this in ConfigureServices:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = Configuration["Authentication:KeycloakAuthentication:ServerAddress"] + "/auth/realms/" + Configuration["Authentication:KeycloakAuthentication:Realm"];
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidAudiences = new string[] { "curl", "financeApplication", "accountingApplication", "swagger"}
};
options.RequireHttpsMetadata = false; //for test only!
options.SaveToken = true;
options.Validate();
});
And in Configure:
app.UseAuthentication();
You can access your token later with IHttpContextAccessor httpContextAccessor, for example:
public KeycloakAuthorizationRequirementHandler(IConfiguration config,
IHttpContextAccessor httpContextAccessor,
IMemoryCache memoryCache)
{
_config = config;
_httpContextAccessor = httpContextAccessor;
_memoryCache = memoryCache;
}
//get accessToken
var accessToken = _httpContextAccessor.HttpContext.GetTokenAsync("access_token");
_httpContextAccessor.HttpContext.Items["username"] = username;
Tell me how it goes.

If you want to use standard .Net Role mappings with Keycloak Client Roles, setup like so:
Startup.cs:
services.AddAuthorization(options =>
{
options.AddPolicy("Users", policy =>
policy.RequireRole("Users"));
});
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Authority = Configuration["Authentication:oidc:Authority"]
options.ClientId = Configuration["Authentication:oidc:ClientId"];
options.ClientSecret = Configuration["Authentication:oidc:ClientSecret"];
options.RequireHttpsMetadata = false;
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
options.RemoteSignOutPath = "/SignOut";
options.SignedOutRedirectUri = "Redirect-here";
options.ResponseType = "code";
});
appsettings.json:
"Authentication": {
"oidc": {
"Authority":"http://your-keycloak-server/auth/realms/your-realm",
"ClientId":"Your-Client-Name",
"ClientSecret":"Your-client-secret"
}
}
Keycloak Client Settings:
Create new Token Mapper
Mapper-Values (enter your own client name)
Now you can use standard authorize role statements to apply your Keycloak Client Roles to your ASP.NET project:
[Authorize(Roles = "Users")]

The thing that worked for us was setting these things in Startup.cs (it's cookie based authentication):
public void Configure(...)
{
(...)
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme,
AutomaticAuthenticate = true,
CookieHttpOnly = true,
CookieSecure = CookieSecurePolicy.SameAsRequest
});
app.UseOpenIdConnectAuthentication(CreateOpenIdConnectOptions(_customConfig));
(...)
}
And setting up the options:
private OpenIdConnectOptions CreateOpenIdConnectOptions(CustomConfigurationFile configuration)
{
var options = new OpenIdConnectOptions
{
AuthenticationScheme = "oidc",
SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme,
Authority = configuration.ServerAddress + "/auth/realms/" + configuration.Realm,
RequireHttpsMetadata = true,
PostLogoutRedirectUri = configuration.SystemAddress,
ClientId = configuration.ClientId,
ClientSecret = configuration.ClientSecret,
ResponseType = OpenIdConnectResponseType.Code,
GetClaimsFromUserInfoEndpoint = true,
SaveTokens = true
};
options.Scope.Clear();
options.Scope.Add("openid");
return options;
}

Could we get an up-to-date answer on this question with .net core 5+? I've recently installed keycloak version 13.0.0 & its working allowing me to use single sign on with a couple apps. Now for the real reason I installed keycloak, for webapi authentication. Based on the answers above I've installed Microsoft.AspNetCore.Authentication.OpenIdConnect & have been working to get this to work on both the webapi side and on the client side.

For those who building KeyCloak-based authentication for .Net 6 backend and looking for JWT-token based solution here is a code to add in ConfigureServices of your app with Delobytes.AspNetCore.Infrastructure client:
services.AddKeyCloakAuthentication("SchemeName", true, options =>
{
options.Authority = "https://mykeycloakinstallation.com/auth/realms/myrealm"; //"issuer" endpoint
options.Audience = "account";
options.OpenIdConfigurationEndpoint = "https://mykeycloakinstallation.com/auth/realms/myrealm/.well-known/openid-configuration";
options.TokenValidationParameters = new TokenValidationOptions
{
RequireExpirationTime = true,
RequireSignedTokens = true,
ValidateIssuer = true,
ValidIssuer = "https://mykeycloakinstallation.com/auth/realms/myrealm",
ValidateAudience = true,
ValidAudience = "account",
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(2),
};
});

Related

SignalR JWT Authorization .NET Framework

I'm trying to authorize via JWT to Signalr through WebSockets with JWT access token. But I get Unauthorize response.
This is my signalR configuration in Startup class:
app.Map("/signalr", map =>
{
map.UseCors(CorsOptions.AllowAll);
map.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions()
{
Provider = new QueryStringOAuthBearerProvider()
});
var hubConfiguration = new HubConfiguration
{
EnableDetailedErrors = true,
EnableJSONP = true
};
map.RunSignalR(hubConfiguration);
});
// get your hub context
var hubContext = GlobalHost.ConnectionManager.GetHubContext<ChatHub>();
This is my Auth Configuration
app.CreatePerOwinContext<IdentityContext>(IdentityContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>((options, context) => ApplicationUserManager.Create(options, context.Get<IdentityContext>()));
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
app.SetDefaultSignInAsAuthenticationType(WsFederationAuthenticationDefaults.AuthenticationType);
app.UseJwtBearerAuthentication(new JwtBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateIssuerSigningKey = true,
ValidIssuer = ConfigManager.EnumDocolaEnvironment.ToString(),
ValidAudience = ConfigManager.EnumDocolaEnvironment.ToString(),
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(ConfigurationManager.AppSettings["JWT:SecretKey"]))
}
});
The auth configuration is before the signalr configuration.
Here is the SignalR auth provider to get the access token:
using Microsoft.Owin.Security.OAuth;
using System.Threading.Tasks;
public class QueryStringOAuthBearerProvider : OAuthBearerAuthenticationProvider
{
public override Task RequestToken(OAuthRequestTokenContext context)
{
var value = context.Request.Query.Get("access_token");
if (!string.IsNullOrEmpty(value))
{
context.Token = value;
}
return Task.FromResult<object>(null);
}
}
In the ChatHub I have the Authorize decorator, I also tried with System.Web.Http.Authorize. In the second case I don't receive an Unauthorize response but in the Context.User I get null.
Thanks in advance.

Setting up OpenID Connect for .NET Core Web API

I have a simple application which uses Angular as front-end and a .NET Core Web API as back-end services. Now I want to secure my WEB API layer. I though I can use OpenID Connect for that purpose. But all the examples or documentation online uses some identity management systems to like (Keycloak, Okta) but i just want to use my user data from a SQL database.
So something like, I hit the WEB API from Angular to get the token generated(using OpenID?) based on the user details sent. The i can just use the token to Authorize users. I want to use OpenID so that i can use some other identity management systems later if i want to.
my startup class in WEB API
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(o =>
{
o.ClientId = "sa";
o.ClientSecret = "sa";
o.Authority = "https://localhost:44352";
o.GetClaimsFromUserInfoEndpoint = true;
o.RequireHttpsMetadata = true;
});
I added a controller with Authorize attribute and added a test method to see what happens when i hit that from Swagger
I see the following error
IOException: IDX20804: Unable to retrieve document from: 'https://localhost:44352/.well-known/openid-configuration'
I am not sure what the error is.
Also I would like to ask if i doing this correct. Can i use the same API (Authority? as in ( o.Authority = "https://localhost:44352";)) to authenticate/get token from).
What you'll need for OpenIDConnect is definitely a Server that implements the oidc-spec.
The .well-known/... url is part of the oidc discovery spec, which Servers like identityserver and keycloak implement. It gives a standardized List of other endpoints to retrieve tokens, get userinfo, get logouturi etc.
Your API does not have such an endpoint, so that's what the error says.
If you want to implement the whole oidc-spec on your own, go for it, but I wouldn't recommend it, it's kind of complex. As I see it, .net core does only implement an openidconnect-client and you're free to choose the server implementation as long as it implements the spec.
Alternatively, have a look at JwtBearer, which is a more lightweight approach to authentication and more easy to implement yourself (and, best of: you could easily change to oidc later on). Good starting points might be these blogposts.
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
}).AddCookie(CookieAuthenticationDefaults.AuthenticationScheme,
options =>
{
options.LoginPath = new PathString("/");
options.AccessDeniedPath = "/Identity/Account/AccessDenied";
options.Cookie = new CookieBuilder()
{
SecurePolicy = CookieSecurePolicy.SameAsRequest,
Path = "/"
};
options.SlidingExpiration = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(sessionTimeout);
}).AddOpenIdConnect(options =>
{
options.ClientId = Configuration.GetValue<string>("oidc:ClientId");
options.ClientSecret = Configuration["oidc:ClientSecret"];
options.CallbackPath = new PathString("/auth/callback");
options.GetClaimsFromUserInfoEndpoint = true;
options.Authority = Configuration["oidc:Authority"];
options.SignedOutRedirectUri = "/";
options.RequireHttpsMetadata = false;
options.SaveTokens = true;
options.UseTokenLifetime = true;
var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Configuration["oidc:ClientSecret"]));
options.TokenValidationParameters = new TokenValidationParameters
{
RequireSignedTokens = true,
IssuerSigningKey = signingKey,
ValidateAudience = true,
ValidateIssuer = true,
ValidateIssuerSigningKey = true,
};
options.ResponseType = OpenIdConnectResponseType.Code;
options.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet;
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("email");
options.Scope.Add("profile");
options.Events = new OpenIdConnectEvents()
{
OnTicketReceived = context =>
{
var identity = context.Principal.Identity as ClaimsIdentity;
if (identity != null)
{
if (!context.Principal.HasClaim(c => c.Type == ClaimTypes.Name) &&
identity.HasClaim(c => c.Type == "name"))
identity.AddClaim(new Claim(ClaimTypes.Name, identity.FindFirst("name").Value));
if (context.Properties.Items.ContainsKey(".TokenNames"))
{
string[] tokenNames = context.Properties.Items[".TokenNames"].Split(';');
foreach (var tokenName in tokenNames)
{
string tokenValue = context.Properties.Items[$".Token.{tokenName}"];
identity.AddClaim(new Claim(tokenName, tokenValue));
}
}
}
var cp = new ClaimsPrincipal(identity);
context.Principal = cp;
return Task.CompletedTask;
},
//OnTokenValidated = context =>
//{
// ClaimsIdentity identity = (ClaimsIdentity)context.Principal.Identity;
// Claim Name = identity.FindFirst("preferred_username");
// Claim gender = identity.FindFirst(ClaimTypes.Gender);
// Claim sub = identity.FindFirst(ClaimTypes.NameIdentifier);
// var claimsToKeep = new List<Claim> { Name, gender, sub };
// var newIdentity = new ClaimsIdentity(claimsToKeep, identity.AuthenticationType);
// context.Principal = new ClaimsPrincipal(newIdentity);
// return Task.FromResult(0);
//},
OnAuthenticationFailed = context =>
{
context.Response.Redirect("/");
context.HandleResponse();
return Task.CompletedTask;
},
OnRedirectToIdentityProvider = context =>
{
context.ProtocolMessage.SetParameter("pfidpadapterid", Configuration["oidc:pfidpadapterid"]);
return Task.FromResult(0);
}
};
});

Core 2.0 API Auth with JWT returns unauthorized

I'm trying to add Token Authentication with JWT to my .Net Core 2.0 app. I have a simple controller that returns a list of users for testing.
[Authorize]
[Route("api/[controller]")]
public class UsersController : Controller
{
...
[HttpGet]
[Route("api/Users/GetUsers")]
public IEnumerable<ApplicationUser> GetUsers()
{
return _userManager.Users;
}
}
I have an API Controller for Token security. It has a login method which returns a Token string result.
[HttpPost(nameof(Login))]
public async Task<IActionResult> Login([FromBody] LoginResource resource)
{
if (resource == null)
return BadRequest("Login resource must be asssigned");
var user = await _userManager.FindByEmailAsync(resource.Email);
if (user == null || (!(await _signInManager.PasswordSignInAsync(user, resource.Password, false, false)).Succeeded))
return BadRequest("Invalid credentials");
string result = GenerateToken(user.UserName, resource.Email);
// Token is created, we can sign out
await _signInManager.SignOutAsync();
return Ok(result);
}
private string GenerateToken(string username, string email)
{
var claims = new Claim[]
{
new Claim(ClaimTypes.Name, username),
new Claim(ClaimTypes.Email, email),
new Claim(JwtRegisteredClaimNames.Nbf, new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds().ToString()),
new Claim(JwtRegisteredClaimNames.Exp, new DateTimeOffset(DateTime.Now.AddDays(1)).ToUnixTimeSeconds().ToString()),
};
var token = new JwtSecurityToken(
new JwtHeader(new SigningCredentials(
new SymmetricSecurityKey(Encoding.UTF8.GetBytes("the secret that needs to be at least 16 characeters long for HmacSha256")),
SecurityAlgorithms.HmacSha256)),
new JwtPayload(claims));
return new JwtSecurityTokenHandler().WriteToken(token);
}
I have a small console app just for testing the API. When I attempt to Get the Users using the jwt. I receive an immediate "unauthorized". If I remove the "[Authorize]" from the users Controller... success. It appears that my header Authorization is not recognized, but not sure why.
private static async Task<String> GetUsers(String jwt)
{
var url = "https://localhost:44300/";
var apiUrl = $"/api/Users/";
using (var client = new HttpClient() { BaseAddress = new Uri(url) })
{
client.BaseAddress = new Uri(url);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {jwt}");
using (var response = await client.GetAsync(apiUrl))
{
if (response.StatusCode == System.Net.HttpStatusCode.OK)
return await response.Content.ReadAsStringAsync();
else return null;
}
}
}
I'm basing my attempts on the article here ... some of which might be slightly out of date.
Update - Excerpt of Startup.cs
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = "Jwt";
options.DefaultChallengeScheme = "Jwt";
}).AddJwtBearer("Jwt", options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
//ValidAudience = "the audience you want to validate",
ValidateIssuer = false,
//ValidIssuer = "the isser you want to validate",
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("the secret that needs to be at least 16 characeters long for HmacSha256")),
ValidateLifetime = true, //validate the expiration and not before values in the token
ClockSkew = TimeSpan.FromMinutes(5) //5 minute tolerance for the expiration date
};
});
Configure...
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseBrowserLink();
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
//app.UseJwtBearerAuthentication(
// new Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions
// );
}
Solution:
This line was escaping the token therefore causing it to be invalid when passed in the next request:
var result = await response.Content.ReadAsStringAsync();
Replaced with:
var result = await response.Content.ReadAsAsync<string>();
Note: To use this ext method I had to "install-package Microsoft.AspNet.WebApi.Client"
I used JWT authentication in my one of project. I would like to show my implementation, maybe this will help you. But probably you forget to add UseAuthentication(); into configure method in startup class.
startup.cs
public void Configure(IApplicationBuilder app)
{
app.UseAuthentication();
app.UseMvc();
}
public void ConfigureServices(IServiceCollection services)
{
var appSettings = Configuration.GetSection("AppSettings");
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}
)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = true,
ValidAudience = appSettings["JwtAudience"],
ValidateIssuer = true,
ValidIssuer = appSettings["JwtIssuer"],
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(appSettings["JwtSigningKey"]))
};
});
}
generateToken method
private string GenerateToken(string email)
{
SecurityKey securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_appSettings.Value.JwtSigningKey));
var token = new JwtSecurityToken(
issuer: _appSettings.Value.JwtIssuer,
audience: _appSettings.Value.JwtAudience,
claims: new[]
{
new Claim(JwtRegisteredClaimNames.UniqueName, email),
new Claim(JwtRegisteredClaimNames.Email, email),
new Claim(JwtRegisteredClaimNames.NameId, Guid.NewGuid().ToString())
},
expires: DateTime.Now.AddMinutes(_appSettings.Value.JwtExpireMinute),
signingCredentials: new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256)
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
I had created a nuget package NetCore.Jwt to simplify this process recently. I didn't find it worth writing all the code each time you needed a Jwt, when you can handle cookies simply with the function SignInAsync. However, if you prefer the manual way, Celal's answer is a clear and straightforward guide for this process.
Alternatively, you can install NetCore.Jwt and use the following in your startup:
services.AddAuthentication(NetCoreJwtDefaults.SchemeName)
.AddNetCoreJwt(options =>
{
// configure your token options such as secret, expiry, and issuer here
});
In your Login function, you can use the extension function for HttpContext
string token = HttpContext.GenerateBearerToken( new Claim[]
{
new Claim(ClaimTypes.Name, username),
new Claim(ClaimTypes.Email, email),
new Claim(JwtRegisteredClaimNames.Nbf, new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds().ToString()),
new Claim(JwtRegisteredClaimNames.Exp, new DateTimeOffset(DateTime.Now.AddDays(1)).ToUnixTimeSeconds().ToString()),
});
In your program.cs dont forget to have this code (and in order) :
app.UseAuthentication();
app.UseAuthorization();

Having trouble with JWT token in ASP.NET Core app with multiple authentication schemes

I have an ASP.NET Core 2.0 app with both web pages and API. I'm using cookie authentication for web pages and now I want to use JWT Tokens for API methods.
I followed this article to set this up which does a pretty good job in walking us through the process: https://wildermuth.com/2017/08/19/Two-AuthorizationSchemes-in-ASP-NET-Core-2
I am, however, getting a strange response in my API method that I set up to use JWT token. When I hit the API method, I do get a Status 200 response in Postman but I never hit the break point I set up in the API method. More interestingly, Visual Studio debugger is showing a successful request and when I click it, I see a response code 302 even though Postman shows me a 200 -- see below:
Stranger still, the response I see in Postman is the HTML code of my login page which I redirect users to if they're not authenticated.
Here's my configuration for authentication. As you'll see below, I use social logins as well so my configuration is a bit long so I put all the configuration in a separate file in order not to clutter my Startup.cs. I simply call this from the ConfigureServices() method.
public static void MyAppAuthenticationConfig(IServiceCollection services, IConfiguration configuration)
{
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = "my_cookie";
options.DefaultChallengeScheme = "my_cookie";
})
.AddCookie("my_cookie", options =>
{
options.AccessDeniedPath = "/Home/Denied";
options.LoginPath = "/Login";
})
.AddCookie("social_auth_cookie")
.AddOAuth("LinkedIn", options =>
{
options.SignInScheme = "social_auth_cookie";
options.ClientId = "my_client_id";
options.ClientSecret = "my_secret";
options.CallbackPath = "/linkedin-callback";
options.AuthorizationEndpoint = "https://www.linkedin.com/oauth/v2/authorization";
options.TokenEndpoint = "https://www.linkedin.com/oauth/v2/accessToken";
options.UserInformationEndpoint = "https://api.linkedin.com/v1/people/~:(id,first-name,last-name,email-address,picture-url,picture-urls::(original))";
options.Scope.Add("r_basicprofile");
options.Scope.Add("r_emailaddress");
options.Events = new OAuthEvents
{
OnCreatingTicket = OnCreatingTicketLinkedInCallBack,
OnTicketReceived = OnTicketReceivedCallback
};
})
.AddFacebook(options =>
{
options.SignInScheme = "social_auth_cookie";
options.AppId = "my_app_id";
options.AppSecret = "my_secret";
options.Events = new OAuthEvents
{
OnCreatingTicket = OnCreatingTicketFacebookCallback,
OnTicketReceived = OnTicketReceivedCallback
};
})
.AddGoogle(options =>
{
options.SignInScheme = "social_auth_cookie";
options.ClientId = "my_id.apps.googleusercontent.com";
options.ClientSecret = "my_secret";
options.CallbackPath = "/google-callback";
options.Events = new OAuthEvents
{
OnCreatingTicket = OnCreatingTicketGoogleCallback,
OnTicketReceived = OnTicketReceivedCallback
};
})
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, jwtBearerOptions =>
{
jwtBearerOptions.RequireHttpsMetadata = false;
jwtBearerOptions.SaveToken = true;
jwtBearerOptions.Challenge = JwtBearerDefaults.AuthenticationScheme;
jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("my_secret_key")),
ValidateIssuer = true,
ValidIssuer = "myapp-api",
ValidateAudience = true,
ValidAudience = "myapp-client",
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(5)
};
});
}
And this is the API method where I want to use JWT Token.
[HttpGet("testit")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public IActionResult Test()
{
return Ok("Hello World!");
}
I have a break point where I return Ok("Hello World!"); but like I said I never hit it.
What am I doing wrong here?
UPDATE:
When I inspect the ChallangeResult("Bearer"), this is what I see:
And here's how I'm sending my request in Postman:

Get claims from JWT token in ASP.NET Core 2.0 API

I'm using mixed authentication in my ASP.NET Core 2.0 Web and API app. Meaning both cookies and now adding JWT token.
The web part of the app uses cookies and in the API part, I want to use JWT token.
My question is how do I get the claims from JWT token? In my web controllers, I can simply use HttpContext.User; to get the claims stored in a cookie. How do I handle it in my API methods where I want to use JWT token?
Here's my AuthenticationBuilder:
public static void MyAuthenticationConfig(IServiceCollection services, IConfiguration configuration)
{
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = "myApp_cookie";
options.DefaultChallengeScheme = "myApp_cookie";
})
.AddCookie("myApp_cookie", options =>
{
options.AccessDeniedPath = "/Unauthorized";
options.LoginPath = "/Login";
})
.AddCookie("social_auth_cookie")
.AddOAuth("LinkedIn", options =>
{
options.SignInScheme = "social_auth_cookie";
options.ClientId = "my_client_id";
options.ClientSecret = "my_secret";
options.CallbackPath = "/linkedin-callback";
options.AuthorizationEndpoint = "https://www.linkedin.com/oauth/v2/authorization";
options.TokenEndpoint = "https://www.linkedin.com/oauth/v2/accessToken";
options.UserInformationEndpoint = "https://api.linkedin.com/v1/people/~:(id,first-name,last-name,email-address,picture-url,picture-urls::(original))";
options.Scope.Add("r_basicprofile");
options.Scope.Add("r_emailaddress");
options.Events = new OAuthEvents
{
OnCreatingTicket = OnCreatingTicketLinkedInCallBack,
OnTicketReceived = OnTicketReceivedCallback
};
})
.AddFacebook(options =>
{
options.SignInScheme = "social_auth_cookie";
options.AppId = "my_app_is";
options.AppSecret = "my_secret";
options.Events = new OAuthEvents
{
OnCreatingTicket = OnCreatingTicketFacebookCallback,
OnTicketReceived = OnTicketReceivedCallback
};
})
.AddGoogle(options =>
{
options.SignInScheme = "social_auth_cookie";
options.ClientId = "my_id.apps.googleusercontent.com";
options.ClientSecret = "my_secret";
options.CallbackPath = "/google-callback";
options.Events = new OAuthEvents
{
OnCreatingTicket = OnCreatingTicketGoogleCallback,
OnTicketReceived = OnTicketReceivedCallback
};
})
.AddJwtBearer("JwtBearer", jwtBearerOptions =>
{
jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("my_secret")),
ValidateIssuer = true,
ValidIssuer = "my-api",
ValidateAudience = true,
ValidAudience = "my-client",
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(5)
};
});
}
Normally the claims of JWT are automatically added to the ClaimsIdentity.
Source:
https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/af5e5c2b0100e8348c63e2d2bb45612e2080841e/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs#L1110).
So you should be able to just use 'User' property of the base 'Controller' class.
public async Task<IActionResult> Get()
{
// TODO Move 'Claims' extraction code to an extension method
var address = User.Claims.Where('GET THE NEEDED CLAIM');
...
}
I never had any problems getting the claims from a JWT token.
But I only used IdentityServer4.AccessTokenValidation so far. But internally it uses the Microsoft JWT Handler afaik.
Couldn't comment because my post would be to long, so making a seperate post instead.
If you follow the guide/link ErazerBrecht posted, the claims are indeed stored in the ClaimsPrincipal User. I created an extension method to retrieve the claim.
Note that I use an Enum for registering my claims. I use a dictionary to pass my claims to the method that generates my token, so my claim key should always be unique.
The extension method:
public static string GetClaim(this ClaimsPrincipal claimsPrincipal, JwtClaim jwtClaim)
{
var claim = claimsPrincipal.Claims.Where(c => c.Type == jwtClaim.ToString()).FirstOrDefault();
if (claim == null)
{
throw new JwtClaimNotFoundException(jwtClaim);
}
return claim.Value;
}
Call it like:
var userId = User.GetClaim(JwtClaim.UserId);

Resources