diff --git a/packages/api-gateway/bin/server.ts b/packages/api-gateway/bin/server.ts index ae748bb55..d2c0ed5bf 100644 --- a/packages/api-gateway/bin/server.ts +++ b/packages/api-gateway/bin/server.ts @@ -91,9 +91,12 @@ void container.load().then((container) => { server.setErrorConfig((app) => { app.use((error: Record, 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'] diff --git a/packages/api-gateway/src/Bootstrap/Container.ts b/packages/api-gateway/src/Bootstrap/Container.ts index 6dc5fba07..51a115ae9 100644 --- a/packages/api-gateway/src/Bootstrap/Container.ts +++ b/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(TYPES.ApiGateway_IS_CONFIGURED_FOR_HOME_SERVER_OR_SELF_HOSTING) @@ -122,7 +117,6 @@ export class ContainerConfigLoader { container .bind(TYPES.ApiGateway_OptionalCrossServiceTokenMiddleware) .to(OptionalCrossServiceTokenMiddleware) - container.bind(TYPES.ApiGateway_WebSocketAuthMiddleware).to(WebSocketAuthMiddleware) container .bind(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(TYPES.ApiGateway_GRPCSessionsClient).toConstantValue( - new SessionsClient( + container.bind(TYPES.ApiGateway_GRPCAuthClient).toConstantValue( + new AuthClient( container.get(TYPES.ApiGateway_AUTH_SERVER_GRPC_URL), grpc.credentials.createInsecure(), { @@ -229,7 +222,7 @@ export class ContainerConfigLoader { container.get(TYPES.ApiGateway_CrossServiceTokenCache), container.get(TYPES.ApiGateway_Logger), container.get(TYPES.ApiGateway_Timer), - container.get(TYPES.ApiGateway_GRPCSessionsClient), + container.get(TYPES.ApiGateway_GRPCAuthClient), container.get(TYPES.ApiGateway_GRPCSyncingServerServiceProxy), ), ) @@ -238,6 +231,20 @@ export class ContainerConfigLoader { } } + if (isConfiguredForGRPCProxy) { + container + .bind(TYPES.ApiGateway_WebSocketAuthMiddleware) + .toConstantValue( + new GRPCWebSocketAuthMiddleware( + container.get(TYPES.ApiGateway_GRPCAuthClient), + container.get(TYPES.ApiGateway_AUTH_JWT_SECRET), + container.get(TYPES.ApiGateway_Logger), + ), + ) + } else { + container.bind(TYPES.ApiGateway_WebSocketAuthMiddleware).to(WebSocketAuthMiddleware) + } + logger.debug('Configuration complete') return container diff --git a/packages/api-gateway/src/Bootstrap/Types.ts b/packages/api-gateway/src/Bootstrap/Types.ts index 16b02e985..704b7ba0f 100644 --- a/packages/api-gateway/src/Bootstrap/Types.ts +++ b/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'), } diff --git a/packages/api-gateway/src/Controller/GRPCWebSocketAuthMiddleware.ts b/packages/api-gateway/src/Controller/GRPCWebSocketAuthMiddleware.ts new file mode 100644 index 000000000..47294b8c9 --- /dev/null +++ b/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 { + 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 + data: Record + } + + 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 = 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() + } +} diff --git a/packages/api-gateway/src/Service/gRPC/GRPCServiceProxy.ts b/packages/api-gateway/src/Service/gRPC/GRPCServiceProxy.ts index 48cafa2c4..38d9cf0d8 100644 --- a/packages/api-gateway/src/Service/gRPC/GRPCServiceProxy.ts +++ b/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) => { diff --git a/packages/api-gateway/src/Service/gRPC/GRPCSyncingServerServiceProxy.ts b/packages/api-gateway/src/Service/gRPC/GRPCSyncingServerServiceProxy.ts index 47c39a801..084eefe7e 100644 --- a/packages/api-gateway/src/Service/gRPC/GRPCSyncingServerServiceProxy.ts +++ b/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).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)}`, ) } diff --git a/packages/auth/bin/server.ts b/packages/auth/bin/server.ts index 89506b2bb..58c62dbbd 100644 --- a/packages/auth/bin/server.ts +++ b/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(TYPES.Auth_AuthenticateRequest), container.get(TYPES.Auth_CreateCrossServiceToken), + container.get>(TYPES.Auth_WebSocketConnectionTokenDecoder), container.get(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) { diff --git a/packages/auth/src/Infra/gRPC/SessionsServer.ts b/packages/auth/src/Infra/gRPC/AuthServer.ts similarity index 55% rename from packages/auth/src/Infra/gRPC/SessionsServer.ts rename to packages/auth/src/Infra/gRPC/AuthServer.ts index dc19449c3..e04c30471 100644 --- a/packages/auth/src/Infra/gRPC/SessionsServer.ts +++ b/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, private logger: Logger, ) {} + async validateWebsocket( + call: grpc.ServerUnaryCall, + callback: grpc.sendUnaryData, + ): Promise { + 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, callback: grpc.sendUnaryData, diff --git a/packages/grpc/lib/auth_grpc_pb.d.ts b/packages/grpc/lib/auth_grpc_pb.d.ts index b5536b59b..d525d9520 100644 --- a/packages/grpc/lib/auth_grpc_pb.d.ts +++ b/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 { - validate: ISessionsService_Ivalidate; +interface IAuthService extends grpc.ServiceDefinition { + validate: IAuthService_Ivalidate; + validateWebsocket: IAuthService_IvalidateWebsocket; } -interface ISessionsService_Ivalidate extends grpc.MethodDefinition { - path: "/auth.Sessions/validate"; +interface IAuthService_Ivalidate extends grpc.MethodDefinition { + path: "/auth.Auth/validate"; requestStream: false; responseStream: false; requestSerialize: grpc.serialize; @@ -20,22 +21,38 @@ interface ISessionsService_Ivalidate extends grpc.MethodDefinition; responseDeserialize: grpc.deserialize; } - -export const SessionsService: ISessionsService; - -export interface ISessionsServer { - validate: grpc.handleUnaryCall; +interface IAuthService_IvalidateWebsocket extends grpc.MethodDefinition { + path: "/auth.Auth/validateWebsocket"; + requestStream: false; + responseStream: false; + requestSerialize: grpc.serialize; + requestDeserialize: grpc.deserialize; + responseSerialize: grpc.serialize; + responseDeserialize: grpc.deserialize; } -export interface ISessionsClient { +export const AuthService: IAuthService; + +export interface IAuthServer { + validate: grpc.handleUnaryCall; + validateWebsocket: grpc.handleUnaryCall; +} + +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, 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, 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, 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, callback: (error: grpc.ServiceError | null, response: auth_pb.ConnectionValidationResponse) => void): grpc.ClientUnaryCall; } diff --git a/packages/grpc/lib/auth_grpc_pb.js b/packages/grpc/lib/auth_grpc_pb.js index 93fcf10ab..a56d9ea4f 100644 --- a/packages/grpc/lib/auth_grpc_pb.js +++ b/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()); +} -var SessionsService = exports.SessionsService = { +function deserialize_auth_WebsocketConnectionAuthorizationHeader(buffer_arg) { + return auth_pb.WebsocketConnectionAuthorizationHeader.deserializeBinary(new Uint8Array(buffer_arg)); +} + + +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); diff --git a/packages/grpc/lib/auth_pb.d.ts b/packages/grpc/lib/auth_pb.d.ts index 92e8e4234..7bb7b0f9b 100644 --- a/packages/grpc/lib/auth_pb.d.ts +++ b/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}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + 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}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + 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, + } +} diff --git a/packages/grpc/lib/auth_pb.js b/packages/grpc/lib/auth_pb.js index edf7e6b84..1e34c4e2b 100644 --- a/packages/grpc/lib/auth_pb.js +++ b/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_, 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_, 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); diff --git a/packages/grpc/proto/auth.proto b/packages/grpc/proto/auth.proto index f01ee4db2..6ee183dfd 100644 --- a/packages/grpc/proto/auth.proto +++ b/packages/grpc/proto/auth.proto @@ -10,6 +10,15 @@ message SessionValidationResponse { string cross_service_token = 1; } -service Sessions { - rpc validate(AuthorizationHeader) returns (SessionValidationResponse) {} +message WebsocketConnectionAuthorizationHeader { + string token = 1; +} + +message ConnectionValidationResponse { + string cross_service_token = 1; +} + +service Auth { + rpc validate(AuthorizationHeader) returns (SessionValidationResponse) {} + rpc validateWebsocket(WebsocketConnectionAuthorizationHeader) returns (ConnectionValidationResponse) {} } diff --git a/packages/websockets/src/Domain/UseCase/SendMessageToClient/SendMessageToClient.spec.ts b/packages/websockets/src/Domain/UseCase/SendMessageToClient/SendMessageToClient.spec.ts index db9b4d84e..abc90dcde 100644 --- a/packages/websockets/src/Domain/UseCase/SendMessageToClient/SendMessageToClient.spec.ts +++ b/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) }) }) diff --git a/packages/websockets/src/Domain/UseCase/SendMessageToClient/SendMessageToClient.ts b/packages/websockets/src/Domain/UseCase/SendMessageToClient/SendMessageToClient.ts index 440a5b909..cbe2d8f55 100644 --- a/packages/websockets/src/Domain/UseCase/SendMessageToClient/SendMessageToClient.ts +++ b/packages/websockets/src/Domain/UseCase/SendMessageToClient/SendMessageToClient.ts @@ -49,9 +49,11 @@ export class SendMessageToClient implements UseCaseInterface { } } 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(