What is the best pattern for API Gateway Service and Authentication Service in microservice design - microservices

I am building a microservice project, with two of the services are:
API Gateway: for routing to suitable service.
Authentication service: for authenticating user credentials/user's tokens.
The Authentication service is built by NestJS with TCP protocol.
Forcase login:
HTTP request from client to API Gateway. API Gateway send to Authentication service const oAuthenticatedUser: LoginUserResponseDto = await this.authMicroServiceClient.send('login', loginRequest).toPromise() for validating the email and password. It will return UserInfo (name, access_token and regresh_token) if user's credentials are correct.
For case: create-post
HTTP request from client to API Gateway with access_token in header. Before calling to post-service for executing post-creation, API Gateway call Authentication service for verifying the token by const authenReponse = await this.authMicroServiceClient.send('verify_access_token', { token: access_token }).toPromise();
My pain point: I cannot use Passport strategy in Authentication service for implementing common verifying token. Because the request to Authentication Service is now not a normal HTTP request. Then these current code are not able to use:
import { PassportStrategy } from '#nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { Injectable, UnauthorizedException } from '#nestjs/common';
import { TJwtPayload } from '../../types/auth/jwt-payload.type';
import { UserSimpleDTO } from '#modules/user/dto';
import { ConfigService } from '#nestjs/config'
import { TokenService } from '#app/shared/services'
/**
* Powered Thuan
* #author thuan.nguyen
* #namespace auth
* #classname JwtAccessTokenStrategy
**/
#Injectable()
export class JwtAccessTokenStrategy extends PassportStrategy(Strategy, 'jwt-access-token') {
constructor(private readonly tokenService: TokenService, configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromExtractors([
ExtractJwt.fromAuthHeaderAsBearerToken(),
]),
ignoreExpiration: true,
secretOrKey: configService.get("JWT_ACCESS_SECRET"),
});
}
async validate(payload: TJwtPayload): Promise<UserSimpleDTO> {
const user = await this.tokenService.validatePayload(payload);
if (!user) {
throw new UnauthorizedException('Cannot find user via payload');
}
return user;
}
}
And
import { Strategy } from 'passport-local';
import { PassportStrategy } from '#nestjs/passport';
import { Injectable, UnauthorizedException } from '#nestjs/common';
import { UserService } from '../user/user.service';
import { UserLoginReqDTO } from '#modules/user/dto';
/**
* Powered by Thuan
* #author thuan.nguyen
* #namespace auth
* #classname LocalStrategy
**/
#Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(
private userService: UserService,
) {
super({ usernameField: 'email' });
}
async validate(email: string, password: string): Promise<UserLoginReqDTO> {
const user: UserLoginReqDTO = await this.userService.getUserIfPasswordMatches(email, password);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
My question is: My design is a good one or not. I would like to know the better design. And How to use above code with requests transfered by TCP protocol.
Thank you!

Related

NestJS Dependency Injection in Websocket Adapter

I'm trying to authenticate and check permission of a user while establishing a websocket connection in a NestJS application.
I've found this discussion which recommends to make use of NestJS Websocket adapter. You can perform the token validation in the options.allowRequest callback as below.
export class AuthenticatedSocketIoAdapter extends IoAdapter {
private readonly authService: AuthService;
constructor(private app: INestApplicationContext) {
super(app);
this.authService = app.get(AuthService);
}
createIOServer(port: number, options?: SocketIO.ServerOptions): any {
options.allowRequest = async (request, allowFunction) => {
const token = request.headers.authorization.replace('Bearer ', '');
const verified = this.authService.verifyToken(token);
if (verified) {
return allowFunction(null, true);
}
return allowFunction('Unauthorized', false);
};
return super.createIOServer(port, options);
}
}
I have a problem however with the dependency injection in the websocket adapter. The IoAdapter's constructor has an INestApplicationContext parameter from which I'm trying to get back the AuthService using app.get(AuthService) as you can see above.
The AuthService injects two other services, a UserService and the JwtService to check the JWT token. My problem is that those services remained not defined in that context.
#Injectable()
export class AuthService {
constructor(private usersService: UsersService, private jwtService: JwtService) {}
verifyToken(token: string): boolean {
// Problem: this.jwtService is undefined
const user = this.jwtService.verify(token, { publicKey });
// ... check user has permissions and return result
}
For info, the AuthService is in another module than the one which defines the Websocket. I also tried to import the AuthService (and its dependencies) in the current module but that didn't help.
Is that possible to make use the service using the app.get() method?
I could solve the DI issue by using app.resolve() instead of app.get()
export class AuthenticatedSocketIoAdapter extends IoAdapter {
private authService: AuthService;
constructor(private app: INestApplicationContext) {
super(app);
app.resolve<AuthService>(AuthService).then((authService) => {
this.authService = authService;
});
}
}
This solved the jwtService injected in the AuthService being undefined.

calling blocking feign client from reactive spring service

I am trying to call generated feign client from reactive spring flux like this:
.doOnNext(user1 -> {
ResponseEntity<Void> response = recorderClient.createUserProfile(new UserProfileDto().principal(user1.getLogin()));
if (!response.getStatusCode().equals(HttpStatus.OK)) {
log.error("recorder backend could not create user profile for user: {} ", user1.getLogin());
throw new RuntimeException("recorder backend could not create user profile for login name" + user1.getLogin());
}
})
Call is executed, but when I try to retrieve jwt token from reactive security context ( in a requets interceptor ) like this:
public static Mono<String> getCurrentUserJWT() {
return ReactiveSecurityContextHolder
.getContext()
.map(SecurityContext::getAuthentication)
.filter(authentication -> authentication.getCredentials() instanceof String)
.map(authentication -> (String) authentication.getCredentials());
}
....
SecurityUtils.getCurrentUserJWT().blockOptional().ifPresent(s -> template.header(AUTHORIZATION_HEADER, String.format("%s %s", BEARER, s)));
context is empty. As I am pretty new to reactive spring I am surely mussing something stupid and important.
Not sure how is your interceptor configured,
but in my case, i just simply implement ReactiveHttpRequestInterceptor and override apply() function
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.stereotype.Component;
import reactivefeign.client.ReactiveHttpRequest;
import reactivefeign.client.ReactiveHttpRequestInterceptor;
import reactor.core.publisher.Mono;
import java.util.Collections;
#Component
public class UserFeignClientInterceptor implements ReactiveHttpRequestInterceptor {
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String BEARER = "Bearer";
#Override
public Mono<ReactiveHttpRequest> apply(ReactiveHttpRequest reactiveHttpRequest) {
return SecurityUtils.getCurrentUserJWT()
.flatMap(s -> {
reactiveHttpRequest.headers().put(AUTHORIZATION_HEADER, Collections.singletonList(String.format("%s %s", BEARER, s)));
return Mono.just(reactiveHttpRequest);
});
}
}

GET request to API request give status code of 502. How can I resolve this?

I am building an application with nestjs framework. I have created controller and service to fetch data from Http endpoint. Instead of getting the json data I am getting
Error: Request failed with status code 502
at createError (node_modules/axios/lib/core/createError.js:16:15)
at settle (node_modules/axios/lib/core/settle.js:17:12)
at IncomingMessage.handleStreamEnd (node_modules/axios/lib/adapters/http.js:237:11)
at IncomingMessage.emit (events.js:203:15)
at endReadableNT (_stream_readable.js:1145:12)
at process._tickCallback (internal/process/next_tick.js:63:19) in the output. The endpoint is working fine on Google Chrome. I have integrated my application with Swagger UI. What changes should I make in my code so that I can get data from endpoint?
Here is my service.ts
import { Injectable, HttpService } from '#nestjs/common';
import { map } from 'rxjs/operators';
#Injectable()
export class MessageService {
constructor(private readonly httpService: HttpService) {}
configEndPoint: string = 'https://jsonplaceholder.typicode.com/todos/1';
getData(source: string, productCode: string, vehicleType: string) {
return this.httpService
.get(this.configEndPoint)
.pipe(map(response => response.data.json()));
}
}
Here is my controller.ts
import { Controller, Post, Body, Get } from '#nestjs/common';
import {
ApiImplicitHeader,
ApiOperation,
ApiResponse,
ApiUseTags,
} from '#nestjs/swagger';
import { ProductEvent } from '../dto/product-event';
import { MessageService } from '../service/message/message-service';
#Controller('/service/api/message')
export class MessageController {
source: string;
productCode: string;
vehicleType: string;
constructor(private messageService: MessageService) {}
#Post()
#ApiUseTags('processor-dispatcher')
#ApiOperation({ title: 'Generate product message for the SNS topics' })
async generateMessage(#Body() productEvent: ProductEvent) {
return JSON.stringify(
this.messageService.getData(
this.source,
this.productCode,
this.vehicleType,
),
);
}
}

How to access websocket from controller or another component/services?

I have a REST API, I want to send event to the client via websocket.
How to inject websocket instance in controller or another component?
Better solution is to create global module. You can then emit events from any other module/controller. A. Afir approach will create multiple instances of Gateway if you try to use it in other modules.
Note: This is just simplest solution
Create socket.module.ts
import { Module, Global } from '#nestjs/common';
import { SocketService } from './socket.service';
#Global()
#Module({
controllers: [],
providers: [SocketService],
exports: [SocketService],
})
export class SocketModule {}
socket.service.ts
import { Injectable } from '#nestjs/common';
import { Server } from 'socket.io';
#Injectable()
export class SocketService {
public socket: Server = null;
}
app.gateway.ts see afterInit function
import { WebSocketGateway, OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect, WebSocketServer } from '#nestjs/websockets';
import { Logger } from '#nestjs/common';
import { Server, Socket } from 'socket.io';
import { SocketService } from './socket/socket.service';
#WebSocketGateway()
export class AppGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
constructor(private socketService: SocketService){
}
#WebSocketServer() public server: Server;
private logger: Logger = new Logger('AppGateway');
afterInit(server: Server) {
this.socketService.socket = server;
}
handleDisconnect(client: Socket) {
this.logger.log(`Client disconnected: ${client.id}`);
}
handleConnection(client: Socket, ...args: any[]) {
this.logger.log(`Client connected: ${client.id}`);
}
}
Then import SocketModule into AppModule and you can use Socket service everywhere.
class Gateway can be injected in another component, and use the server instance.
#Controller()
export class AppController {
constructor(
private readonly appService: AppService,
private readonly messageGateway: MessageGateway
) {}
#Get()
async getHello() {
this.messageGateway.server.emit('messages', 'Hello from REST API');
return this.appService.getHello();
}
}
I suppose that #Raold missed a fact in the documentation:
Gateways should not use request-scoped providers because they must act as singletons. Each gateway encapsulates a real socket and cannot be instantiated multiple times.
So it means that we can neither instantiate the gateway class multiple times nor do it explicitly using injection scopes features.
So creating just only one gateway for one namespaces will be right and it will produce only one instance of the websocket or socket.io server.

