NestJS - Pass user data from custom auth guard to resolvers - graphql

I know this question gets asked frequently for the default passport AuthGuard('yourStrategy'),
but haven't found the answer for custom auth guards yet.
Why I use a custom auth guard? Because the default one and GraphQL seems to be unable to work together.
Since some update on GraphQL's side, the default AuthGuard cannot read the header any more.
I need to pass the user data, which I got from the bearer token, somehow to the resolvers.
How is passport doing this? How would you do this? I'm pretty new to nestJS and the lack of dockumentation and / or propper tutorials drives me crazy.
Relevant code:
auth.guard.ts
#Injectable()
export class AuthGuard implements CanActivate {
constructor(readonly jwtService: JwtService/*, readonly userService: UsersService*/) { }
canActivate(context: ExecutionContext): boolean {
const ctx = GqlExecutionContext.create(context);
const request = ctx.getContext().request;
const Authorization = request.get('Authorization');
if (Authorization) {
const token = Authorization.replace('Bearer ', '');
const { userId, firstName } = this.jwtService.verify(token) as { userId: string; firstName: string } ;
return !!userId;
}
}
}
jwt.strategy.ts
#Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: jwtConstants.secret,
});
}
async validate(payload) {
return {userId: payload.userId, firstName: payload.firstName};
}
}
auth.module.ts
#Module({
imports: [
forwardRef(() => UserModule) ,
PassportModule.register({
defaultStrategy: 'jwt'
}),
JwtModule.register({
secret: jwtConstants.secret,
signOptions: {expiresIn: 3600}
})],
providers: [AuthService, JwtStrategy, AuthResolver, AuthGuard],
exports: [AuthService, JwtModule, AuthGuard]
})
export class AuthModule {
}
example resolver
#UseGuards(AuthGuard)
#Resolver((of) => UserSchema)
export class UserResolver {
constructor(private readonly userService: UserService) {}
// ===========================================================================
// Queries
// ===========================================================================
#Query(() => UserDto, {description: 'Searchs for a user by a given id'})
async getUserById(#Args('id') id: string) {
/*
* Instead of passing the userID as an query argument, get it from the bearer token / auth guard!
*/
const result = await this.userService.findById(id);
if(result) return result;
return new NotFoundException('User not found!');
}
}
Thanks for help in advance! ;-)
Edit: In case you need to see more code, you could use my github repo: https://github.com/JensUweB/ExamAdmin-Backend

Never mind. I have found a solution to this myself. I found a workaround to get the passport AuthGuard back to work with GraphQL. And for the userId: use a custom User Decorator: github.com/nestjs/graphql/issues/48#issuecomment-420693225

Related

graphql ExecutionContext not recognized by nest-keycloak-connect

I would like to authorize users in GraphQL in a NestJs project. I use nest-keycloak-connect for this.
Unfortunately, when calling query with "Authorization" set in Headers, I get the error: [Keycloak] Empty JWT, unauthorized.
So it looks like nest-keycloak-connect doesn't recognize that context comes from graphql.
However, when looking at the source code of nest-keycloak-connect, context is checked for http and graphql there.
So what should I do to make nest-keycloak-connect start using graphql correctly?
nest-keycloak-connect context type checking
export const extractRequest = (context: ExecutionContext): [any, any] => {
let request: any, response: any;
// Check if request is coming from graphql or http
if (context.getType() === 'http') {
// http request
const httpContext = context.switchToHttp();
request = httpContext.getRequest();
response = httpContext.getResponse();
} else if (context.getType<GqlContextType>() === 'graphql') {
let gql: any;
// Check if graphql is installed
try {
gql = require('#nestjs/graphql');
} catch (er) {
throw new Error('#nestjs/graphql is not installed, cannot proceed');
}
// graphql request
const gqlContext = gql.GqlExecutionContext.create(context).getContext();
request = gqlContext.req;
response = gqlContext.res;
}
return [request, response];
};
my auth settings
#Module({
imports: [
MongooseModule.forRoot('mongodb://localhost/Products'),
KeycloakConnectModule.register({
authServerUrl: 'http://localhost:8080/auth',
realm: 'users',
clientId: 'users-service',
secret: 'h1xAJnShNwPmxzySR8Y0d3fLh27iwPPh',
policyEnforcement: PolicyEnforcementMode.PERMISSIVE, // optional
tokenValidation: TokenValidation.ONLINE, // optional
}),
GraphQLModule.forRoot<ApolloFederationDriverConfig>({
driver: ApolloFederationDriver,
autoSchemaFile: true,
}),
ProductModule,
ProductImageModule,
ProductAttributeModule,
],
providers: [
{
provide: APP_GUARD,
useClass: AuthGuard,
},
{
provide: APP_GUARD,
useClass: ResourceGuard,
},
{
provide: APP_GUARD,
useClass: RoleGuard,
},
],
controllers: [],
})
export class AppModule {}
Resolver
//FindAll
#Query(() => [Product])
#Roles({roles: ['user']})
async products() {
const products = await this.productService.findAll();
return products;
}
Header
{
"Authorization":"eyJhbGciOiJSUzI1N..."
}
Solved by adding Bearer to the header
{
"authorization":"Bearer eyJhbGciOiJSUzI1NiIsInR..."
}

