(CORE) CORE (2022)

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.






Comments ( )
<00>  <01>  <02>  <03>  <04>  <05>  <06>  <07>  <08>  <09>  <10>  <11>  <12>  <13>  <14>  <15>  <16>  <17>  <18>  <19>  <20>  <21>  <22>  <23
Link to this page: http://www.vb-net.com/SshQueueNotificationServer/Index2.htm
<SITEMAP>  <MVC>  <ASP>  <NET>  <DATA>  <KIOSK>  <FLEX>  <SQL>  <NOTES>  <LINUX>  <MONO>  <FREEWARE>  <DOCS>  <ENG>  <CHAT ME>  <ABOUT ME>  < THANKS ME>