I have an MVC application and one exposed API Endpoint. I authenticated my MVC application with the defaults from Identity Core, I use User.FindFirstValue(ClaimTypes.NameIdentifier) to find if a certain user is logged in, etc.
For my API Endpoint, I use JWT authentication below is the configuration code for JWT:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(jwt =>
{
var key = Encoding.ASCII.GetBytes(Configuration["Jwt:Secret"]);
jwt.SaveToken = true;
jwt.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false,
RequireExpirationTime = false,
ValidateLifetime = true
};
});
Here is the controller for token request:
[HttpPost]
[Route("token")]
public async Task<IActionResult> Token([FromBody] UserLoginRequest user)
{
if (ModelState.IsValid)
{
var existingUser = await _userManager.FindByEmailAsync(user.Email);
if (existingUser == null)
{
return BadRequest();
}
var isCorrect = await _userManager.CheckPasswordAsync(existingUser, user.Password);
if (isCorrect)
{
var jwtToken = _identityService.GenerateJwtToken(existingUser);
return Ok(new RegistrationResponse()
{
Result = true,
Token = jwtToken
});
}
else
{
return BadRequest();
}
}
return BadRequest();
}
On my MVC controllers, I use [Authorize]
On my API Endpoint i use [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
My GenerateJWTToken method:
public string GenerateJwtToken(IdentityUser user)
{
var jwtTokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_jwtConfig.Secret);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim("Id", user.Id),
new Claim(JwtRegisteredClaimNames.Sub, user.Email),
new Claim(JwtRegisteredClaimNames.Email, user.Email),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
}),
Expires = DateTime.UtcNow.AddHours(6),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha512Signature)
};
var token = jwtTokenHandler.CreateToken(tokenDescriptor);
var jwtToken = jwtTokenHandler.WriteToken(token);
return jwtToken;
}
}
But obviously, this solution fails to function because once I start my MVC Application and try to log in, I get redirected back to Index and I'm still unauthorized. And vice versa with the API, when I make a Postman call, I get the token, and when I try to call my Bookmarks Controller to query user's bookmarks I get zero, although there are bookmarks for that certain user.
Any ideas on how could I make this work would be welcomed.
Inside my JWT Token Generator, I get details of the user I would like to store as claims such as the username, which can be used to identify the user.
public static class JwtTokenExtensions
{
/// <summary>
/// Generates a JWT Bearer token containing the users email
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
public static string GenerateJwtToken(this Identity user)
{
// Set our token claims
Claim[] claims = {
// Unique ID for this token
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString("N")),
new(JwtRegisteredClaimNames.Email, user.Email),
// The username using the Identity name so it fills out the HttpContext.User.Identity.Name value
new(ClaimsIdentity.DefaultNameClaimType, user.UserName),
// Add user Id so that UserManager.GetUserAsync can find the user based on Id
new Claim(ClaimTypes.NameIdentifier, user.Id)
};
// Create the credentials used to generate the token
SigningCredentials credentials =
new SigningCredentials(SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:SecretKey"])),
SecurityAlgorithms.HmacSha256);
// Generate the Jwt Token that lasts for an hour before expiring
JwtSecurityToken token =
new JwtSecurityToken
(Configuration["Jwt:Issuer"],
Configuration["Jwt:Audience"],
claims:claims,
signingCredentials:credentials,
expires: DateTime.Now.AddHours(1));
// Return the generated token.
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
Inside the api controllers with JWT authorization, I can get the user via the HttpContext
var user = await _userManager.GetUserAsync(HttpContext.User);
In the controller for your token request, try adding [AllowAnonymous], like this:
[AllowAnonymous]
[HttpPost]
[Route("token")]
public async Task<IActionResult> Token([FromBody] UserLoginRequest user)
{
// snip...
}
Related
I have been trying to get this working now for quite a some time but can't figure out how to do it properly. I am able to implement Rememeber Me with LocalStorage. However I want to implement Remember Me functionality with JWT using cookie where I would be able to set expiration time. I think I have messed up the login logic? Can somebody point out what is wrong here?
I can also add other parts from my application if necessary.
AuthorizeController.cs:
[HttpPost]
public async Task<IActionResult> Login([FromBody] LoginModel login)
{
ApplicationUser user = await this.SignInManager.UserManager.FindByEmailAsync(login.Email);
if (user == null)
{
List<string> errors = new List<string>();
errors.Add("No such user has been found.");
return BadRequest(new LoginResult
{
Successful = false,
Errors = errors,
});
}
bool emailConfirmed = await this.UserManager.IsEmailConfirmedAsync(user);
if (!emailConfirmed)
{
List<string> errors = new List<string>();
errors.Add("Email not confirmed.");
return BadRequest(new LoginResult
{
Successful = false,
Errors = errors,
});
}
Microsoft.AspNetCore.Identity.SignInResult result =
await this.SignInManager.PasswordSignInAsync(login.Email, login.Password, login.RememberMe, false);
if (!result.Succeeded)
{
List<string> errors = new List<string>();
errors.Add("Email and password are invalid.");
return BadRequest(new LoginResult
{
Successful = false,
Errors = errors,
});
}
IList<string> roles = await this.SignInManager.UserManager.GetRolesAsync(user);
List<Claim> claims = new List<Claim>
{
new Claim(ClaimTypes.Name, login.Email)
};
ClaimsIdentity identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
ClaimsPrincipal principal = new ClaimsPrincipal(identity);
AuthenticationProperties props = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTime.UtcNow.AddMonths(1)
};
// to register the cookie to the browser
this.HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal, props).Wait();
foreach (string role in roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(this.Configuration["JwtSecurityKey"]));
SigningCredentials creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
DateTime expiry = DateTime.Now.AddDays(Convert.ToInt32(this.Configuration["JwtExpiryInDays"]));
JwtSecurityToken token = new JwtSecurityToken(
this.Configuration["JwtIssuer"],
this.Configuration["JwtAudience"],
claims,
expires: expiry,
signingCredentials: creds
);
return Ok(new LoginResult
{
Successful = true,
Token = new JwtSecurityTokenHandler().WriteToken(token),
});
}
Startup.cs:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["JwtIssuer"],
ValidAudience = Configuration["JwtAudience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JwtSecurityKey"]))
};
})
.AddCookie(options =>
{
options.Cookie.Name = "MySpecialCookie";
options.LoginPath = "/login";
//options.LogoutPath = "/Home/Index";
//options.AccessDeniedPath = "AccessDenied";
options.ExpireTimeSpan = TimeSpan.FromDays(30);
options.SlidingExpiration = true; // the cookie would be re-issued on any request half way through the ExpireTimeSpan
//options.Cookie.Expiration = TimeSpan.FromDays(5);
options.EventsType = typeof(CookieAuthEvent);
});
services.AddScoped<CookieAuthEvent>();
services.AddAuthorization(config =>
{
config.AddPolicy(Policies.IsAdmin, Policies.IsAdminPolicy());
config.AddPolicy(Policies.IsUser, Policies.IsUserPolicy());
});
services.ConfigureApplicationCookie(options =>
{
options.Cookie.HttpOnly = true;
options.Events.OnRedirectToLogin = context =>
{
context.Response.StatusCode = 401;
return Task.CompletedTask;
};
});
On Client side I am currently using AuthorizeApi with LocalStorage. This is working but I want to move this to Cookie.
AuthorizeApi.cs:
public async Task<LoginResult> Login(LoginModel loginModel)
{
//var stringContent = new StringContent(JsonSerializer.Serialize(LoginModel), Encoding.UTF8, "application/json");
HttpResponseMessage responseMessage = await this.HttpClient.PostAsJsonAsync("Authorize/Login", loginModel);
LoginResult result = await responseMessage.Content.ReadFromJsonAsync<LoginResult>();
if (result.Successful)
{
if (loginModel.RememberMe)
{
await this.LocalStorage.SetItemAsync("MySpecialToken", result.Token);
}
((ApiAuthenticationStateProvider)this.AuthenticationStateProvider).MarkUserAsAuthenticated(result.Token);
this.HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", result.Token);
return result;
}
return result;
}
ApiAuthenticationStateProvider.cs:
public void MarkUserAsAuthenticated(string token)
{
ClaimsPrincipal authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(ParseClaimsFromJwt(token), "jwt"));
Task<AuthenticationState> authState = Task.FromResult(new AuthenticationState(authenticatedUser));
NotifyAuthenticationStateChanged(authState);
}
On an older project I created custom authentication that used cookies to store the session identifier.
The process was simple. Start by adding a service to house the tokens for the current user session, that will probably be sent to your API, or alternatively update your injected HttpClient right after authentication or on your MainLayout once you've retrieved the value from the cookie:
if (!httpClient.DefaultRequestHeaders.Contains("SessionID"))
httpClient.DefaultRequestHeaders.Add("SessionID", await JSRuntime.InvokeAsync<string>("MyJs.Cookies.Get", "SessionID"));
Then on your MainLayout check if the cookie has a value before you navigate to authentication. To do this, I used the following JavaScript:
window.MyJs = {
Cookies: {
Set: function (name, value, date) {
var d = new Date(date);
var expires = "expires=" + d.toUTCString();
document.cookie = name + "=" + value + ";" + expires + ";path=/";
},
Get: function (name) {
name = name + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
},
Remove: function (name) {
RadixTrie.Cookies.Set(name, "", "01 Jan 1970 00:00:00 UTC");
}
}
}
Do the check in MainLayout like this:
protected override async Task OnAfterRenderAsync(bool firstRender) {
if (firstRender)
{
...
var sessionID = await JSRuntime.InvokeAsync<string>("MyJs.Cookies.Get", "SessionID");
if (string.IsNullOrWhiteSpace(sessionID))
NavigationManager.NavigateTo("Auth/Login", true);
...
}
}
Once you've authenticated, you can just set the cookie:
internal async Task Login() {
...
await JSRuntime.InvokeVoidAsync("MyJs.Cookies.Set", "SessionID", loginResponse.Token, loginResponse.Expires);
//{loginResponse.Token:string} {loginResponse.Expires:datetime}
...
}
EDIT:
I wasn't satisfied with this answer and took some time to consider alternatives. I don't have a fully coded solution, but a better way of implementing a token for authentication, and in your case to keep a session live, would be using Set-Cookie header in your responses from your API.
I suggest creating middleware to handle the reading and resetting of the token.
But let's start with the login. Once a user authenticated, you can update the response in your endpoint like:
[HttpPost]
public async Task<IActionResult> Login([FromBody] LoginModel login)
{
...
Response.Headers.Add("Set-Cookie", $"SessionID={Guid.NewGuid()}; Expires={DateTime.Now.AddMonths(1).ToString("dd MMM yyyy hh:mm:ss") + " UTC"}; HttpOnly"); //Valid for 1 month, HttpOnly
...
return Ok();
}
It would be a good idea to make the token and cookie string generation reusable at this point. Consider encryption too.
Thereafter, add middleware to your API and on each request read the cookie to get the token:
public async Task Invoke(HttpContext context)
{
...
context.Request.Cookies.TryGetValue("SessionID", out string sessionID);
...
await _next(context);
...
//Reset the token after each request for improved security
context.Response.Headers.Add("Set-Cookie", $"SessionID={Guid.NewGuid()}; Expires={DateTime.Now.AddMonths(1).ToString("dd MMM yyyy hh:mm:ss") + " UTC"}; HttpOnly"); //Valid for 1 month, HttpOnly
}
EDIT 2:
If a cookie is reset and there are asynchronous work being done using the API, some requests might fail if a request is made just after the cookie change before being updated. This is a very small window and should be low in chance. I will take some time in the future to test this theory and update this answer.
Basically, I can log in just fine in my Client and access all pages. If I log out, I can't access anything on my client, it always redirects to my login page. Directly accessing the api endpoint is another matter. Say I call mylocalapp.com/api/users, directly from the browser url bar, I can still get all data. So I added [Authorize]. Unexpectedly, I get a 401 if I do that, DESPITE being logged in. So I checked HttpContext.User and simply User.Identity and they're empty, despite successful authentication. ClaimsIdentity is also empty. Can you identify anything that I might have done wrong? According to what I found on Google, this IS how it should be done. Thanks for your help.
Login method:
Takes user data, checks if ok, creates a JWT token and gets browser cookies (if null - session expired) then appends the token to the browser. And then logs the user in via Sign In Manager. If ok, returns ok. Context var is just for debugging purposes.
[AllowAnonymous]
[HttpPost]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
async public Task<IActionResult> LoginPost(AuthUser authUser)
{
var user = _context.Users.FirstOrDefault(user => user.Email == authUser.Email);
if (authUser is null) return BadRequest(AppResources.NullUser);
if (user is null) return BadRequest(AppResources.UserBadCredentials);
else
{
var isPassword = _userManager.CheckPasswordAsync(user, authUser.Password);
if (!isPassword.Result) return BadRequest(AppResources.UserBadCredentials);
var token = _jwtHandlerAuth.Authentication(authUser);
if (token == null) return BadRequest(AppResources.UserAuthenticationImpossible);
string cookieValue = Request.Cookies["jwt"];
var returnUser = new ReturnUser
{
Email = user.Email,
Name = $"{user.FirstName} {user.LastName}",
UserName = user.UserName
};
if (cookieValue != token)
{
Response.Cookies.Append("jwt", token, new CookieOptions
{
HttpOnly = true,
IsEssential = true,
SameSite = SameSiteMode.None,
Secure = true
});
}
var signInResult = await _signInManager.PasswordSignInAsync(authUser.Email, authUser.Password, false, false);
if (!signInResult.Succeeded) return BadRequest(AppResources.UserAuthenticationImpossible);
var context = _signInManager.Context;
return Ok(returnUser);
}
}
My Authentication method (creates the token):
public string Authentication(AuthUser authUser)
{
var tokenHandler = new JwtSecurityTokenHandler();
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
new Claim("UserName", authUser.Email),
new Claim("Email", authUser.Email)
}),
Expires = DateTime.UtcNow.AddHours(2),
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(Encoding.UTF32.GetBytes(_privateKey)),
SecurityAlgorithms.HmacSha256Signature)
};
var claimsPrincipal = new ClaimsPrincipal(tokenDescriptor.Subject);
var token = tokenHandler.CreateToken(tokenDescriptor);
tokenHandler.WriteToken(token);
return tokenHandler.WriteToken(token);
}
And my Startup class services and middleware:
var builder = services.AddIdentityCore<User>();
var identityBuilder = new IdentityBuilder(builder.UserType, builder.Services);
identityBuilder.AddRoles<IdentityRole>();
identityBuilder.AddEntityFrameworkStores<IdentityContext>();
identityBuilder.AddSignInManager<SignInManager<User>>();
identityBuilder.AddDefaultTokenProviders();
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/build";
});
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
x.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddCookie("Identity.Application").AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF32.GetBytes(Configuration.GetSection("Jwt:PrivateKey").Value)),
ValidateIssuer = false,
ValidateAudience = false
};
});
services.AddSingleton<IJwtHandlerAuth>(new JwtHandlerAuth(Configuration.GetSection("Jwt:PrivateKey").Value));
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
I have implemented JWT tokens to know who the current user is in an API application that is being used by an MVC controller.
I'm building something like a forum app. The user must be logged in to be able to post, so I'm basically trying to use the JWT token to store the current user's email.
When the user click on "Create Post" the action should get the token and its value, the problem is that I don't know how to use the token to protect controllers or retrieve data from current user, I have already copied and pasted the token in jwt.io to check if the data is stored correctly in the token and the value (the user's email) is stored correctly.
The API controller with the "login" action:
public async Task<IActionResult> login([FromBody] Usuario model)
{
//check if user exists and the password is correct
//generates the token
var SecretKey = config.GetValue<string>("SecretKey");
var key = Encoding.ASCII.GetBytes(SecretKey);
var claims = new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.Mail)
});
claims.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Mail));
var tokenDesc = new SecurityTokenDescriptor
{
Subject = claims,
Expires = DateTime.UtcNow.AddMinutes(20),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var tokenHandler = new JwtSecurityTokenHandler();
var createdToken = tokenHandler.CreateToken(tokenDesc);
string bearer_token = tokenHandler.WriteToken(createdToken);
using(var client = new HttpClient())
{
client.DefaultRequestHeaders.Add("Authorization", "Bearer" + bearer_token);
}
return Ok(bearer_token);
}
}
The MVC controller from where the API is used:
public async Task<IActionResult> login(Usuario model)
{
HttpClient hc = new HttpClient();
hc.BaseAddress = new Uri("https://localhost:44325/api/Usuarios/");
var login = await hc.PostAsJsonAsync<Usuario>("login", model);
//check the response
var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
identity.AddClaim(new Claim(ClaimTypes.Name, model.Email));
var principal = new ClaimsPrincipal(identity);
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);
HttpContext.Session.SetString("JWToken", login.ToString());
hc.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", login.ToString());
return RedirectToAction("IndexForumList", "ForumControllerMVC");
}
}
This is the API method to "Create Posts" and where the token should be used, here the userId is null:
public async Task<IActionResult> createPost([FromForm]ForumModel model)
{
string userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
//retrieves the current user email, validates and save the content to database
}
And this is the MVC method to "Create Posts":
public async Task<IActionResult> createPost(ForumModel model)
{
HttpClient hc = new HttpClient();
hc.BaseAddress = new Uri("https://localhost:44325/api/Usuarios/");
//var userPost = hc.PostAsJsonAsync<ForumModel>("Usuarios/createPost", model);
var userPost = await hc.PostAsync("createPost", formContent);
if(userPost.IsSuccessStatusCode == true)
{
return RedirectToAction("IndexForumList", "ForoControllerMVC");
}
}
I have been suffering with this due to my lack of knowledge about JWT, any help is appreciated.
UPDATE
The startup.cs
public void ConfigureServices(IServiceCollection services)
{
var key = Encoding.ASCII.GetBytes(Configuration.GetValue<string>("SecretKey"));
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
services.AddSession(
options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(10);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
}
If I understand your question correctly,
To protect your API you can decorate it with [Authorize] attribute. Eg -
[Authorize]
[HttpGet]
public IActionResult GetAll()
{
var users = _userService.GetAll();
return Ok(users);
}
And to validate your tokens since you are using .netcore for your api, you ll have to create a middleware that will validate the token before your requests hit the API endpoint. You can follow this tutorial for more details on how to use JWT with ASP.NET core.
To get user Id in your case, you ll have to validate the token first and then extract the UserId. Try changing your code in createPost api to this -
public async Task<IActionResult> createPost([FromForm]ForumModel model)
{
var tokenHandler = new JwtSecurityTokenHandler();
var SecretKey = config.GetValue<string>("SecretKey");
var key = Encoding.ASCII.GetBytes(SecretKey);
var token = HttpContext.Request.Headers["Authorization"];
tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false,
ClockSkew = TimeSpan.Zero
}, out SecurityToken validatedToken);
var jwtToken = (JwtSecurityToken)validatedToken;
var userId = int.Parse(jwtToken.Claims.First(x => x.Type == "NameIdentifier").Value);
}
Although this should be handled in the middleware and you can then attach the authenticated user to the current HttpContext.Items collection to make it accessible within the scope of the current request. All this is explained in the tutorial in detail. Hope that helps.!
I'm currently having an issues where a given client that has a valid claim identity specifying that the client is in the given role, not get authorised for actions and controllers that require that role.
I'm using an API to generate the token which the client will use to access both the API and the website, the tokens are generated as JWT.
I have looked on other posts that have this issue, but almost all of the are using identity which isn't required for this project, since we are using JWT.
I am using HttpContext.Signin to sign in the user, passing it the token received from the API which contains the user's roles and other claims.
I have included the code that's dealing with tokens, and the token received from the API -at the very end-. As clearly seen, the user has the role attribute, yet they are will not be authorised by the authentication middleware.
Creating the token
public async Task<string> CreateBearerTokenAsync(User user, string audience)
{
// Create identity for the client
var claims = await _clientManager.CreateUserClaimsIdentityAsync(user.Id);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = claims,
Expires = DateTime.UtcNow.AddHours(2),
IssuedAt = DateTime.UtcNow,
Issuer = "Bikefy Api",
Audience = audience,
SigningCredentials = _encryptionService.TokenSignKey
};
// Create the token
var token = new JwtSecurityTokenHandler().CreateJwtSecurityToken(tokenDescriptor);
// Write the token to string.
return new JwtSecurityTokenHandler().WriteToken(token);
}
Claims identity
public async Task<ClaimsIdentity> CreateUserClaimsIdentityAsync(Guid clientId)
{
if (clientId == null || Guid.Empty == clientId)
throw new ArgumentNullException($"{nameof(clientId)}");
var user = await GetClientByIdAsync(clientId);
var id = new ClaimsIdentity("ApiKey", ClaimsIdentity.DefaultNameClaimType, ClaimsIdentity.DefaultRoleClaimType);
var secKey = await GetSecurityStampAsync(clientId);
// Add default claims
id.AddClaim(new Claim(ClaimTypes.NameIdentifier, clientId.ToString(), ClaimValueTypes.String));
if (user.FirstName != null)
id.AddClaim(new Claim(ClaimTypes.GivenName, user.FirstName, ClaimValueTypes.String));
if (user.LastName != null)
id.AddClaim(new Claim(ClaimTypes.Surname, user.LastName, ClaimValueTypes.String));
id.AddClaim(new Claim(IdentityProviderClaimType, "Bikey Identity", ClaimValueTypes.String));
id.AddClaim(new Claim(SecurityStampClaimType, secKey, ClaimValueTypes.String));
if (user.RoleName != null)
id.AddClaim(new Claim($"{nameof(User.RoleName)}", user.RoleName, ClaimValueTypes.String));
// Get roles
var roles = await GetRolesAsync(clientId);
foreach (var role in roles)
id.AddClaim(new Claim(ClaimTypes.Role, role));
// Add user claims
id.AddClaims(await GetClaimsAsync(clientId));
return id;
}
Cookie token configuration
public static void ConfigureAuthentication(this IServiceCollection services, IConfiguration configuration)
{
var secretKeys = new SecretKeys();
configuration.GetSection("SecretKeys").Bind(secretKeys);
var encryptService = new EncryptionService("00E7EB8C24190E2187", secretKeys);
services.AddSingleton(encryptService);
services.AddSingleton(secretKeys);
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
x.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
}).AddCookie(c =>
{
c.Cookie.Name = "CityCyles.Auth";
c.Cookie.HttpOnly = true;
c.Cookie.Expiration = TimeSpan.FromDays(1);
c.Cookie.SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.Always;
c.LoginPath = $"/Account/Login";
c.LogoutPath = $"/Account/Logout";
c.AccessDeniedPath = $"/Account/AccessDenied";
});
}
User Login
try
{
result = await _apiProvider.SendPostRequest(_apiProvider.BuildUrl("Auth", "Authenticate"), apiModel);
// Convert the string into a token
var token = new JwtSecurityTokenHandler().ReadJwtToken(result);
// Create cookie options
var authOptions = new AuthenticationProperties()
{
AllowRefresh = model.Remember,
ExpiresUtc = DateTime.UtcNow.AddHours(2),
IssuedUtc = DateTime.UtcNow,
IsPersistent = true
};
// Get the user claims from the user
var claimsIdentity = new ClaimsIdentity(token.Claims, CookieAuthenticationDefaults.AuthenticationScheme, ClaimTypes.Name, ClaimTypes.Role);
// Sign in the user
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity), authOptions);
// Redirect the user to the requested page or take them home
if (!string.IsNullOrEmpty(returnUrl))
return Redirect(returnUrl);
return RedirectToAction("Index", "Home");
}
catch (WebException ex)
{
_logger.LogWarning(LogEvents.HandlingLogin, ex, "Error authenticating user.");
//Display the error
ModelState.AddModelError("Custom-Error", ex.Message);
return View(model);
}
Controller Action
[HttpGet]
[Authorize(Roles = CityCyclesRoles.CityCyclesDocks)]
public async Task<IActionResult> OnBoard()
{
return View();
}
Token
{
"nameid": "3e637f01-85a9-4437-a113-50d2953d014e",
"given_name": "Stephanie",
"family_name": "Lee",
"http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider": "Bikey Identity",
"Bikeyfy.Identity.SecurityStamp": "1b6228cf-945a-4503-a64e-1dcb7b649c22",
"role": "CityCycles.Staff.Docks",
"nbf": 1552586386,
"exp": 1552593586,
"iat": 1552586386,
"iss": "CityCycles Api",
"aud": "CityCycles.Web"
}
I am using identity server 4 for authentication using grant type as 'ResourceOwnerPassword'. I am able to authenticate the user but not able to get claims related to user. So how can I get those ?
Below is my code
Client
Startup.cs
app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
{
Authority = "http://localhost:5000",
RequireHttpsMetadata = false,
ApiName = "api1"
});
Controller
public async Task<IActionResult> Authentication(LoginViewModel model)
{
var disco = await DiscoveryClient.GetAsync("http://localhost:5000");
// request token
var tokenClient = new TokenClient(disco.TokenEndpoint, "ro.client", "secret");
var tokenResponse = await tokenClient.RequestResourceOwnerPasswordAsync(model.Email, model.Password, "api1");
if (tokenResponse.IsError)
{
Console.WriteLine(tokenResponse.Error);
}
// Here I am not getting the claims, it is coming Forbidden
var extraClaims = new UserInfoClient(disco.UserInfoEndpoint);
var identityClaims = await extraClaims.GetAsync(tokenResponse.AccessToken);
if (!tokenResponse.IsError)
{
Console.WriteLine(identityClaims.Json);
}
Console.WriteLine(tokenResponse.Json);
Console.WriteLine("\n\n");
}
Server
Startup.cs
services.AddIdentityServer()
.AddTemporarySigningCredential()
.AddInMemoryPersistedGrants()
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients(Configuration))
.AddAspNetIdentity<ApplicationUser>()
.AddProfileService<IdentityProfileService>()
.AddResourceOwnerValidator<ResourceOwnerPasswordValidator>();
Config.cs
public static IEnumerable<Client> GetClients(IConfigurationRoot Configuration)
{
// client credentials client
return new List<Client>
{
// resource owner password grant client
new Client
{
ClientId = "ro.client",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AlwaysSendClientClaims = true,
AlwaysIncludeUserClaimsInIdToken = true,
AccessTokenType = AccessTokenType.Jwt
}
};
}
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("api1", "My API")
};
}
But when I check my access token in jwt.io there I can see the claims But why I am not able to get in the controller ?
Any help on this appreciated !
You can call the UserInfoEndpoint, as per your example, but you can also get additional claims if you define your ApiResource as requiring them.
For example, rather than just defining your ApiResource like you are:
new ApiResource("api1", "My API")
You can use the expanded format and define what UserClaims you'd like to have when getting an access token for this scope.
For example:
new ApiResource
{
Name = "api1",
ApiSecrets = { new Secret(*some secret*) },
UserClaims = {
JwtClaimTypes.Email,
JwtClaimTypes.PhoneNumber,
JwtClaimTypes.GivenName,
JwtClaimTypes.FamilyName,
JwtClaimTypes.PreferredUserName
},
Description = "My API",
DisplayName = "MyApi1",
Enabled = true,
Scopes = { new Scope("api1") }
}
Then in your own implementation of the IProfileService you will find that calls to GetProfileDataAsync have a list of what claims are requested in the context (ProfileDataRequestContext.RequestedClaimTypes). Given that list of what's been asked for, you can then add any claims you like - however you like - to the context.IssuedClaims that you return from that method. These will then be a part of the access token.
If you only want certain claims by specifically calling the UserInfo endpoint though, you'll want to create an IdentityResource definition and have that scope included as part of your original token request.
For example:
new IdentityResource
{
Name = "MyIdentityScope",
UserClaims = {
JwtClaimTypes.EmailVerified,
JwtClaimTypes.PhoneNumberVerified
}
}
But your first problem is following the other answer here so you don't get 'forbidden' as the response to the UserInfo endpoint!
Try sending the token along the request, when calling the UserInfoEndpoint. Try this:
var userInfoClient = new UserInfoClient(doc.UserInfoEndpoint, token);
var response = await userInfoClient.GetAsync();
var claims = response.Claims;
official docs