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
This commit is contained in:
Karol Sójko 2023-12-07 12:58:07 +01:00 committed by GitHub
parent 4600a49e88
commit d5c1b76de0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 658 additions and 51 deletions

View file

@ -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']

View file

@ -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

View file

@ -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'),
}

View file

@ -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()
}
}

View file

@ -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) => {

View file

@ -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)}`,
)
}

View file

@ -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) {

View file

@ -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>,

View file

@ -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>;
}
export const SessionsService: ISessionsService;
export interface ISessionsServer {
validate: grpc.handleUnaryCall<auth_pb.AuthorizationHeader, 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 interface ISessionsClient {
export const AuthService: IAuthService;
export interface IAuthServer {
validate: grpc.handleUnaryCall<auth_pb.AuthorizationHeader, auth_pb.SessionValidationResponse>;
validateWebsocket: grpc.handleUnaryCall<auth_pb.WebsocketConnectionAuthorizationHeader, auth_pb.ConnectionValidationResponse>;
}
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;
}

View file

@ -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);

View file

@ -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,
}
}

View file

@ -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);

View file

@ -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) {}
}

View file

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

View file

@ -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(