How to get the id_token in blazor web assembly - asp.net-core

I have got a Blazor WebAssembly (latest 3.2.0) app with oidc Authentication.
The asp.net authentication provides a way to get the accessToken but can't see any means to access the id_token (jwt) which is required for my scenario.
I can see the id_token in the local storage of the browser.
What would be best way to access it?
Thanks

You can read it from the session storage using JSInterop, it is stored at key oidc.user:{app baseUri}:{app client id} :
#inject IJSRuntime JSRuntime
#inject NavigationManager NavigationManager
...
#code {
private async Task<string> ReadIdToken()
{
const string clientId = "your oidc client id";
var userDataKey = $"oidc.user:{NavigationManager.BaseUri}:{clientId}";
var userData = await JSRuntime.InvokeAsync<UserData>("sessionStorage.getItem", userDataKey);
return userData.id_token;
}
class UserData
{
public string id_token { get; set; }
public int expires_at { get; set; }
}
}

Here's a working code sample that allows you to get the id_token in raw format as well as a list of claims parsed from it.
Note: You should authenticate before you can see the results...
#page "/"
#inject IJSRuntime JSRuntime
#inject NavigationManager NavigationManager
#using System.Security.Claims
#using System.Text.Json
<p>#JwtToken</p>
#foreach (var claim in claims)
{
<p>#claim</p>
}
#code {
List<Claim> claims = new List<Claim>();
string JwtToken;
protected override async Task OnInitializedAsync()
{
await GetJwtToken();
}
private async Task GetJwtToken()
{
var baseUri = NavigationManager.BaseUri.Substring(0,
NavigationManager.BaseUri.Length - 1);
// client id example: RoleBasedApiAuthorization.Client
const string clientID = "<Place here your client id>";
var key = $"oidc.user:{baseUri}:{clientID}";
JwtToken = await JSRuntime.InvokeAsync<string>
("sessionStorage.getItem", key);
if (JwtToken != null)
{
claims = ParseClaimsFromJwt(JwtToken).ToList();
}
}
public IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
{
var payload = jwt.Split('.')[1];
var jsonBytes = ParseBase64WithoutPadding(payload);
var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);
return keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString()));
}
private byte[] ParseBase64WithoutPadding(string base64)
{
switch (base64.Length % 4)
{
case 2: base64 += "=="; break;
case 3: base64 += "="; break;
}
return Convert.FromBase64String(base64);
}
}

Thank you guys SO much for this - I've been banging my head against this for a week (doh - forgot to look at the browser session data in Chrome to think about using JRRuntime...).
I'm not sure if this is Cognito-specific, but the key for me is not using the NavigationManager BaseUri, but the OIDC Authority.
#page "/"
#using System.Text.Json
#inject IJSRuntime JSRuntime
<AuthorizeView>
<Authorized>
<div>
<b>CachedAuthSettings</b>
<pre>
#JsonSerializer.Serialize(authSettings, indented);
</pre>
<br/>
<b>CognitoUser</b><br/>
<pre>
#JsonSerializer.Serialize(user, indented);
</pre>
</div>
</Authorized>
<NotAuthorized>
<div class="alert alert-warning" role="alert">
Everything requires you to Log In first.
</div>
</NotAuthorized>
</AuthorizeView>
#code {
JsonSerializerOptions indented = new JsonSerializerOptions() { WriteIndented = true };
CachedAuthSettings authSettings;
CognitoUser user;
protected override async Task OnInitializedAsync()
{
string key = "Microsoft.AspNetCore.Components.WebAssembly.Authentication.CachedAuthSettings";
string authSettingsRAW = await JSRuntime.InvokeAsync<string>("sessionStorage.getItem", key);
authSettings = JsonSerializer.Deserialize<CachedAuthSettings>(authSettingsRAW);
string userRAW = await JSRuntime.InvokeAsync<string>("sessionStorage.getItem", authSettings?.OIDCUserKey);
user = JsonSerializer.Deserialize<CognitoUser>(userRAW);
}
public class CachedAuthSettings
{
public string authority { get; set; }
public string metadataUrl { get; set; }
public string client_id { get; set; }
public string[] defaultScopes { get; set; }
public string redirect_uri { get; set; }
public string post_logout_redirect_uri { get; set; }
public string response_type { get; set; }
public string response_mode { get; set; }
public string scope { get; set; }
public string OIDCUserKey => $"oidc.user:{authority}:{client_id}";
}
public class CognitoUser
{
public string id_token { get; set; }
public string access_token { get; set; }
public string refresh_token { get; set; }
public string token_type { get; set; }
public string scope { get; set; }
public int expires_at { get; set; }
}
}
I get serialization errors if I directly try and convert the string to classes using JSRuntme.InvokeAsync but it works fine with the JsonSerializer, that's why you see that seemingly extra step there.

