Asynchronous MultiThreaded SSH engine for Web (Net Core 6, Linux) - Part 5,6 (Crypt/Encrypt JWT Auth header, Middleware, User scoped service, custom AU attribute, custom HttpClient and Typed SignalRHub with saving ConnectionID to Singleton service).
5. Crypt/Encrypt JWT Auth header, Middleware, User scoped service, custom AU attribute, custom HttpClient.
BackendPI used JWT Authentication, I have describe prototype of this project in topic BackendAPI (Net Core 5) project template with Custom Attribute, Service and Controller Example, MySQL database with masked password in config, SwaggerUI, EF Core and extension to EF Core and uploaded scheme of this project to Github https://github.com/Alex-1347/BackendAPI and VisualStudio marketplace https://marketplace.visualstudio.com/items?itemName=vb-netcom.BackendApiNetCore5VB.
In practice this project has a couple changing, of course user password stored in DB is encrypted.
Function GenerateJwtToken and ValidateJWT look as this:
1: Imports System.IdentityModel.Tokens.Jwt
2: Imports System.Text
3: Imports Microsoft.IdentityModel.Tokens
4:
5: Namespace Jwt
6: Partial Public Module JWT
7: Public Function ValidateJWT(ByVal JWT_Token As String, JwtSetting As JwtSettings) As String
8: Dim ValidatedToken As SecurityToken = Nothing
9: Try
10: Dim JwtChecker = New JwtSecurityTokenHandler
11: Dim Key = Encoding.ASCII.GetBytes(JwtSetting.Key)
12: JwtChecker.ValidateToken(JWT_Token, New TokenValidationParameters With {
13: .ValidateIssuerSigningKey = True,
14: .IssuerSigningKey = New SymmetricSecurityKey(Key),
15: .ValidateIssuer = True,
16: .ValidateAudience = False,
17: .ValidIssuer = JwtSetting.Issuer,
18: .ClockSkew = TimeSpan.Zero 'set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later)
19: }, ValidatedToken)
20: Dim JwtToken = CType(ValidatedToken, JwtSecurityToken)
21: Dim UserId = CInt(JwtToken.Claims.First(Function(x) x.Type = "id").Value)
22: Return UserId
23: Catch
24: 'do nothing if jwt validation fails
25: 'user Is Not attached to context so request won't have access to secure routes
26: Return ""
27: End Try
28: End Function
29:
30: End Module
31: End Namespace
1: Imports BackendAPI.Model
2: Imports System.IdentityModel.Tokens.Jwt
3: Imports System.Text
4: Imports Microsoft.IdentityModel.Tokens
5: Imports System.Security.Claims
6:
7: Namespace Jwt
8: Partial Public Module JWT
9: Public Function GenerateJwtToken(ByVal CrUser As ApplicationUser, JwtSetting As JwtSettings) As String
10: 'generate token that is valid for 7 days
11: Dim SecurityKey = Encoding.ASCII.GetBytes(JwtSetting.Key)
12: Dim Credintals = New SigningCredentials(New SymmetricSecurityKey(SecurityKey), SecurityAlgorithms.HmacSha256Signature)
13: Dim Claims = New ClaimsIdentity({New Claim("id", CrUser.Id.ToString())})
14: Dim TokenDescriptor = New SecurityTokenDescriptor With {
15: .Subject = Claims,
16: .Expires = DateTime.UtcNow.AddDays(7),
17: .SigningCredentials = Credintals,
18: .Issuer = JwtSetting.Issuer
19: }
20: Dim TokenHandler = New JwtSecurityTokenHandler()
21: Dim Token As SecurityToken = TokenHandler.CreateToken(TokenDescriptor)
22: Return TokenHandler.WriteToken(Token)
23: End Function
24: End Module
25: End Namespace
MiddleWare inject additional parameters to each request, as result of checking JWT.
1: Imports BackendAPI.Services
2: Imports Microsoft.AspNetCore.Http
3: Imports Microsoft.Extensions.Options
4: Imports Microsoft.IdentityModel.Tokens
5:
6: Namespace Jwt
7: Public Class JwtMiddleware
8: Private ReadOnly _Next As RequestDelegate
9: Private ReadOnly _JwtSettings As JwtSettings
10:
11: Public Sub New(ByVal NextDelegate As RequestDelegate, ByVal JwtSettings As IOptions(Of JwtSettings))
12: _Next = NextDelegate
13: _JwtSettings = JwtSettings.Value
14: End Sub
15:
16: Public Async Function Invoke(ByVal Context As HttpContext, ByVal UserService As IUserService) As Task
17: Dim token = Context.Request.Headers("Authorization").FirstOrDefault?.Split(" ").Last
18: If token IsNot Nothing Then AttachUserToContext(Context, UserService, token)
19: Await _Next(Context)
20: End Function
21:
22: Private Sub AttachUserToContext(ByVal Context As HttpContext, ByVal UserService As IUserService, ByVal JWT_Token As String)
23: Dim UserId = JWT.ValidateJWT(JWT_Token, _JwtSettings)
24: If Not String.IsNullOrEmpty(UserId) Then
25: If IsNumeric(UserId) Then
26: 'attach user to context on successful jwt validation
27: Context.Items("User") = UserService.GetById(UserId)
28: Context.Request.Headers.Add("JwtUserName", UserService.GetById(UserId).UserName)
29: End If
30: End If
31: End Sub
32:
33: End Class
34: End Namespace
And each controller methods can be filtered by Authorizing custom attributes.
1: Imports BackendAPI.Model
2: Imports Microsoft.AspNetCore.Http
3: Imports Microsoft.AspNetCore.Mvc
4: Imports Microsoft.AspNetCore.Mvc.Filters
5: Imports System
6:
7: Namespace Jwt
8:
9: <AttributeUsage(AttributeTargets.[Class] Or AttributeTargets.Method)>
10: Public Class AuthorizeAttribute
11: Inherits Attribute
12: Implements IAuthorizationFilter
13:
14: Public Sub OnAuthorization(ByVal Context As AuthorizationFilterContext) Implements IAuthorizationFilter.OnAuthorization
15: Dim CurUser = CType(Context.HttpContext.Items("User"), ApplicationUser)
16:
17: If CurUser Is Nothing Then
18: Context.Result = New JsonResult(New With {
19: Key .message = "Unauthorized"
20: }) With {
21: .StatusCode = StatusCodes.Status401Unauthorized
22: }
23: End If
24: End Sub
25:
26: End Class
27:
28: End Namespace
For processing JWT I use that library.
Middleware can be initialization as service.
Opposite side of this mechanism is HttpClient for receiving correct JWT token. Pay attention that HttpClient can NOT be working in multitask mode and we will wait when HttpClient is busy.
1: Imports System.Text
2: Imports Newtonsoft.Json
3:
4: Public Module NotificationToken
5: Public Function GetNotificationToken(Request As MyWebClient, Username As String, Password As String) As String
6: Try
7: Dim PostPrm = New BackendAPI.Model.AuthenticateRequest With {
8: .Username = Username,
9: .Password = Password
10: }
11: Dim PostData = JsonConvert.SerializeObject(PostPrm)
12: 'WebClient does not support concurrent I/O operations.
13: While (Request.IsBusy)
14: System.Threading.Thread.Sleep(Random.Shared.Next(1000))
15: End While
16: Dim Response = Encoding.UTF8.GetString(Request.UploadData("/Users/Authenticate", Encoding.UTF8.GetBytes(PostData)))
17: Dim Ret1 = JsonConvert.DeserializeObject(Response)
18: Return Ret1("token").ToString
19: Catch ex As Exception
20: Debug.WriteLine(ex.Message)
21: End Try
22: End Function
23: End Module
MyWebClient is redefine request timeout.
1: Imports System.Net
2:
3: Public Class MyWebClient
4: Inherits WebClient
5: Protected Overloads Function GetWebRequest(URL As Uri) As WebRequest
6: Dim WebRequest = MyBase.GetWebRequest(URL)
7: WebRequest.ContentType = "application/json"
8: WebRequest.Timeout = Integer.MaxValue
9: Return WebRequest
10: End Function
11: End Class
As a result I can in any project place create web request in the same way.
1: Request.Headers.Add("Content-Type", "application/json")
2: Dim Token = GetNotificationToken(Request, NotificationTokenLogin, NotificationTokenPass)
3: Request.Headers.Clear()
4: Request.Headers.Add("Authorization", "Bearer: " & Token)
5: Request.Headers.Add("Content-Type", "application/json")
6: Dim PostPrm = New BashJobFinishedRequest With {
7: .i = NextJob.i,
8: .CrDate = NextJob.CrDate,
9: .toServer = NextJob.toServer,
10: .toVm = NextJob.toVm,
11: .toUser = NextJob.toUser,
12: .SubscribeId = NextJob.SubscribeId,
13: .Command = NextJob.Command,
14: .Comment = NextJob.Comment,
15: .LastUpdate = NextJob.LastUpdate}
16: Dim PostData = JsonConvert.SerializeObject(PostPrm)
17: Try
18: 'Post
19: Dim Response = Encoding.UTF8.GetString(Request.UploadData(_NotificationUrl, Encoding.UTF8.GetBytes(PostData)))
or
1: Request.Headers.Add("Content-Type", "application/json")
2: Dim Token = GetNotificationToken(Request, NotificationTokenLogin, NotificationTokenPass)
3: Request.Headers.Clear()
4: Request.Headers.Add("Authorization", "Bearer: " & Token)
5: Request.Headers.Add("Content-Type", "application/json")
6: Try
7: 'Get
8: Dim Response = Request.DownloadString(NotificationCacheStateUrl)
9: ...
6. Typed SignalRHub with saving ConnectionID to Singleton service
Common templates of this project I have uploaded to Github https://github.com/ViacheslavUKR/SignalRTypedHub and VisualStudio Marketplace https://marketplace.visualstudio.com/items?itemName=vb-net-com.MicroserviceWithTypedSignalRHub two years ago.
So, Typed SygnalR Hub composes from definition of message to send to client, in my case this is message definition.
Initializing SignalR service to DI container.
Also SignalR Hub has options to logging.
Pay attention, SignalR is included in package Microsoft.NET.Sdk.Web and not exists in package Microsoft.NET.Sdk. And no need any additional reference to create library project with SignalR functions.
After this preparation is done, we can create SignalR Hub by the same way.
1: Imports BackendAPI.Model
2: Imports BackendAPI.Services
3: Imports Microsoft.AspNetCore.Http
4: Imports Microsoft.AspNetCore.SignalR
5: Imports Microsoft.Extensions.Configuration
6: Imports Microsoft.Extensions.Logging
7: Imports Microsoft.Extensions.Options
8:
9: Namespace Notification
10: Public Class NotificationHub
11: Inherits Hub(Of IHubNotificationMesssage)
12:
13:
14: Private ReadOnly _logger As ILogger(Of NotificationHub)
15: Private ReadOnly _NotificationCache As INotificationCacheService
16: Private ReadOnly _Aes As AesCryptor
17: Private ReadOnly _DB As ApplicationDbContext
18: Private ReadOnly _UserService As IUserService
19: Private ReadOnly _httpContextAccessor As IHttpContextAccessor
20: Private ReadOnly _Trace As Boolean
21: Private ReadOnly _WithResult As Boolean
22: Private ReadOnly _JwtSettings As Jwt.JwtSettings
23: Public Sub New(logger As ILogger(Of NotificationHub), AppSettings As IOptions(Of Jwt.JwtSettings), NotificationService As INotificationCacheService, UserService As IUserService, Cryptor As IAesCryptor, DbContext As ApplicationDbContext, httpContextAccessor As IHttpContextAccessor, Configuration As IConfiguration)
24: _logger = logger
25: _Aes = Cryptor
26: _DB = DbContext
27: _UserService = UserService
28: _httpContextAccessor = httpContextAccessor
29: _Trace = Configuration.GetValue(Of Boolean)("TraceAPI:Trace")
30: _WithResult = Configuration.GetValue(Of Boolean)("TraceAPI:Trace")
31: _NotificationCache = NotificationService
32: _JwtSettings = AppSettings.Value
33: End Sub
34:
35: 'after new
36: Public Overrides Async Function OnConnectedAsync() As Task
37: Dim JWTToken As String = Context.GetHttpContext().Request.Headers("Authorization")
38: Dim ValidUserID As String = ""
39: Try
40: ValidUserID = Jwt.ValidateJWT(JWTToken, _JwtSettings)
41: Catch e As Microsoft.IdentityModel.Tokens.SecurityTokenExpiredException
42: _logger.LogInformation("token experied")
43: End Try
44:
45: If ValidUserID IsNot Nothing Then
46: _NotificationCache.AddSignalRClientConnection(Context.ConnectionId, ValidUserID)
47: _logger.LogInformation($"User {ValidUserID} connected to {Me.[GetType].Name} hub, ConnectionId={Context.ConnectionId}, Connections {_NotificationCache.PrintSignalRConnectionKeys}")
48: Else
49: _logger.LogInformation("WrongToken")
50: End If
51: Await MyBase.OnConnectedAsync()
52: End Function
53:
54: 'after new Again
55: Public Overrides Async Function OnDisconnectedAsync(ByVal Exception As System.Exception) As Task
56: _logger.LogInformation("OnDisconnectedAsync" & Exception?.Message)
57:
58: Dim JWTToken As String = Context.GetHttpContext().Request.Headers("Authorization")
59: Dim ValidUserID As String = ""
60:
61: Try
62: ValidUserID = Jwt.ValidateJWT(JWTToken, _JwtSettings)
63: Catch e As Microsoft.IdentityModel.Tokens.SecurityTokenExpiredException
64: _logger.LogInformation("token experied")
65: End Try
66:
67: If ValidUserID IsNot Nothing Then
68: _NotificationCache.DelSignalRClientConnection(Context.ConnectionId)
69: _logger.LogInformation($"User {ValidUserID} disconnected from {Me.[GetType]().Name} hub, ConnectionId={Context.ConnectionId}, Connections {_NotificationCache.PrintSignalRConnectionKeys}")
70: Else
71: _logger.LogInformation("WrongToken")
72: End If
73:
74: Await MyBase.OnDisconnectedAsync(Exception)
75: End Function
76:
77: End Class
78: End Namespace
As you can see, in my case SignalR Hub doing nothing, instead print log and check JWT token. If User is correct UserName and ConnectionID stored in Singleton services INotificationCacheService.
When site working we can see how to user and connectionId appears and disappears in INotificationCacheService service.
|