OpenShift: Cannot Connect to WebSocket with Alias (bug)

I have a Java Spring Web application which uses WebSockets. An HTML file connects to the WebSocket using the uri:
var wsUri = "wss://" + document.location.hostname + ":8443" + "/serverendpoint";
Here is my serverendpoint.java code that creates the WebSocket:
package com.myapp.spring.web.controller;
import java.io.IOException;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import org.springframework.web.socket.server.standard.SpringConfigurator;
#ServerEndpoint(value="/serverendpoint", configurator = SpringConfigurator.class)
public class serverendpoint {
#OnOpen
public void handleOpen () {
System.out.println("JAVA: Client is now connected...");
}
#OnMessage
public String handleMessage (Session session, String message) throws IOException {
if (message.equals("ping")) {
// return "pong"
session.getBasicRemote().sendText("pong");
}
else if (message.equals("close")) {
handleClose();
return null;
}
System.out.println("JAVA: Received from client: "+ message);
MyClass mc = new MyClass(message);
String res = mc.action();
session.getBasicRemote().sendText(res);
return res;
}
#OnClose
public void handleClose() {
System.out.println("JAVA: Client is now disconnected...");
}
#OnError
public void handleError (Throwable t) {
t.printStackTrace();
}
}
When I connect to the websocket using the http://myapp-myproject.rhcloud.com/mt URL, the WebSocket connects. However, when I set up an alias to the http://myapp-myproject.rhcloud.com, which is called https://someurl.com/mt, the websocket doesn't connect. Why is this? I get the following error message in Google Chrome:
Furthermore, the websocket uses a wss connection at port 8443. This is a secure request equivalent to https. Therefore, how can it work with the http://myapp-myproject.rhcloud.com/mt URL which is an http URL, and why is it not connecting with the alias?
Thank you so much for your help!

Resources