Related

How to implement JWT authentication from ASP.NET Core Web API to Vue.js SPA?

I made Vue.js single page application for using almost 10-15 API's which are written in ASP.NET Core Web API by me. I would like to use JWT authentication with this project. However I don't have any idea about how should I implement JWT authentication.
On backend side I store token, passwordHash and passwordSalt in User.cs model (I mean, I store in database). Then I created JWT token in controller which does register and login operations. After that I did some configuring in program.cs and tried authentication by Swagger, it works! Actually on backend side everything is okay.
public class User
{
[Key]
public int Id { get; set; }
public string? Name { get; set; }
public string? Surname { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public byte[] PasswordHash { get; set; }
public byte[] PasswordSalt { get; set; }
[DataType(DataType.Date)]
public DateTime BirthDate { get; set; }
public string? Gender { get; set; }
public string? Token { get; set; }
[DataType(DataType.DateTime)]
public DateTime CreatingDate { get; set; }
public string? BioText { get; set; }
public ICollection<Friendship> Friendships { get; set; }
public ICollection<Content> Contents { get; set; }
}
[ApiController]
public class AuthController : ControllerBase
{
private IUserService _service;
private IMapper _mapper;
private readonly IConfiguration _configuration;
public AuthController(IMapper mapper, IConfiguration configuration)
{
_service = new UserManager();
_mapper = mapper;
_configuration = configuration;
}
[HttpPost("register")]
public async Task<ActionResult<User>> Register(UserAuthDto request)
{
CreatePasswordHash(request.Password, out byte[] passwordHash, out byte[] passwordSalt);
User user = _mapper.Map<User>(request);
user.Username = request.Username;
user.PasswordHash= passwordHash;
user.PasswordSalt = passwordSalt;
_service.CreateUser(user);
return Ok(user);
}
[HttpPost("login")]
public async Task<ActionResult<bool>> Login(UserAuthDto request)
{
User user = _service.GetAllUsers()
.Where(x => x.Username == request.Username)
.FirstOrDefault();
if (user == null)
{
return BadRequest("User not found.");
}
else
{
if(!VerifyPasswordHash(request.Password, user.PasswordSalt, user.PasswordHash))
{
return BadRequest("Wrong Password.");
}
else
{
string token = CreateToken(user);
user.Token = token;
return Ok(token);
}
}
}
private void CreatePasswordHash(string password, out byte[] passwordHash, out byte[] passwordSalt)
{
using(var hmac = new HMACSHA512())
{
passwordSalt = hmac.Key;
passwordHash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password));
}
}
private bool VerifyPasswordHash(string password, byte[] passwordSalt, byte[] passwordHash)
{
using(var hmac = new HMACSHA512(passwordSalt))
{
var computedHash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes((string)password));
return computedHash.SequenceEqual(passwordHash);
}
}
private string CreateToken(User user)
{
List<Claim> claims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.Username),
};
var key = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(_configuration.GetSection("AppSettings:Token").Value));
var cred = new SigningCredentials(key, SecurityAlgorithms.HmacSha512Signature);
var token = new JwtSecurityToken(
claims: claims,
expires: DateTime.Now.AddDays(1),
signingCredentials: cred);
var jwt = new JwtSecurityTokenHandler().WriteToken(token);
return jwt;
}
}
The problem is I don't know how can I use token on frontend side. I mean should I use it in body of every requests or doesn't backend need anymore token? I am open to ideas I need your help, thank you.

Invalid object name 'RefreshToken