Why is ValidateNested fields not working in e2e tests?

I have the following NestJS controller:
class PhoneTestDto {
#IsNotEmpty()
#IsPhoneNumber()
phone: string
}
class TestDto {
#IsNotEmpty()
#Type(() => PhoneTestDto)
#ValidateNested({ always: true, each: true })
#ArrayNotEmpty()
phones: PhoneTestDto[]
}
#Controller('v1/user')
export class UserController {
constructor(private readonly userService: UserService) {}
#Post()
async addUser(#Body() body: TestDto): Promise<LoginResponseDto> {
console.log("valid")
return
}
}
When I run the app and send through Postman a request such as:
{
"phones": [{}]
}
I get a correct response (phones.0.phone must be a valid phone number...)
When I try to run an e2e test, it passes without validating the phone.
This is my test file:
describe('Test', () => {
let app: INestApplication
beforeEach(async () => {
jest.resetModules()
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
}).compile()
app = moduleRef.createNestApplication()
app.useGlobalPipes(new ValidationPipe({transform: true}))
await app.init()
})
it('test1', async () => {
const response = await request(app.getHttpServer())
.post('/v1/user')
.send({phones: [{}]})
expect(response['res']['statusCode']).toEqual(201)
})
})
Sending {} or {[]} does produce a validation error so validation is generally working, but not the validation of nested fields in the context of testing.
ALSO: Removing jest.resetModules() brings correct behavior.
I'm not sure how jest.resetModules() relate to nestjs validation, and how should I use if I do need to reset modules.

How to validate a DTO fields?

