Blazor 5x - قسمت 31 - احراز هویت و اعتبارسنجی کاربران Blazor WASM - بخش 1 - انجام تنظیمات اولیه
نویسنده: وحید نصیری
تاریخ: ۱۴۰۰/۰۱/۰۹ ۱۵:۱۰
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
<Authorizing>
<p>Please wait, we are authorizing the user.</p>
</Authorizing>
<NotAuthorized>
<p>Not Authorized</p>
</NotAuthorized>
</AuthorizeRouteView>
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState> {
"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"
} using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text.Json;
namespace BlazorWasm.Client.Utils
{
/// <summary>
/// From the Steve Sanderson’s Mission Control project:
/// https://github.com/SteveSandersonMS/presentation-2019-06-NDCOslo/blob/master/demos/MissionControl/MissionControl.Client/Util/ServiceExtensions.cs
/// </summary>
public static class JwtParser
{
public static IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
{
var claims = new List<Claim>();
var payload = jwt.Split('.')[1];
var jsonBytes = ParseBase64WithoutPadding(payload);
var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);
claims.AddRange(keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString())));
return claims;
}
private static byte[] ParseBase64WithoutPadding(string base64)
{
switch (base64.Length % 4)
{
case 2: base64 += "=="; break;
case 3: base64 += "="; break;
}
return Convert.FromBase64String(base64);
}
}
} <Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="5.0.4" />
</ItemGroup>
</Project> namespace BlazorWasm.Client.Services
{
public class AuthStateProvider : AuthenticationStateProvider
{
private readonly HttpClient _httpClient;
private readonly ILocalStorageService _localStorage;
public AuthStateProvider(HttpClient httpClient, ILocalStorageService localStorage)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_localStorage = localStorage ?? throw new ArgumentNullException(nameof(localStorage));
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var token = await _localStorage.GetItemAsync<string>(ConstantKeys.LocalToken);
if (token == null)
{
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
}
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", token);
return new AuthenticationState(
new ClaimsPrincipal(
new ClaimsIdentity(JwtParser.ParseClaimsFromJwt(token), "jwtAuthType")
)
);
}
}
} namespace BlazorServer.Common
{
public static class ConstantKeys
{
// ...
public const string LocalToken = "JWT Token";
}
} namespace BlazorWasm.Client
{
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
// ...
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<AuthenticationStateProvider, AuthStateProvider>();
// ...
}
}
} @using Microsoft.AspNetCore.Components.Authorization
namespace BlazorWasm.Client.Services
{
public interface IClientAuthenticationService
{
Task<AuthenticationResponseDTO> LoginAsync(AuthenticationDTO userFromAuthentication);
Task LogoutAsync();
Task<RegisterationResponseDTO> RegisterUserAsync(UserRequestDTO userForRegisteration);
}
} namespace BlazorWasm.Client.Services
{
public class ClientAuthenticationService : IClientAuthenticationService
{
private readonly HttpClient _client;
private readonly ILocalStorageService _localStorage;
public ClientAuthenticationService(HttpClient client, ILocalStorageService localStorage)
{
_client = client;
_localStorage = localStorage;
}
public async Task<AuthenticationResponseDTO> LoginAsync(AuthenticationDTO userFromAuthentication)
{
var response = await _client.PostAsJsonAsync("api/account/signin", userFromAuthentication);
var responseContent = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<AuthenticationResponseDTO>(responseContent);
if (response.IsSuccessStatusCode)
{
await _localStorage.SetItemAsync(ConstantKeys.LocalToken, result.Token);
await _localStorage.SetItemAsync(ConstantKeys.LocalUserDetails, result.UserDTO);
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", result.Token);
return new AuthenticationResponseDTO { IsAuthSuccessful = true };
}
else
{
return result;
}
}
public async Task LogoutAsync()
{
await _localStorage.RemoveItemAsync(ConstantKeys.LocalToken);
await _localStorage.RemoveItemAsync(ConstantKeys.LocalUserDetails);
_client.DefaultRequestHeaders.Authorization = null;
}
public async Task<RegisterationResponseDTO> RegisterUserAsync(UserRequestDTO userForRegisteration)
{
var response = await _client.PostAsJsonAsync("api/account/signup", userForRegisteration);
var responseContent = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<RegisterationResponseDTO>(responseContent);
if (response.IsSuccessStatusCode)
{
return new RegisterationResponseDTO { IsRegisterationSuccessful = true };
}
else
{
return result;
}
}
}
} namespace BlazorWasm.Client
{
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
// ...
builder.Services.AddScoped<IClientAuthenticationService, ClientAuthenticationService>();
// ...
}
}
} Blazor WebAssembly apps don't currently have a concept of DI scopes. Scoped-registered services behave like Singleton services.
public static IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
{
var claims = new List<Claim>();
var payload = jwt.Split('.')[1];
var jsonBytes = ParseBase64WithoutPadding(payload);
var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);
claims.AddRange(keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString())));
ExtractRolesFromJwt(claims, keyValuePairs);
return claims;
} private static void ExtractRolesFromJwt(List<Claim> claims, Dictionary<string, object> keyValuePairs)
{
keyValuePairs.TryGetValue(ClaimTypes.Role, out object roles);
if (roles != null)
{
var parsedRoles = roles.ToString().Trim().TrimStart('[').TrimEnd(']').Split(',');
if (parsedRoles.Length > 1)
{
claims.AddRange(parsedRoles.Select(parsedRole => new Claim(ClaimTypes.Role, parsedRole.Trim('"'))));
}
else
{
claims.Add(new Claim(ClaimTypes.Role, parsedRoles[0]));
}
keyValuePairs.Remove(ClaimTypes.Role);
}
} {
"iss": "https://localhost:5001/",
"iat": 1617386360,
"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": "14fc329a-6198-4b0d-958d-daa9f07707ec",
"DisplayName": "vahid@dntips.ir",
"http://schemas.microsoft.com/ws/2008/06/identity/claims/role": [
"Admin",
"Employee"
],
"nbf": 1617386360,
"exp": 1617387560,
"aud": "Any"
} public static class JwtParser
{
public static IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
{
var claims = new List<Claim>();
var payload = jwt.Split('.')[1];
var jsonBytes = ParseBase64WithoutPadding(payload);
var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);
foreach(var item in keyValuePairs)
{
if (item.Value is JsonElement element && element.ValueKind == JsonValueKind.Array)
{
foreach(var itemValue in element.EnumerateArray())
{
claims.Add(new Claim(item.Key, itemValue.ToString()));
}
}
else
{
claims.Add(new Claim(item.Key, item.Value.ToString()));
}
}
return claims;
} using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text.Json;
namespace BlazorWasm.Client.Utils
{
public class JwtInfo
{
public IEnumerable<Claim> Claims { set; get; }
public DateTime? ExpirationDateUtc { set; get; }
public bool IsExpired { set; get; }
public IEnumerable<string> Roles { set; get; }
}
/// <summary>
/// From the Steve Sanderson’s Mission Control project:
/// https://github.com/SteveSandersonMS/presentation-2019-06-NDCOslo/blob/master/demos/MissionControl/MissionControl.Client/Util/ServiceExtensions.cs
/// </summary>
public static class JwtParser
{
public static JwtInfo ParseClaimsFromJwt(string jwt)
{
var claims = new List<Claim>();
var payload = jwt.Split('.')[1];
var jsonBytes = getBase64WithoutPadding(payload);
foreach (var keyValue in JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes))
{
if (keyValue.Value is JsonElement element && element.ValueKind == JsonValueKind.Array)
{
foreach (var itemValue in element.EnumerateArray())
{
claims.Add(new Claim(keyValue.Key, itemValue.ToString()));
}
}
else
{
claims.Add(new Claim(keyValue.Key, keyValue.Value.ToString()));
}
}
var roles = getRoles(claims);
var expirationDateUtc = getDateUtc(claims, "exp");
var isExpired = getIsExpired(expirationDateUtc);
return new JwtInfo
{
Claims = claims,
Roles = roles,
ExpirationDateUtc = expirationDateUtc,
IsExpired = isExpired
};
}
private static IList<string> getRoles(IList<Claim> claims) =>
claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value).ToList();
private static byte[] getBase64WithoutPadding(string base64)
{
switch (base64.Length % 4)
{
case 2: base64 += "=="; break;
case 3: base64 += "="; break;
}
return Convert.FromBase64String(base64);
}
private static bool getIsExpired(DateTime? expirationDateUtc) =>
!expirationDateUtc.HasValue || !(expirationDateUtc.Value > DateTime.UtcNow);
private static DateTime? getDateUtc(IList<Claim> claims, string type)
{
var exp = claims.SingleOrDefault(claim => claim.Type == type);
if (exp == null)
{
return null;
}
var expValue = getTimeValue(exp.Value);
if (expValue == null)
{
return null;
}
var dateTimeEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
return dateTimeEpoch.AddSeconds(expValue.Value);
}
private static long? getTimeValue(string claimValue)
{
if (long.TryParse(claimValue, out long resultLong))
return resultLong;
if (float.TryParse(claimValue, out float resultFloat))
return (long)resultFloat;
if (double.TryParse(claimValue, out double resultDouble))
return (long)resultDouble;
return null;
}
}
} // In ParseClaimsFromJwt method
return new JwtInfo
{
Claims = claims,
Roles = roles,
ExpirationDateUtc = expirationDateUtc,
IsExpired = isExpired
};
// In GetAuthenticationStateAsync with cannot convert error
return new AuthenticationState(
new ClaimsPrincipal(
new ClaimsIdentity(JwtParser.ParseClaimsFromJwt(token), "jwtAuthType")
)
);
// Resharper suggestion: Explicit cast but with suspicious cast warning.
return new AuthenticationState(
new ClaimsPrincipal(
new ClaimsIdentity((IEnumerable<Claim>)JwtParser.ParseClaimsFromJwt(token), "jwtAuthType")
)
); blazor.webassembly.js:1 crit: Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
Unhandled exception rendering component: The input is not a valid Base-64 string as it contains a non-base 64 character, more than two padding characters, or an illegal character among the padding characters.
System.FormatException: The input is not a valid Base-64 string as it contains a non-base 64 character, more than two padding characters, or an illegal character among the padding characters.
at System.Convert.FromBase64CharPtr(Char* inputPtr, Int32 inputLength)
at System.Convert.FromBase64String(String s) private static byte[] getBase64WithoutPadding(string base64)
{
// From:
// https://github.com/dvsekhvalnov/jose-jwt/blob/master/jose-jwt/util/Base64Url.cs#L16
// https://github.com/auth0/jwt-decode/blob/master/lib/base64_url_decode.js#L15
// https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/0665af62cc58a28ebe184dd97f4d018f84e1d83d/src/Microsoft.IdentityModel.Tokens/Base64UrlEncoder.cs#L175
base64 = base64.Replace('-', '+'); // 62nd char of encoding
base64 = base64.Replace('_', '/'); // 63rd char of encoding
switch (base64.Length % 4) // Pad with trailing '='s
{
case 0: break; // No pad chars in this case
case 2: base64 += "=="; break; // Two pad chars
case 3: base64 += "="; break; // One pad char
default: throw new ArgumentOutOfRangeException(nameof(base64), "Illegal base64url string!");
}
return Convert.FromBase64String(base64); // Standard base64 decoder
}