i am trying to make a login system with jwt and refresh token using sql as db and asp.net core as frontend.
and i get below error. in data base i have made user table but not RefreshToken table as RefreshToken is going to generated and saved on cookie of the browser.
Invalid object name 'RefreshToken
here is my Model User
public class User
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Username { get; set; }
[JsonIgnore]
public string Password { get; set; }
[JsonIgnore]
public List<RefreshToken> RefreshTokens { get; set; }
}
here is another model RefreshToken
using Microsoft.EntityFrameworkCore;
using System;
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
namespace Api_R.Entities
{
[Owned]
public class RefreshToken
{
[Key]
[JsonIgnore]
public int Id { get; set; }
public string Token { get; set; }
public DateTime Expires { get; set; }
/////////
}
}
and here i am using my controller
public AuthenticateResponse Authenticate(AuthenticateRequest model, string ipAddress)
{
var user = _apiDbContext.User.SingleOrDefault(x => x.Username == model.Username && x.Password==model.Password);
// validate
if (user == null)//|| !BCryptNet.Verify(model.Password, user.PasswordHash))
throw new AppException("Username or password is incorrect");
// authentication successful so generate jwt and refresh tokens
var jwtToken = _jwtUtils.GenerateJwtToken(user);
var refreshToken = _jwtUtils.GenerateRefreshToken(ipAddress);
user.RefreshTokens.Add(refreshToken);
// remove old refresh tokens from user
removeOldRefreshTokens(user);
return new AuthenticateResponse(user, jwtToken, refreshToken.Token);
}
and now i am getting error invalid object name refresh token

Why i'm getting an empty array in result of httpget?

I have StatsUserModel ( code below )
StatsUserModel.cs
namespace WebAPI.Models
{
public class StatsUserModel
{
public int DeviceID { get; set; }
public int bloodpress_sys { get; set; }
public int bloodpress_dia { get; set; }
public int saturation { get; set; }
public int BPM { get; set; }
public int veinsdiameter { get; set; }
public string Id { get; set; }
}
}
And i have Users from AspNetUsers
AspNetUsers and Stats structure
And i have AuthenticationContext.cs
namespace WebAPI.Models
{
public class AuthenticationContext : IdentityDbContext
{
public AuthenticationContext(DbContextOptions options):base(options)
{
}
public DbSet<ApplicationUser> ApplicationUsers { get; set; }
public DbSet<StatsUserModel> statsUserModels { get; set; }
}
}
So, I have created StatsController and HttpGet method
StatsController.cs
namespace WebAPI.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class StatsController : ControllerBase
{
private UserManager<ApplicationUser> _userManager;
private AuthenticationContext context;
public StatsController(AuthenticationContext _context, UserManager<ApplicationUser> userManager)
{
context = _context;
_userManager = userManager;
}
[HttpGet]
[Authorize]
public async Task<Object> GetStats(LoginModel model)
{
string userId = User.Claims.First(c => c.Type == "UserID").Value;
var user = await _userManager.FindByIdAsync(userId);
var data = context.statsUserModels.Where(s => s.Id == user.Id);
return data;
}
}
}
Generating a JWT
[HttpPost]
[Route("Login")]
public async Task<IActionResult> Login(LoginModel loginModel)
{
var user = await _userManager.FindByNameAsync(loginModel.UserName);
if(user != null && await _userManager.CheckPasswordAsync(user, loginModel.Password))
{
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
new Claim("UserID", user.Id.ToString())
}),
Expires = DateTime.Now.AddDays(1),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_applicationSettings.JWT_Secret)), SecurityAlgorithms.HmacSha256Signature)
};
var tokenHandler = new JwtSecurityTokenHandler();
var securityToken = tokenHandler.CreateToken(tokenDescriptor);
var token = tokenHandler.WriteToken(securityToken);
return Ok(new { token });
}
else
{
return BadRequest(new { message = "Username or password invalid" });
}
}
I use Postman to test login and it's returns me a JWT token and everything works fine, but when i'm passing that login name and password, it returns me an empty array with 200OK code
Fixed, i have made Add-Migration and filled the table once more time and everything worked, Thanks Rena !

getting 400 error on webapi call blazorserver