I have an endpoint with no entry params:
async myendpoint(): Promise<any> {
const customer = await this.customerService.findOne(1);
if (customer) {
return await this.customerService.mapToDestination(customer);
}...
}
Then I have my method mapToDestination where I simply assign vars:
async mapToDestination(customer: Customer): Promise<DestinationDto> {
const destination: DestinationDto = {
lastname: customer.lastname,
firstname: customer.firstname,...
Finally, I have my DTO:
import {IsEmail, IsNotEmpty, IsOptional, IsNumber, IsBoolean, IsString, IsDate, MaxLength, Length, NotEquals} from 'class-validator';
import {ApiProperty} from '#nestjs/swagger';
export class DestinationDto {
#IsString()
#IsNotEmpty()
#MaxLength(32)
lastname: string;
#IsString()
#IsNotEmpty()
#MaxLength(20)
firstname: string; ...
I would like my DTO fields to be validated automatically following the decorators when I'm mapping it in my mapToDestination() method. I look through the web and the official documentation and I gave a try to Validators (ValidationPipe) but it does not seem to be my need as it validates the endpoint entry params.
Please, could you explain to me how to achieve this automatic validation? Thanks in advance.
I won't be "automatic" but you could instantiate your own instance of the validator from class validator and use it against the DTO in your service. Otherwise, it won't ever happen automatically because as you said, the ValidationPipe only works on the entry of the endpoint.
Example
Inside of mapToDestination so long as customer is an instance of DestinationDTO` you can have something like this:
#Injectable()
export class CustomerService {
async mapToDestination(customer: DestinationDTO) {
const errors = await validate(customer);
if (errors) {
throw new BadRequestException('Some Error Message');
}
...
}
...
}

Acceptance testing with memory datasource doesn't work

I created acceptance test of controller that using memory datasource.
Before the test start, I was trying to have clean database and only add 1 user credential to login by calling endpoint since the rest of test require authenticated access with access token.
I can get the user info in getUser after created the user record from givenUser.
However, the login request can't find the credential. It seems that the helpers and client are not sharing the same memory datasource.
I am not sure what configuration setup is wrong in this case.
src/tests/fixtures/datasources/testdb.datasource.ts
import {juggler} from '#loopback/repository';
export const testdb: juggler.DataSource = new juggler.DataSource({
name: 'db',
connector: 'memory',
});
src/datasources/mongodb.datasource.ts
import {inject} from '#loopback/core';
import {juggler} from '#loopback/repository';
import * as config from './mongodb.datasource.json';
export class MongodbDataSource extends juggler.DataSource {
static dataSourceName = 'mongodb';
constructor(
#inject('datasources.config.mongodb', {optional: true})
dsConfig: object = config,
) {
super(dsConfig);
}
}
user.controller.acceptance.ts
import {Client, expect, supertest} from '#loopback/testlab';
import {ApiApplication} from '../..';
import {setupApplication} from './test-helper';
import {givenEmptyDatabase, givenUser,getUser} from '../helpers/database.helpers';
describe('UserController', () => {
let app: ApiApplication;
let client: supertest.SuperTest<supertest.Test>;;
let jwtToken: string;
before('setupApplication', async () => {
({app, client} = await setupApplication());
});
before(givenEmptyDatabase);
before(givenUser);
before(getUser);
before(async () => {
const response = await client
.post('/login')
.send({username: 'user1', password: 'password'});
jwtToken = response.body.token;
});
after(async () => {
await app.stop();
});
it('invokes GET /info without authentication', async () => {
const expectedError = {
error: {
statusCode: 401,
name: 'UnauthorizedError',
message: 'Unauthorized'
}
};
const res = await client.get('/user/info').expect(401);
expect(res.body).to.containEql(expectedError);
});
test-help.ts
import {ApiApplication} from '../..';
import {
createRestAppClient,
givenHttpServerConfig,
Client,
} from '#loopback/testlab';
import {testdb} from '../fixtures/datasources/testdb.datasource';
export async function setupApplication(): Promise<AppWithClient> {
const app = new ApiApplication();
await app.bind('datasources.config.mongodb').to({
name: 'mongodb',
connector: 'memory',
debug: true,
});
await app.boot();
await app.start();
const client = createRestAppClient(app);
return {app, client};
}
export interface AppWithClient {
app: ApiApplication;
client: Client;
}
database.helper.ts
import { UserRepository } from '../../repositories';
import { User } from '../../models';
import { testdb } from '../fixtures/datasources/testdb.datasource';
export async function givenEmptyDatabase() {
let userRepo: UserRepository;
userRepo = new UserRepository(testdb);
userRepo.deleteAll();
}
export async function givenUser() {
let userRepo = new UserRepository(testdb);
const user = {
username: 'user1',
password: 'password',
created_at: new Date('2019-08-08'),
updated_at: new Date('2019-08-08'),
}
await userRepo.create(operator);
}
export async function getUser() {
let userRepo = new UserRepository(testdb);
const users = await userRepo.find();
console.log(users);
}
It seems that the helpers and client are not sharing the same memory datasource.
Exactly.
It's important to realize that you can have multiple datasources using the same connector, for example you can one logDb using one MongoDB server and userDb connecting to another one. The same applies to memory datasources, you can have multiple datasources using the memory connector and each instance will have its own space for data.
Since you are already changing the configuration of your main mongodb datasource in the tests, my recommendation is to use that modified datasource when setting up the initial test data.
In database.helper.ts:
export async function givenEmptyDatabase(db) {
let userRepo: UserRepository;
userRepo = new UserRepository(db);
userRepo.deleteAll();
}
export async function givenUser(db) {
// ...
}
// and so on
In your test:
before(async () => {
const db = await app.get<juggler.DataSource>('datasources.config.mongodb');
await givenEmptyDatabase(db);
});
Personally, I strongly recommend use the same database in your tests as you will use in production. The memory database behaves differently from MongoDB. Certain operations may pass when storing data in memory but fail when MongoDB is used.
You can learn more about testing in LoopBack's "Best Practice" guides: Testing your application

admin-on-rest / restClient : call a resource with no auth

I made a register page that use restClient to send a POST to /users api.
But my problem is that the only way to send a POST is to be logged first as I receive this error log from the restClient :
'Could not find stored JWT and no authentication strategy was given'
Is there a way to desactivate the authentication middleware for a specific api call ?
// registerActions.js
import { CREATE } from 'admin-on-rest'
export const USER_REGISTER = 'AOR/USER_REGISTER'
export const USER_REGISTER_LOADING = 'AOR/USER_REGISTER_LOADING'
export const USER_REGISTER_FAILURE = 'AOR/USER_REGISTER_FAILURE'
export const USER_REGISTER_SUCCESS = 'AOR/USER_REGISTER_SUCCESS'
export const userRegister = (data, basePath) => ({
type: USER_REGISTER,
payload: { data: { email: data.username, ...data } },
meta: { resource: 'users', fetch: CREATE, auth: true },
})
//registerSaga.js
import { put, takeEvery, all } from 'redux-saga/effects'
import { push } from 'react-router-redux'
import { showNotification } from 'admin-on-rest'
import {
USER_REGISTER,
USER_REGISTER_LOADING,
USER_REGISTER_SUCCESS,
USER_REGISTER_FAILURE
} from './registerActions'
function* registerSuccess() {
yield put(showNotification('Register approved'))
yield put(push('/'))
}
function* registerFailure({ error }) {
yield put(showNotification('Error: register not approved', 'warning'))
console.error(error)
}
export default function* commentSaga() {
yield all([
takeEvery(USER_REGISTER_SUCCESS, registerSuccess),
takeEvery(USER_REGISTER_FAILURE, registerFailure),
])
}
You'll probably have to make your own feathers client and explicitly bypass the call to authenticate for this specific request
You can also write a rest wrappper this will intercept the call for this particular case and bypass auth
https://marmelab.com/admin-on-rest/RestClients.html#decorating-your-rest-client-example-of-file-upload
So something like below
const restWrapper = requestHandler => (type, resource, params) => {
import { fetchUtils } from 'admin-on-rest';
if (type === 'CREATE' && resource === 'users') {
return fetchUtils.fetchJson(url, params)
.then((response) => {
const {json} = response;
return { data: json };
})
}
Eliminates the need of rewriting an entire Rest Client when you only want to override the default behaviour for a single case

Resources