Blazor 5x - قسمت 25 - تهیه API مخصوص Blazor WASM - بخش 2 - تامین پایهی اعتبارسنجی و احراز هویت
نویسنده: وحید نصیری
تاریخ: ۱۴۰۰/۰۱/۰۲ ۱۲:۴۵
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
using Microsoft.AspNetCore.Identity;
namespace BlazorServer.Entities
{
public class ApplicationUser : IdentityUser
{
public string Name { get; set; }
}
} public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
namespace BlazorWasm.WebApi
{
public class Startup
{
// ...
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// ... dotnet tool update --global dotnet-ef --version 5.0.4 dotnet build dotnet ef migrations --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ add AddNameToAppUser --context ApplicationDbContext dotnet ef --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ database update --context ApplicationDbContext
using System.ComponentModel.DataAnnotations;
namespace BlazorServer.Models
{
public class UserRequestDTO
{
[Required(ErrorMessage = "Name is required")]
public string Name { get; set; }
[Required(ErrorMessage = "Email is required")]
[RegularExpression("^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$",
ErrorMessage = "Invalid email address")]
public string Email { get; set; }
public string PhoneNo { get; set; }
[Required(ErrorMessage = "Password is required.")]
[DataType(DataType.Password)]
public string Password { get; set; }
[Required(ErrorMessage = "Confirm password is required")]
[DataType(DataType.Password)]
[Compare("Password", ErrorMessage = "Password and confirm password is not matched")]
public string ConfirmPassword { get; set; }
}
} public class RegistrationResponseDTO
{
public bool IsRegistrationSuccessful { get; set; }
public IEnumerable<string> Errors { get; set; }
} using System;
using BlazorServer.Entities;
using BlazorServer.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
using System.Linq;
using BlazorServer.Common;
namespace BlazorWasm.WebApi.Controllers
{
[Route("api/[controller]/[action]")]
[ApiController]
[Authorize]
public class AccountController : ControllerBase
{
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly UserManager<ApplicationUser> _userManager;
private readonly RoleManager<IdentityRole> _roleManager;
public AccountController(SignInManager<ApplicationUser> signInManager,
UserManager<ApplicationUser> userManager,
RoleManager<IdentityRole> roleManager)
{
_roleManager = roleManager ?? throw new ArgumentNullException(nameof(roleManager));
_userManager = userManager ?? throw new ArgumentNullException(nameof(userManager));
_signInManager = signInManager ?? throw new ArgumentNullException(nameof(signInManager));
}
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> SignUp([FromBody] UserRequestDTO userRequestDTO)
{
var user = new ApplicationUser
{
UserName = userRequestDTO.Email,
Email = userRequestDTO.Email,
Name = userRequestDTO.Name,
PhoneNumber = userRequestDTO.PhoneNo,
EmailConfirmed = true
};
var result = await _userManager.CreateAsync(user, userRequestDTO.Password);
if (!result.Succeeded)
{
var errors = result.Errors.Select(e => e.Description);
return BadRequest(new RegistationResponseDTO { Errors = errors, IsRegistrationSuccessful = false });
}
var roleResult = await _userManager.AddToRoleAsync(user, ConstantRoles.Customer);
if (!roleResult.Succeeded)
{
var errors = result.Errors.Select(e => e.Description);
return BadRequest(new RegistationResponseDTO { Errors = errors, IsRegistrationSuccessful = false });
}
return StatusCode(201); // Created
}
}
} [Route("api/[controller]/[action]")]
[ApiController]
[Authorize]
using System.ComponentModel.DataAnnotations;
namespace BlazorServer.Models
{
public class AuthenticationDTO
{
[Required(ErrorMessage = "UserName is required")]
[RegularExpression("^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$",
ErrorMessage = "Invalid email address")]
public string UserName { get; set; }
[Required(ErrorMessage = "Password is required.")]
[DataType(DataType.Password)]
public string Password { get; set; }
}
} using System.Collections.Generic;
namespace BlazorServer.Models
{
public class AuthenticationResponseDTO
{
public bool IsAuthSuccessful { get; set; }
public string ErrorMessage { get; set; }
public string Token { get; set; }
public UserDTO UserDTO { get; set; }
}
public class UserDTO
{
public string Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string PhoneNo { get; set; }
}
} namespace BlazorServer.Models
{
public class BearerTokensOptions
{
public string Key { set; get; }
public string Issuer { set; get; }
public string Audience { set; get; }
public int AccessTokenExpirationMinutes { set; get; }
}
} {
"BearerTokens": {
"Key": "This is my shared key, not so secret, secret!",
"Issuer": "https://localhost:5001/",
"Audience": "Any",
"AccessTokenExpirationMinutes": 20
}
} namespace BlazorWasm.WebApi
{
public class Startup
{
// ...
public void ConfigureServices(IServiceCollection services)
{
services.AddOptions<BearerTokensOptions>().Bind(Configuration.GetSection("BearerTokens"));
// ... using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using BlazorServer.Entities;
using BlazorServer.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
namespace BlazorServer.Services
{
public interface ITokenFactoryService
{
Task<string> CreateJwtTokensAsync(ApplicationUser user);
}
public class TokenFactoryService : ITokenFactoryService
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly BearerTokensOptions _configuration;
public TokenFactoryService(
UserManager<ApplicationUser> userManager,
IOptionsSnapshot<BearerTokensOptions> bearerTokensOptions)
{
_userManager = userManager ?? throw new ArgumentNullException(nameof(userManager));
if (bearerTokensOptions is null)
{
throw new ArgumentNullException(nameof(bearerTokensOptions));
}
_configuration = bearerTokensOptions.Value;
}
public async Task<string> CreateJwtTokensAsync(ApplicationUser user)
{
var signingCredentials = new SigningCredentials(
new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration.Key)),
SecurityAlgorithms.HmacSha256);
var claims = await getClaimsAsync(user);
var now = DateTime.UtcNow;
var tokenOptions = new JwtSecurityToken(
issuer: _configuration.Issuer,
audience: _configuration.Audience,
claims: claims,
notBefore: now,
expires: now.AddMinutes(_configuration.AccessTokenExpirationMinutes),
signingCredentials: signingCredentials);
return new JwtSecurityTokenHandler().WriteToken(tokenOptions);
}
private async Task<List<Claim>> getClaimsAsync(ApplicationUser user)
{
string issuer = _configuration.Issuer;
var claims = new List<Claim>
{
// Issuer
new Claim(JwtRegisteredClaimNames.Iss, issuer, ClaimValueTypes.String, issuer),
// Issued at
new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64, issuer),
new Claim(ClaimTypes.Name, user.Email, ClaimValueTypes.String, issuer),
new Claim(ClaimTypes.Email, user.Email, ClaimValueTypes.String, issuer),
new Claim("Id", user.Id, ClaimValueTypes.String, issuer),
new Claim("DisplayName", user.Name, ClaimValueTypes.String, issuer),
};
var roles = await _userManager.GetRolesAsync(user);
foreach (var role in roles)
{
claims.Add(new Claim(ClaimTypes.Role, role, ClaimValueTypes.String, issuer));
}
return claims;
}
}
} namespace BlazorWasm.WebApi
{
public class Startup
{
// ...
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<ITokenFactoryService, TokenFactoryService>();
// ... namespace BlazorWasm.WebApi.Controllers
{
[Route("api/[controller]/[action]")]
[ApiController]
[Authorize]
public class AccountController : ControllerBase
{
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly UserManager<ApplicationUser> _userManager;
private readonly ITokenFactoryService _tokenFactoryService;
public AccountController(
SignInManager<ApplicationUser> signInManager,
UserManager<ApplicationUser> userManager,
ITokenFactoryService tokenFactoryService)
{
_userManager = userManager ?? throw new ArgumentNullException(nameof(userManager));
_signInManager = signInManager ?? throw new ArgumentNullException(nameof(signInManager));
_tokenFactoryService = tokenFactoryService;
}
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> SignIn([FromBody] AuthenticationDTO authenticationDTO)
{
var result = await _signInManager.PasswordSignInAsync(
authenticationDTO.UserName, authenticationDTO.Password,
isPersistent: false, lockoutOnFailure: false);
if (!result.Succeeded)
{
return Unauthorized(new AuthenticationResponseDTO
{
IsAuthSuccessful = false,
ErrorMessage = "Invalid Authentication"
});
}
var user = await _userManager.FindByNameAsync(authenticationDTO.UserName);
if (user == null)
{
return Unauthorized(new AuthenticationResponseDTO
{
IsAuthSuccessful = false,
ErrorMessage = "Invalid Authentication"
});
}
var token = await _tokenFactoryService.CreateJwtTokensAsync(user);
return Ok(new AuthenticationResponseDTO
{
IsAuthSuccessful = true,
Token = token,
UserDTO = new UserDTO
{
Name = user.Name,
Id = user.Id,
Email = user.Email,
PhoneNo = user.PhoneNumber
}
});
}
}
}
{
"iss": "https://localhost:5001/",
"iat": 1616396383,
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "vahid@dntips.ir",
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress": "vahid@dntips.ir",
"Id": "582855fb-e95b-45ab-b349-5e9f7de40c0c",
"DisplayName": "vahid@dntips.ir",
"http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Admin",
"nbf": 1616396383,
"exp": 1616397583,
"aud": "Any"
} namespace BlazorWasm.WebApi
{
public class Startup
{
// ...
public void ConfigureServices(IServiceCollection services)
{
var bearerTokensSection = Configuration.GetSection("BearerTokens");
services.AddOptions<BearerTokensOptions>().Bind(bearerTokensSection);
// ...
var apiSettings = bearerTokensSection.Get<BearerTokensOptions>();
var key = Encoding.UTF8.GetBytes(apiSettings.Key);
services.AddAuthentication(opt =>
{
opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
opt.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(cfg =>
{
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateAudience = true,
ValidateIssuer = true,
ValidAudience = apiSettings.Audience,
ValidIssuer = apiSettings.Issuer,
ClockSkew = TimeSpan.Zero,
ValidateLifetime = true
};
});
// ... namespace BlazorWasm.WebApi
{
public class Startup
{
// ...
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "BlazorWasm.WebApi", Version = "v1" });
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Description = "Please enter the token in the field",
Name = "Authorization",
Type = SecuritySchemeType.ApiKey
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement {
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
new string[] { }
}
});
});
}
// ... Individual به همراه ساختن پروژه Host استفاده بشه تمامی پیجهای Identity در بخش Host قرار میگیره.