i am trying to setup a blazor server app, calling a webapi.
I keep getting a 400 error returned, when I call the API.
I have 3 Projects, projectserver and projectapi. projectserver is where the Blazor app sits and Project API is where the API sits.
I don't know if the apicall can find the API as it does not hit any breakpoints in the API section, I am totally confused, as if it cannot find the API then it should return a 404 or other error and not 400 ?
thank you for your efforts.
this is my code,
Projectserver, this is where I post the Register Model to the API
public string message { get; set; }
public RegisterModel r = new RegisterModel();
private async Task Create(MouseEventArgs e)
{
var json = Newtonsoft.Json.JsonConvert.SerializeObject(r);
var client = clientfactory.CreateClient("ServerApi");
var result = await client.PostAsJsonAsync("/Account/Register",json); // check the Startup file and check base address for the Full route.
message = result.StatusCode.ToString();
}
}
the ClientFactory returns the base address of what is defined in startup.cs
services.AddHttpClient("ServerApi", client => client.BaseAddress = new Uri("https://localhost:44302/"));
the API is Projectserver and defined as follows.
[Route("[controller]")]
[ApiController]
public class AccountContoller : ControllerBase
{
private readonly ApplicationDbContext _context;
private readonly SecurityOptions _securityOptions;
private readonly JwtIssuerOptions _jwtOptions;
// GET: api/<Account>
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
// GET api/<Account>/5
[HttpGet("{id}")]
public string Get(int id)
{
return "value";
}
// POST api/<Account>
[HttpPost]
public void Post([FromBody] string value)
{
}
// POST api/<Account>
[HttpPost("Register")]
public async Task<ActionResult<RegisterResult>> Register(RegisterModel model)
{
RegisterResult r = new RegisterResult();
var Exisits = await _context.Users.Where(r => r.EmailAddress == model.Email).FirstOrDefaultAsync();
if(Exisits != null)
{
r.Sucsess = false;
r.ErrorMessage = "Email - Already Exisits";
return r;
}
else
{
try
{
User newuser = new User();
newuser.CreatedDateTime = DateTime.UtcNow;
newuser.UserID = Guid.NewGuid();
newuser.MobileNumber = model.MobileNumber;
newuser.Password = model.Password;
newuser.FirstName = model.FirstName;
newuser.Surname = model.LastName;
_context.Users.Add(newuser);
await _context.SaveChangesAsync();
r.Sucsess = true;
return r;
}
catch(Exception e)
{
r.Sucsess = false;
r.ErrorMessage = e.ToString();
return r;
}
}
}
the Model classes are defined as Serializable
[Serializable]
public class RegisterResult
{
public bool Sucsess { get; set; }
public string ErrorMessage { get; set; }
}
[Serializable]
public class RegisterModel
{
public string UserName { get; set; }
public string Password { get; set; }
public string Email { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string RoleID { get; set; }
public string EntityID { get; set; }
public string MobileNumber { get; set; }
}
Can you please modify your code as below and give it a try:-
var serializedBody = JsonConvert.SerializeObject(r);
var jsonRequestBodyContent = new StringContent(serializedBody, Encoding.UTF8,"application/json");
var client = clientfactory.CreateClient("ServerApi");
var result = await client.PostAsync("/Account/Register",jsonRequestBodyContent);

MediatR 3.0.1 possible bug? Cannot get IAsyncRequestHandler working

I am getting the following error message when executing IRequest with IAsyncRequestHandler.
System.InvalidOperationException: 'No service for type 'MediatR.IRequestHandler`2[TestProject.Domain.Requests.Users.CreateUserRequest,TestProject.Domain.Requests.Users.CreateUserResponse]' has been registered.'
This is how i register it in the startup class
// Add framework services.
services.AddMvc();
services.AddMediatR(typeof(CreateUserRequest).GetTypeInfo().Assembly);
CreateUserRequest and Response
public class CreateUserRequest : IRequest<CreateUserResponse>
{
public string EmailAddress { get; set; }
public int OrganisationId { get; set; }
public string Password { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
public class CreateUserResponse
{
public int UserId { get; set; }
public string EmailAddress { get; set; }
}
Request handler
public class CreateUserRequestHandler : IAsyncRequestHandler<CreateUserRequest, CreateUserResponse>
{
private readonly UserManager<User> _userManager;
public CreateUserRequestHandler()
{
}
public async Task<CreateUserResponse> Handle(CreateUserRequest request)
{
//create the user and assign it to the organisation
var user = new User
{
Email = request.EmailAddress,
OrganisationUsers = new List<OrganisationUser> { new OrganisationUser { OrganisationId = request.OrganisationId } }
};
//create new user with password.
await _userManager.CreateAsync(user, request.Password);
//create response.
var response = new CreateUserResponse{UserId = user.Id, EmailAddress = user.Email};
return response;
}
}
Controller class
public class UserController : Controller
{
private readonly IMediator _mediator;
public UserController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
public async Task<CreateUserResponse> Post(CreateUserRequest request)
{
return await _mediator.Send(request);
}
}
the error occurs inside the controller class it does not hit the async request handler.
Is there anything wrong with the DI registration? I have looked at the examples but could not find anything specific to aspnet core.