Просмотр исходного кода

feat: replace websocket connection validation with grpc (#954)

* feat: replace websocket connection validation with grpc

* fix(api-gateway): error logs metadata details

* fix binding

* fix logs severity on websockets

* add user uuid to grpc call error logs
Karol Sójko 1 год назад
Родитель
Сommit
d5c1b76de0

+ 6 - 3
packages/api-gateway/bin/server.ts

@@ -91,9 +91,12 @@ void container.load().then((container) => {
 
   server.setErrorConfig((app) => {
     app.use((error: Record<string, unknown>, request: Request, response: Response, _next: NextFunction) => {
-      logger.error(
-        `[URL: |${request.method}| ${request.url}][SNJS: ${request.headers['x-snjs-version']}][Application: ${request.headers['x-application-version']}] Error thrown: ${error.stack}`,
-      )
+      logger.error(`${error.stack}`, {
+        method: request.method,
+        url: request.url,
+        snjs: request.headers['x-snjs-version'],
+        application: request.headers['x-application-version'],
+      })
       logger.debug(
         `[URL: |${request.method}| ${request.url}][SNJS: ${request.headers['x-snjs-version']}][Application: ${
           request.headers['x-application-version']

+ 20 - 13
packages/api-gateway/src/Bootstrap/Container.ts

@@ -22,19 +22,13 @@ import { EndpointResolver } from '../Service/Resolver/EndpointResolver'
 import { RequiredCrossServiceTokenMiddleware } from '../Controller/RequiredCrossServiceTokenMiddleware'
 import { OptionalCrossServiceTokenMiddleware } from '../Controller/OptionalCrossServiceTokenMiddleware'
 import { Transform } from 'stream'
-import {
-  ISessionsClient,
-  ISyncingClient,
-  SessionsClient,
-  SyncRequest,
-  SyncResponse,
-  SyncingClient,
-} from '@standardnotes/grpc'
+import { AuthClient, IAuthClient, ISyncingClient, SyncRequest, SyncResponse, SyncingClient } from '@standardnotes/grpc'
 import { GRPCServiceProxy } from '../Service/gRPC/GRPCServiceProxy'
 import { GRPCSyncingServerServiceProxy } from '../Service/gRPC/GRPCSyncingServerServiceProxy'
 import { SyncResponseHttpRepresentation } from '../Mapping/Sync/Http/SyncResponseHttpRepresentation'
 import { SyncRequestGRPCMapper } from '../Mapping/Sync/GRPC/SyncRequestGRPCMapper'
 import { SyncResponseGRPCMapper } from '../Mapping/Sync/GRPC/SyncResponseGRPCMapper'
+import { GRPCWebSocketAuthMiddleware } from '../Controller/GRPCWebSocketAuthMiddleware'
 
 export class ContainerConfigLoader {
   async load(configuration?: {
@@ -51,6 +45,7 @@ export class ContainerConfigLoader {
     const isConfiguredForSelfHosting = env.get('MODE', true) === 'self-hosted'
     const isConfiguredForHomeServerOrSelfHosting = isConfiguredForHomeServer || isConfiguredForSelfHosting
     const isConfiguredForInMemoryCache = env.get('CACHE_TYPE', true) === 'memory'
+    const isConfiguredForGRPCProxy = env.get('SERVICE_PROXY_TYPE', true) === 'grpc'
 
     container
       .bind<boolean>(TYPES.ApiGateway_IS_CONFIGURED_FOR_HOME_SERVER_OR_SELF_HOSTING)
@@ -122,7 +117,6 @@ export class ContainerConfigLoader {
     container
       .bind<OptionalCrossServiceTokenMiddleware>(TYPES.ApiGateway_OptionalCrossServiceTokenMiddleware)
       .to(OptionalCrossServiceTokenMiddleware)
-    container.bind<WebSocketAuthMiddleware>(TYPES.ApiGateway_WebSocketAuthMiddleware).to(WebSocketAuthMiddleware)
     container
       .bind<SubscriptionTokenAuthMiddleware>(TYPES.ApiGateway_SubscriptionTokenAuthMiddleware)
       .to(SubscriptionTokenAuthMiddleware)
@@ -153,7 +147,6 @@ export class ContainerConfigLoader {
           new DirectCallServiceProxy(configuration.serviceContainer, container.get(TYPES.ApiGateway_FILES_SERVER_URL)),
         )
     } else {
-      const isConfiguredForGRPCProxy = env.get('SERVICE_PROXY_TYPE', true) === 'grpc'
       if (isConfiguredForGRPCProxy) {
         container.bind(TYPES.ApiGateway_AUTH_SERVER_GRPC_URL).toConstantValue(env.get('AUTH_SERVER_GRPC_URL'))
         container.bind(TYPES.ApiGateway_SYNCING_SERVER_GRPC_URL).toConstantValue(env.get('SYNCING_SERVER_GRPC_URL'))
@@ -165,8 +158,8 @@ export class ContainerConfigLoader {
           ? +env.get('GRPC_MAX_MESSAGE_SIZE', true)
           : 1024 * 1024 * 50
 
-        container.bind<ISessionsClient>(TYPES.ApiGateway_GRPCSessionsClient).toConstantValue(
-          new SessionsClient(
+        container.bind<IAuthClient>(TYPES.ApiGateway_GRPCAuthClient).toConstantValue(
+          new AuthClient(
             container.get<string>(TYPES.ApiGateway_AUTH_SERVER_GRPC_URL),
             grpc.credentials.createInsecure(),
             {
@@ -229,7 +222,7 @@ export class ContainerConfigLoader {
               container.get<CrossServiceTokenCacheInterface>(TYPES.ApiGateway_CrossServiceTokenCache),
               container.get<winston.Logger>(TYPES.ApiGateway_Logger),
               container.get<TimerInterface>(TYPES.ApiGateway_Timer),
-              container.get<ISessionsClient>(TYPES.ApiGateway_GRPCSessionsClient),
+              container.get<IAuthClient>(TYPES.ApiGateway_GRPCAuthClient),
               container.get<GRPCSyncingServerServiceProxy>(TYPES.ApiGateway_GRPCSyncingServerServiceProxy),
             ),
           )
@@ -238,6 +231,20 @@ export class ContainerConfigLoader {
       }
     }
 
+    if (isConfiguredForGRPCProxy) {
+      container
+        .bind<GRPCWebSocketAuthMiddleware>(TYPES.ApiGateway_WebSocketAuthMiddleware)
+        .toConstantValue(
+          new GRPCWebSocketAuthMiddleware(
+            container.get<IAuthClient>(TYPES.ApiGateway_GRPCAuthClient),
+            container.get<string>(TYPES.ApiGateway_AUTH_JWT_SECRET),
+            container.get<winston.Logger>(TYPES.ApiGateway_Logger),
+          ),
+        )
+    } else {
+      container.bind<WebSocketAuthMiddleware>(TYPES.ApiGateway_WebSocketAuthMiddleware).to(WebSocketAuthMiddleware)
+    }
+
     logger.debug('Configuration complete')
 
     return container

+ 1 - 1
packages/api-gateway/src/Bootstrap/Types.ts

@@ -34,6 +34,6 @@ export const TYPES = {
   ApiGateway_CrossServiceTokenCache: Symbol.for('ApiGateway_CrossServiceTokenCache'),
   ApiGateway_Timer: Symbol.for('ApiGateway_Timer'),
   ApiGateway_EndpointResolver: Symbol.for('ApiGateway_EndpointResolver'),
-  ApiGateway_GRPCSessionsClient: Symbol.for('ApiGateway_GRPCSessionsClient'),
+  ApiGateway_GRPCAuthClient: Symbol.for('ApiGateway_GRPCAuthClient'),
   ApiGateway_GRPCSyncingClient: Symbol.for('ApiGateway_GRPCSyncingClient'),
 }

+ 117 - 0
packages/api-gateway/src/Controller/GRPCWebSocketAuthMiddleware.ts

@@ -0,0 +1,117 @@
+import { CrossServiceTokenData } from '@standardnotes/security'
+import * as grpc from '@grpc/grpc-js'
+import { NextFunction, Request, Response } from 'express'
+import { BaseMiddleware } from 'inversify-express-utils'
+import { verify } from 'jsonwebtoken'
+import { Logger } from 'winston'
+import { ConnectionValidationResponse, IAuthClient, WebsocketConnectionAuthorizationHeader } from '@standardnotes/grpc'
+
+export class GRPCWebSocketAuthMiddleware extends BaseMiddleware {
+  constructor(
+    private authClient: IAuthClient,
+    private jwtSecret: string,
+    private logger: Logger,
+  ) {
+    super()
+  }
+
+  async handler(request: Request, response: Response, next: NextFunction): Promise<void> {
+    const authHeaderValue = request.headers.authorization as string
+
+    if (!authHeaderValue) {
+      response.status(401).send({
+        error: {
+          tag: 'invalid-auth',
+          message: 'Invalid login credentials.',
+        },
+      })
+
+      return
+    }
+
+    const promise = new Promise((resolve, reject) => {
+      try {
+        const request = new WebsocketConnectionAuthorizationHeader()
+        request.setToken(authHeaderValue)
+
+        this.authClient.validateWebsocket(
+          request,
+          (error: grpc.ServiceError | null, response: ConnectionValidationResponse) => {
+            if (error) {
+              const responseCode = error.metadata.get('x-auth-error-response-code').pop()
+              if (responseCode) {
+                return resolve({
+                  status: +responseCode,
+                  data: {
+                    error: {
+                      message: error.metadata.get('x-auth-error-message').pop(),
+                      tag: error.metadata.get('x-auth-error-tag').pop(),
+                    },
+                  },
+                  headers: {
+                    contentType: 'application/json',
+                  },
+                })
+              }
+
+              return reject(error)
+            }
+
+            return resolve({
+              status: 200,
+              data: {
+                authToken: response.getCrossServiceToken(),
+              },
+              headers: {
+                contentType: 'application/json',
+              },
+            })
+          },
+        )
+      } catch (error) {
+        return reject(error)
+      }
+    })
+
+    try {
+      const authResponse = (await promise) as {
+        status: number
+        headers: Record<string, unknown>
+        data: Record<string, unknown>
+      }
+
+      if (authResponse.status > 200) {
+        response.setHeader('content-type', authResponse.headers['content-type'] as string)
+        response.status(authResponse.status).send(authResponse.data)
+
+        return
+      }
+
+      const crossServiceToken = authResponse.data.authToken as string
+
+      response.locals.authToken = crossServiceToken
+
+      const decodedToken = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] })
+
+      response.locals.user = decodedToken.user
+      response.locals.session = decodedToken.session
+      response.locals.roles = decodedToken.roles
+    } catch (error) {
+      this.logger.error(
+        `Could not pass the request to websocket connection validation on underlying service: ${
+          (error as Error).message
+        }`,
+      )
+
+      response
+        .status(500)
+        .send(
+          "Unfortunately, we couldn't handle your request. Please try again or contact our support if the error persists.",
+        )
+
+      return
+    }
+
+    return next()
+  }
+}

+ 3 - 3
packages/api-gateway/src/Service/gRPC/GRPCServiceProxy.ts

@@ -2,7 +2,7 @@ import { AxiosInstance, AxiosError, AxiosResponse, Method } from 'axios'
 import { Request, Response } from 'express'
 import { Logger } from 'winston'
 import { TimerInterface } from '@standardnotes/time'
-import { ISessionsClient, AuthorizationHeader, SessionValidationResponse } from '@standardnotes/grpc'
+import { IAuthClient, AuthorizationHeader, SessionValidationResponse } from '@standardnotes/grpc'
 import * as grpc from '@grpc/grpc-js'
 
 import { CrossServiceTokenCacheInterface } from '../Cache/CrossServiceTokenCacheInterface'
@@ -23,7 +23,7 @@ export class GRPCServiceProxy implements ServiceProxyInterface {
     private crossServiceTokenCache: CrossServiceTokenCacheInterface,
     private logger: Logger,
     private timer: TimerInterface,
-    private sessionsClient: ISessionsClient,
+    private authClient: IAuthClient,
     private gRPCSyncingServerServiceProxy: GRPCSyncingServerServiceProxy,
   ) {}
 
@@ -41,7 +41,7 @@ export class GRPCServiceProxy implements ServiceProxyInterface {
 
         this.logger.debug('[GRPCServiceProxy] Validating session via gRPC')
 
-        this.sessionsClient.validate(
+        this.authClient.validate(
           request,
           metadata,
           (error: grpc.ServiceError | null, response: SessionValidationResponse) => {

+ 6 - 6
packages/api-gateway/src/Service/gRPC/GRPCSyncingServerServiceProxy.ts

@@ -44,9 +44,9 @@ export class GRPCSyncingServerServiceProxy {
 
             if (error.code === Status.INTERNAL) {
               this.logger.error(
-                `[GRPCSyncingServerServiceProxy] Internal gRPC error: ${error.message}. Payload: ${JSON.stringify(
-                  payload,
-                )}`,
+                `[GRPCSyncingServerServiceProxy][${response.locals.user.uuid}] Internal gRPC error: ${
+                  error.message
+                }. Payload: ${JSON.stringify(payload)}`,
               )
             }
 
@@ -61,9 +61,9 @@ export class GRPCSyncingServerServiceProxy {
           (error as Record<string, unknown>).code === Status.INTERNAL
         ) {
           this.logger.error(
-            `[GRPCSyncingServerServiceProxy] Internal gRPC error: ${JSON.stringify(error)}. Payload: ${JSON.stringify(
-              payload,
-            )}`,
+            `[GRPCSyncingServerServiceProxy][${response.locals.user.uuid}] Internal gRPC error: ${JSON.stringify(
+              error,
+            )}. Payload: ${JSON.stringify(payload)}`,
           )
         }
 

+ 8 - 5
packages/auth/bin/server.ts

@@ -30,10 +30,11 @@ import { InversifyExpressServer } from 'inversify-express-utils'
 import { ContainerConfigLoader } from '../src/Bootstrap/Container'
 import TYPES from '../src/Bootstrap/Types'
 import { Env } from '../src/Bootstrap/Env'
-import { SessionsServer } from '../src/Infra/gRPC/SessionsServer'
-import { SessionsService } from '@standardnotes/grpc'
+import { AuthServer } from '../src/Infra/gRPC/AuthServer'
+import { AuthService } from '@standardnotes/grpc'
 import { AuthenticateRequest } from '../src/Domain/UseCase/AuthenticateRequest'
 import { CreateCrossServiceToken } from '../src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken'
+import { TokenDecoderInterface, WebSocketConnectionTokenData } from '@standardnotes/security'
 
 const container = new ContainerConfigLoader()
 void container.load().then((container) => {
@@ -95,14 +96,16 @@ void container.load().then((container) => {
 
   const gRPCPort = env.get('GRPC_PORT', true) ? +env.get('GRPC_PORT', true) : 50051
 
-  const sessionsServer = new SessionsServer(
+  const authServer = new AuthServer(
     container.get<AuthenticateRequest>(TYPES.Auth_AuthenticateRequest),
     container.get<CreateCrossServiceToken>(TYPES.Auth_CreateCrossServiceToken),
+    container.get<TokenDecoderInterface<WebSocketConnectionTokenData>>(TYPES.Auth_WebSocketConnectionTokenDecoder),
     container.get<winston.Logger>(TYPES.Auth_Logger),
   )
 
-  grpcServer.addService(SessionsService, {
-    validate: sessionsServer.validate.bind(sessionsServer),
+  grpcServer.addService(AuthService, {
+    validate: authServer.validate.bind(authServer),
+    validateWebsocket: authServer.validateWebsocket.bind(authServer),
   })
   grpcServer.bindAsync(`0.0.0.0:${gRPCPort}`, grpc.ServerCredentials.createInsecure(), (error, port) => {
     if (error) {

+ 74 - 2
packages/auth/src/Infra/gRPC/SessionsServer.ts → packages/auth/src/Infra/gRPC/AuthServer.ts

@@ -1,20 +1,92 @@
 import * as grpc from '@grpc/grpc-js'
 import { Status } from '@grpc/grpc-js/build/src/constants'
 
-import { AuthorizationHeader, ISessionsServer, SessionValidationResponse } from '@standardnotes/grpc'
+import {
+  AuthorizationHeader,
+  ConnectionValidationResponse,
+  IAuthServer,
+  SessionValidationResponse,
+  WebsocketConnectionAuthorizationHeader,
+} from '@standardnotes/grpc'
 
 import { AuthenticateRequest } from '../../Domain/UseCase/AuthenticateRequest'
 import { User } from '../../Domain/User/User'
 import { CreateCrossServiceToken } from '../../Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken'
 import { Logger } from 'winston'
+import { ErrorTag } from '@standardnotes/responses'
+import { TokenDecoderInterface, WebSocketConnectionTokenData } from '@standardnotes/security'
 
-export class SessionsServer implements ISessionsServer {
+export class AuthServer implements IAuthServer {
   constructor(
     private authenticateRequest: AuthenticateRequest,
     private createCrossServiceToken: CreateCrossServiceToken,
+    protected tokenDecoder: TokenDecoderInterface<WebSocketConnectionTokenData>,
     private logger: Logger,
   ) {}
 
+  async validateWebsocket(
+    call: grpc.ServerUnaryCall<WebsocketConnectionAuthorizationHeader, ConnectionValidationResponse>,
+    callback: grpc.sendUnaryData<ConnectionValidationResponse>,
+  ): Promise<void> {
+    try {
+      const token: WebSocketConnectionTokenData | undefined = this.tokenDecoder.decodeToken(call.request.getToken())
+
+      if (token === undefined) {
+        const metadata = new grpc.Metadata()
+        metadata.set('x-auth-error-message', 'Invalid authorization token.')
+        metadata.set('x-auth-error-tag', ErrorTag.AuthInvalid)
+        metadata.set('x-auth-error-response-code', '401')
+        return callback(
+          {
+            code: Status.PERMISSION_DENIED,
+            message: 'Invalid authorization token.',
+            name: ErrorTag.AuthInvalid,
+            metadata,
+          },
+          null,
+        )
+      }
+
+      const resultOrError = await this.createCrossServiceToken.execute({
+        userUuid: token.userUuid,
+        sessionUuid: token.sessionUuid,
+      })
+      if (resultOrError.isFailed()) {
+        const metadata = new grpc.Metadata()
+        metadata.set('x-auth-error-message', resultOrError.getError())
+        metadata.set('x-auth-error-response-code', '400')
+
+        return callback(
+          {
+            code: Status.INVALID_ARGUMENT,
+            message: resultOrError.getError(),
+            name: 'INVALID_ARGUMENT',
+            metadata,
+          },
+          null,
+        )
+      }
+
+      const response = new ConnectionValidationResponse()
+      response.setCrossServiceToken(resultOrError.getValue())
+
+      this.logger.debug('[SessionsServer] Websocket connection validated via gRPC')
+
+      callback(null, response)
+    } catch (error) {
+      this.logger.error(`[SessionsServer] Error validating websocket connection via gRPC: ${(error as Error).message}`)
+
+      callback(
+        {
+          code: Status.UNKNOWN,
+          message: 'An error occurred while validating websocket connection',
+          name: 'UNKNOWN',
+        },
+        null,
+      )
+    }
+  }
+
   async validate(
     call: grpc.ServerUnaryCall<AuthorizationHeader, SessionValidationResponse>,
     callback: grpc.sendUnaryData<SessionValidationResponse>,

+ 25 - 8
packages/grpc/lib/auth_grpc_pb.d.ts

@@ -7,12 +7,13 @@
 import * as grpc from "@grpc/grpc-js";
 import * as auth_pb from "./auth_pb";
 
-interface ISessionsService extends grpc.ServiceDefinition<grpc.UntypedServiceImplementation> {
-    validate: ISessionsService_Ivalidate;
+interface IAuthService extends grpc.ServiceDefinition<grpc.UntypedServiceImplementation> {
+    validate: IAuthService_Ivalidate;
+    validateWebsocket: IAuthService_IvalidateWebsocket;
 }
 
-interface ISessionsService_Ivalidate extends grpc.MethodDefinition<auth_pb.AuthorizationHeader, auth_pb.SessionValidationResponse> {
-    path: "/auth.Sessions/validate";
+interface IAuthService_Ivalidate extends grpc.MethodDefinition<auth_pb.AuthorizationHeader, auth_pb.SessionValidationResponse> {
+    path: "/auth.Auth/validate";
     requestStream: false;
     responseStream: false;
     requestSerialize: grpc.serialize<auth_pb.AuthorizationHeader>;
@@ -20,22 +21,38 @@ interface ISessionsService_Ivalidate extends grpc.MethodDefinition<auth_pb.Autho
     responseSerialize: grpc.serialize<auth_pb.SessionValidationResponse>;
     responseDeserialize: grpc.deserialize<auth_pb.SessionValidationResponse>;
 }
+interface IAuthService_IvalidateWebsocket extends grpc.MethodDefinition<auth_pb.WebsocketConnectionAuthorizationHeader, auth_pb.ConnectionValidationResponse> {
+    path: "/auth.Auth/validateWebsocket";
+    requestStream: false;
+    responseStream: false;
+    requestSerialize: grpc.serialize<auth_pb.WebsocketConnectionAuthorizationHeader>;
+    requestDeserialize: grpc.deserialize<auth_pb.WebsocketConnectionAuthorizationHeader>;
+    responseSerialize: grpc.serialize<auth_pb.ConnectionValidationResponse>;
+    responseDeserialize: grpc.deserialize<auth_pb.ConnectionValidationResponse>;
+}
 
-export const SessionsService: ISessionsService;
+export const AuthService: IAuthService;
 
-export interface ISessionsServer {
+export interface IAuthServer {
     validate: grpc.handleUnaryCall<auth_pb.AuthorizationHeader, auth_pb.SessionValidationResponse>;
+    validateWebsocket: grpc.handleUnaryCall<auth_pb.WebsocketConnectionAuthorizationHeader, auth_pb.ConnectionValidationResponse>;
 }
 
-export interface ISessionsClient {
+export interface IAuthClient {
     validate(request: auth_pb.AuthorizationHeader, callback: (error: grpc.ServiceError | null, response: auth_pb.SessionValidationResponse) => void): grpc.ClientUnaryCall;
     validate(request: auth_pb.AuthorizationHeader, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: auth_pb.SessionValidationResponse) => void): grpc.ClientUnaryCall;
     validate(request: auth_pb.AuthorizationHeader, metadata: grpc.Metadata, options: Partial<grpc.CallOptions>, callback: (error: grpc.ServiceError | null, response: auth_pb.SessionValidationResponse) => void): grpc.ClientUnaryCall;
+    validateWebsocket(request: auth_pb.WebsocketConnectionAuthorizationHeader, callback: (error: grpc.ServiceError | null, response: auth_pb.ConnectionValidationResponse) => void): grpc.ClientUnaryCall;
+    validateWebsocket(request: auth_pb.WebsocketConnectionAuthorizationHeader, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: auth_pb.ConnectionValidationResponse) => void): grpc.ClientUnaryCall;
+    validateWebsocket(request: auth_pb.WebsocketConnectionAuthorizationHeader, metadata: grpc.Metadata, options: Partial<grpc.CallOptions>, callback: (error: grpc.ServiceError | null, response: auth_pb.ConnectionValidationResponse) => void): grpc.ClientUnaryCall;
 }
 
-export class SessionsClient extends grpc.Client implements ISessionsClient {
+export class AuthClient extends grpc.Client implements IAuthClient {
     constructor(address: string, credentials: grpc.ChannelCredentials, options?: object);
     public validate(request: auth_pb.AuthorizationHeader, callback: (error: grpc.ServiceError | null, response: auth_pb.SessionValidationResponse) => void): grpc.ClientUnaryCall;
     public validate(request: auth_pb.AuthorizationHeader, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: auth_pb.SessionValidationResponse) => void): grpc.ClientUnaryCall;
     public validate(request: auth_pb.AuthorizationHeader, metadata: grpc.Metadata, options: Partial<grpc.CallOptions>, callback: (error: grpc.ServiceError | null, response: auth_pb.SessionValidationResponse) => void): grpc.ClientUnaryCall;
+    public validateWebsocket(request: auth_pb.WebsocketConnectionAuthorizationHeader, callback: (error: grpc.ServiceError | null, response: auth_pb.ConnectionValidationResponse) => void): grpc.ClientUnaryCall;
+    public validateWebsocket(request: auth_pb.WebsocketConnectionAuthorizationHeader, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: auth_pb.ConnectionValidationResponse) => void): grpc.ClientUnaryCall;
+    public validateWebsocket(request: auth_pb.WebsocketConnectionAuthorizationHeader, metadata: grpc.Metadata, options: Partial<grpc.CallOptions>, callback: (error: grpc.ServiceError | null, response: auth_pb.ConnectionValidationResponse) => void): grpc.ClientUnaryCall;
 }

+ 36 - 3
packages/grpc/lib/auth_grpc_pb.js

@@ -15,6 +15,17 @@ function deserialize_auth_AuthorizationHeader(buffer_arg) {
   return auth_pb.AuthorizationHeader.deserializeBinary(new Uint8Array(buffer_arg));
 }
 
+function serialize_auth_ConnectionValidationResponse(arg) {
+  if (!(arg instanceof auth_pb.ConnectionValidationResponse)) {
+    throw new Error('Expected argument of type auth.ConnectionValidationResponse');
+  }
+  return Buffer.from(arg.serializeBinary());
+}
+
+function deserialize_auth_ConnectionValidationResponse(buffer_arg) {
+  return auth_pb.ConnectionValidationResponse.deserializeBinary(new Uint8Array(buffer_arg));
+}
+
 function serialize_auth_SessionValidationResponse(arg) {
   if (!(arg instanceof auth_pb.SessionValidationResponse)) {
     throw new Error('Expected argument of type auth.SessionValidationResponse');
@@ -26,10 +37,21 @@ function deserialize_auth_SessionValidationResponse(buffer_arg) {
   return auth_pb.SessionValidationResponse.deserializeBinary(new Uint8Array(buffer_arg));
 }
 
+function serialize_auth_WebsocketConnectionAuthorizationHeader(arg) {
+  if (!(arg instanceof auth_pb.WebsocketConnectionAuthorizationHeader)) {
+    throw new Error('Expected argument of type auth.WebsocketConnectionAuthorizationHeader');
+  }
+  return Buffer.from(arg.serializeBinary());
+}
+
+function deserialize_auth_WebsocketConnectionAuthorizationHeader(buffer_arg) {
+  return auth_pb.WebsocketConnectionAuthorizationHeader.deserializeBinary(new Uint8Array(buffer_arg));
+}
+
 
-var SessionsService = exports.SessionsService = {
+var AuthService = exports.AuthService = {
   validate: {
-    path: '/auth.Sessions/validate',
+    path: '/auth.Auth/validate',
     requestStream: false,
     responseStream: false,
     requestType: auth_pb.AuthorizationHeader,
@@ -39,6 +61,17 @@ var SessionsService = exports.SessionsService = {
     responseSerialize: serialize_auth_SessionValidationResponse,
     responseDeserialize: deserialize_auth_SessionValidationResponse,
   },
+  validateWebsocket: {
+    path: '/auth.Auth/validateWebsocket',
+    requestStream: false,
+    responseStream: false,
+    requestType: auth_pb.WebsocketConnectionAuthorizationHeader,
+    responseType: auth_pb.ConnectionValidationResponse,
+    requestSerialize: serialize_auth_WebsocketConnectionAuthorizationHeader,
+    requestDeserialize: deserialize_auth_WebsocketConnectionAuthorizationHeader,
+    responseSerialize: serialize_auth_ConnectionValidationResponse,
+    responseDeserialize: deserialize_auth_ConnectionValidationResponse,
+  },
 };
 
-exports.SessionsClient = grpc.makeGenericClientConstructor(SessionsService);
+exports.AuthClient = grpc.makeGenericClientConstructor(AuthService);

+ 40 - 0
packages/grpc/lib/auth_pb.d.ts

@@ -45,3 +45,43 @@ export namespace SessionValidationResponse {
         crossServiceToken: string,
     }
 }
+
+export class WebsocketConnectionAuthorizationHeader extends jspb.Message { 
+    getToken(): string;
+    setToken(value: string): WebsocketConnectionAuthorizationHeader;
+
+    serializeBinary(): Uint8Array;
+    toObject(includeInstance?: boolean): WebsocketConnectionAuthorizationHeader.AsObject;
+    static toObject(includeInstance: boolean, msg: WebsocketConnectionAuthorizationHeader): WebsocketConnectionAuthorizationHeader.AsObject;
+    static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
+    static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
+    static serializeBinaryToWriter(message: WebsocketConnectionAuthorizationHeader, writer: jspb.BinaryWriter): void;
+    static deserializeBinary(bytes: Uint8Array): WebsocketConnectionAuthorizationHeader;
+    static deserializeBinaryFromReader(message: WebsocketConnectionAuthorizationHeader, reader: jspb.BinaryReader): WebsocketConnectionAuthorizationHeader;
+}
+
+export namespace WebsocketConnectionAuthorizationHeader {
+    export type AsObject = {
+        token: string,
+    }
+}
+
+export class ConnectionValidationResponse extends jspb.Message { 
+    getCrossServiceToken(): string;
+    setCrossServiceToken(value: string): ConnectionValidationResponse;
+
+    serializeBinary(): Uint8Array;
+    toObject(includeInstance?: boolean): ConnectionValidationResponse.AsObject;
+    static toObject(includeInstance: boolean, msg: ConnectionValidationResponse): ConnectionValidationResponse.AsObject;
+    static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
+    static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
+    static serializeBinaryToWriter(message: ConnectionValidationResponse, writer: jspb.BinaryWriter): void;
+    static deserializeBinary(bytes: Uint8Array): ConnectionValidationResponse;
+    static deserializeBinaryFromReader(message: ConnectionValidationResponse, reader: jspb.BinaryReader): ConnectionValidationResponse;
+}
+
+export namespace ConnectionValidationResponse {
+    export type AsObject = {
+        crossServiceToken: string,
+    }
+}

+ 304 - 0
packages/grpc/lib/auth_pb.js

@@ -22,7 +22,9 @@ var global = (function() {
 }.call(null));
 
 goog.exportSymbol('proto.auth.AuthorizationHeader', null, global);
+goog.exportSymbol('proto.auth.ConnectionValidationResponse', null, global);
 goog.exportSymbol('proto.auth.SessionValidationResponse', null, global);
+goog.exportSymbol('proto.auth.WebsocketConnectionAuthorizationHeader', null, global);
 /**
  * Generated by JsPbCodeGenerator.
  * @param {Array=} opt_data Optional initial data array, typically from a
@@ -65,6 +67,48 @@ if (goog.DEBUG && !COMPILED) {
    */
   proto.auth.SessionValidationResponse.displayName = 'proto.auth.SessionValidationResponse';
 }
+/**
+ * Generated by JsPbCodeGenerator.
+ * @param {Array=} opt_data Optional initial data array, typically from a
+ * server response, or constructed directly in Javascript. The array is used
+ * in place and becomes part of the constructed object. It is not cloned.
+ * If no data is provided, the constructed object will be empty, but still
+ * valid.
+ * @extends {jspb.Message}
+ * @constructor
+ */
+proto.auth.WebsocketConnectionAuthorizationHeader = function(opt_data) {
+  jspb.Message.initialize(this, opt_data, 0, -1, null, null);
+};
+goog.inherits(proto.auth.WebsocketConnectionAuthorizationHeader, jspb.Message);
+if (goog.DEBUG && !COMPILED) {
+  /**
+   * @public
+   * @override
+   */
+  proto.auth.WebsocketConnectionAuthorizationHeader.displayName = 'proto.auth.WebsocketConnectionAuthorizationHeader';
+}
+/**
+ * Generated by JsPbCodeGenerator.
+ * @param {Array=} opt_data Optional initial data array, typically from a
+ * server response, or constructed directly in Javascript. The array is used
+ * in place and becomes part of the constructed object. It is not cloned.
+ * If no data is provided, the constructed object will be empty, but still
+ * valid.
+ * @extends {jspb.Message}
+ * @constructor
+ */
+proto.auth.ConnectionValidationResponse = function(opt_data) {
+  jspb.Message.initialize(this, opt_data, 0, -1, null, null);
+};
+goog.inherits(proto.auth.ConnectionValidationResponse, jspb.Message);
+if (goog.DEBUG && !COMPILED) {
+  /**
+   * @public
+   * @override
+   */
+  proto.auth.ConnectionValidationResponse.displayName = 'proto.auth.ConnectionValidationResponse';
+}
 
 
 
@@ -325,4 +369,264 @@ proto.auth.SessionValidationResponse.prototype.setCrossServiceToken = function(v
 };
 
 
+
+
+
+if (jspb.Message.GENERATE_TO_OBJECT) {
+/**
+ * Creates an object representation of this proto.
+ * Field names that are reserved in JavaScript and will be renamed to pb_name.
+ * Optional fields that are not set will be set to undefined.
+ * To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
+ * For the list of reserved names please see:
+ *     net/proto2/compiler/js/internal/generator.cc#kKeyword.
+ * @param {boolean=} opt_includeInstance Deprecated. whether to include the
+ *     JSPB instance for transitional soy proto support:
+ *     http://goto/soy-param-migration
+ * @return {!Object}
+ */
+proto.auth.WebsocketConnectionAuthorizationHeader.prototype.toObject = function(opt_includeInstance) {
+  return proto.auth.WebsocketConnectionAuthorizationHeader.toObject(opt_includeInstance, this);
+};
+
+
+/**
+ * Static version of the {@see toObject} method.
+ * @param {boolean|undefined} includeInstance Deprecated. Whether to include
+ *     the JSPB instance for transitional soy proto support:
+ *     http://goto/soy-param-migration
+ * @param {!proto.auth.WebsocketConnectionAuthorizationHeader} msg The msg instance to transform.
+ * @return {!Object}
+ * @suppress {unusedLocalVariables} f is only used for nested messages
+ */
+proto.auth.WebsocketConnectionAuthorizationHeader.toObject = function(includeInstance, msg) {
+  var f, obj = {
+    token: jspb.Message.getFieldWithDefault(msg, 1, "")
+  };
+
+  if (includeInstance) {
+    obj.$jspbMessageInstance = msg;
+  }
+  return obj;
+};
+}
+
+
+/**
+ * Deserializes binary data (in protobuf wire format).
+ * @param {jspb.ByteSource} bytes The bytes to deserialize.
+ * @return {!proto.auth.WebsocketConnectionAuthorizationHeader}
+ */
+proto.auth.WebsocketConnectionAuthorizationHeader.deserializeBinary = function(bytes) {
+  var reader = new jspb.BinaryReader(bytes);
+  var msg = new proto.auth.WebsocketConnectionAuthorizationHeader;
+  return proto.auth.WebsocketConnectionAuthorizationHeader.deserializeBinaryFromReader(msg, reader);
+};
+
+
+/**
+ * Deserializes binary data (in protobuf wire format) from the
+ * given reader into the given message object.
+ * @param {!proto.auth.WebsocketConnectionAuthorizationHeader} msg The message object to deserialize into.
+ * @param {!jspb.BinaryReader} reader The BinaryReader to use.
+ * @return {!proto.auth.WebsocketConnectionAuthorizationHeader}
+ */
+proto.auth.WebsocketConnectionAuthorizationHeader.deserializeBinaryFromReader = function(msg, reader) {
+  while (reader.nextField()) {
+    if (reader.isEndGroup()) {
+      break;
+    }
+    var field = reader.getFieldNumber();
+    switch (field) {
+    case 1:
+      var value = /** @type {string} */ (reader.readString());
+      msg.setToken(value);
+      break;
+    default:
+      reader.skipField();
+      break;
+    }
+  }
+  return msg;
+};
+
+
+/**
+ * Serializes the message to binary data (in protobuf wire format).
+ * @return {!Uint8Array}
+ */
+proto.auth.WebsocketConnectionAuthorizationHeader.prototype.serializeBinary = function() {
+  var writer = new jspb.BinaryWriter();
+  proto.auth.WebsocketConnectionAuthorizationHeader.serializeBinaryToWriter(this, writer);
+  return writer.getResultBuffer();
+};
+
+
+/**
+ * Serializes the given message to binary data (in protobuf wire
+ * format), writing to the given BinaryWriter.
+ * @param {!proto.auth.WebsocketConnectionAuthorizationHeader} message
+ * @param {!jspb.BinaryWriter} writer
+ * @suppress {unusedLocalVariables} f is only used for nested messages
+ */
+proto.auth.WebsocketConnectionAuthorizationHeader.serializeBinaryToWriter = function(message, writer) {
+  var f = undefined;
+  f = message.getToken();
+  if (f.length > 0) {
+    writer.writeString(
+      1,
+      f
+    );
+  }
+};
+
+
+/**
+ * optional string token = 1;
+ * @return {string}
+ */
+proto.auth.WebsocketConnectionAuthorizationHeader.prototype.getToken = function() {
+  return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, ""));
+};
+
+
+/**
+ * @param {string} value
+ * @return {!proto.auth.WebsocketConnectionAuthorizationHeader} returns this
+ */
+proto.auth.WebsocketConnectionAuthorizationHeader.prototype.setToken = function(value) {
+  return jspb.Message.setProto3StringField(this, 1, value);
+};
+
+
+
+
+
+if (jspb.Message.GENERATE_TO_OBJECT) {
+/**
+ * Creates an object representation of this proto.
+ * Field names that are reserved in JavaScript and will be renamed to pb_name.
+ * Optional fields that are not set will be set to undefined.
+ * To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
+ * For the list of reserved names please see:
+ *     net/proto2/compiler/js/internal/generator.cc#kKeyword.
+ * @param {boolean=} opt_includeInstance Deprecated. whether to include the
+ *     JSPB instance for transitional soy proto support:
+ *     http://goto/soy-param-migration
+ * @return {!Object}
+ */
+proto.auth.ConnectionValidationResponse.prototype.toObject = function(opt_includeInstance) {
+  return proto.auth.ConnectionValidationResponse.toObject(opt_includeInstance, this);
+};
+
+
+/**
+ * Static version of the {@see toObject} method.
+ * @param {boolean|undefined} includeInstance Deprecated. Whether to include
+ *     the JSPB instance for transitional soy proto support:
+ *     http://goto/soy-param-migration
+ * @param {!proto.auth.ConnectionValidationResponse} msg The msg instance to transform.
+ * @return {!Object}
+ * @suppress {unusedLocalVariables} f is only used for nested messages
+ */
+proto.auth.ConnectionValidationResponse.toObject = function(includeInstance, msg) {
+  var f, obj = {
+    crossServiceToken: jspb.Message.getFieldWithDefault(msg, 1, "")
+  };
+
+  if (includeInstance) {
+    obj.$jspbMessageInstance = msg;
+  }
+  return obj;
+};
+}
+
+
+/**
+ * Deserializes binary data (in protobuf wire format).
+ * @param {jspb.ByteSource} bytes The bytes to deserialize.
+ * @return {!proto.auth.ConnectionValidationResponse}
+ */
+proto.auth.ConnectionValidationResponse.deserializeBinary = function(bytes) {
+  var reader = new jspb.BinaryReader(bytes);
+  var msg = new proto.auth.ConnectionValidationResponse;
+  return proto.auth.ConnectionValidationResponse.deserializeBinaryFromReader(msg, reader);
+};
+
+
+/**
+ * Deserializes binary data (in protobuf wire format) from the
+ * given reader into the given message object.
+ * @param {!proto.auth.ConnectionValidationResponse} msg The message object to deserialize into.
+ * @param {!jspb.BinaryReader} reader The BinaryReader to use.
+ * @return {!proto.auth.ConnectionValidationResponse}
+ */
+proto.auth.ConnectionValidationResponse.deserializeBinaryFromReader = function(msg, reader) {
+  while (reader.nextField()) {
+    if (reader.isEndGroup()) {
+      break;
+    }
+    var field = reader.getFieldNumber();
+    switch (field) {
+    case 1:
+      var value = /** @type {string} */ (reader.readString());
+      msg.setCrossServiceToken(value);
+      break;
+    default:
+      reader.skipField();
+      break;
+    }
+  }
+  return msg;
+};
+
+
+/**
+ * Serializes the message to binary data (in protobuf wire format).
+ * @return {!Uint8Array}
+ */
+proto.auth.ConnectionValidationResponse.prototype.serializeBinary = function() {
+  var writer = new jspb.BinaryWriter();
+  proto.auth.ConnectionValidationResponse.serializeBinaryToWriter(this, writer);
+  return writer.getResultBuffer();
+};
+
+
+/**
+ * Serializes the given message to binary data (in protobuf wire
+ * format), writing to the given BinaryWriter.
+ * @param {!proto.auth.ConnectionValidationResponse} message
+ * @param {!jspb.BinaryWriter} writer
+ * @suppress {unusedLocalVariables} f is only used for nested messages
+ */
+proto.auth.ConnectionValidationResponse.serializeBinaryToWriter = function(message, writer) {
+  var f = undefined;
+  f = message.getCrossServiceToken();
+  if (f.length > 0) {
+    writer.writeString(
+      1,
+      f
+    );
+  }
+};
+
+
+/**
+ * optional string cross_service_token = 1;
+ * @return {string}
+ */
+proto.auth.ConnectionValidationResponse.prototype.getCrossServiceToken = function() {
+  return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, ""));
+};
+
+
+/**
+ * @param {string} value
+ * @return {!proto.auth.ConnectionValidationResponse} returns this
+ */
+proto.auth.ConnectionValidationResponse.prototype.setCrossServiceToken = function(value) {
+  return jspb.Message.setProto3StringField(this, 1, value);
+};
+
+
 goog.object.extend(exports, proto.auth);

+ 10 - 1
packages/grpc/proto/auth.proto

@@ -10,6 +10,15 @@ message SessionValidationResponse {
   string cross_service_token = 1;
 }
 
-service Sessions {
+message WebsocketConnectionAuthorizationHeader {
+  string token = 1;
+}
+
+message ConnectionValidationResponse {
+  string cross_service_token = 1;
+}
+
+service Auth {
   rpc validate(AuthorizationHeader) returns (SessionValidationResponse) {}
+  rpc validateWebsocket(WebsocketConnectionAuthorizationHeader) returns (ConnectionValidationResponse) {}
 }

+ 1 - 1
packages/websockets/src/Domain/UseCase/SendMessageToClient/SendMessageToClient.spec.ts

@@ -121,7 +121,7 @@ describe('SendMessageToClient', () => {
       message: 'message',
     })
 
-    expect(result.isFailed()).toBe(true)
+    expect(result.isFailed()).toBe(false)
     expect(webSocketsConnectionRepository.removeConnection).toHaveBeenCalledTimes(1)
   })
 })

+ 3 - 1
packages/websockets/src/Domain/UseCase/SendMessageToClient/SendMessageToClient.ts

@@ -49,9 +49,11 @@ export class SendMessageToClient implements UseCaseInterface<void> {
         }
       } catch (error) {
         if (error instanceof GoneException) {
-          this.logger.info(`Connection ${connection.props.connectionId} for user ${userUuid.value} is gone. Removing.`)
+          this.logger.debug(`Connection ${connection.props.connectionId} for user ${userUuid.value} is gone. Removing.`)
 
           await this.removeGoneConnection(connection.props.connectionId)
+
+          return Result.ok()
         }
 
         return Result.fail(