I'm trying to follow along with this tutorial and I'm struggling to convert the implementation to GraphQL.
local.strategy.ts
#Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authenticationService: AuthenticationService) {
super();
}
async validate(email: string, password: string): Promise<any> {
const user = await this.authenticationService.getAuthenticatedUser(
email,
password,
);
if (!user) throw new UnauthorizedException();
return user;
}
}
local.guard.ts
#Injectable()
export class LogInWithCredentialsGuard extends AuthGuard('local') {
async canActivate(context: ExecutionContext): Promise<boolean> {
const ctx = GqlExecutionContext.create(context);
const { req } = ctx.getContext();
req.body = ctx.getArgs();
await super.canActivate(new ExecutionContextHost([req]));
await super.logIn(req);
return true;
}
}
authentication.type.ts
#InputType()
export class AuthenticationInput {
#Field()
email: string;
#Field()
password: string;
}
authentication.resolver.ts
#UseGuards(LogInWithCredentialsGuard)
#Mutation(() => User, { nullable: true })
logIn(
#Args('variables')
_authenticationInput: AuthenticationInput,
#Context() req: any,
) {
return req.user;
}
mutation
mutation {
logIn(variables: {
email: "email#email.com",
password: "123123"
} ) {
id
email
}
}
Even the above credentials are correct, I'm receiving an unauthorized error.
The problem is in your LogInWithCredentialsGuard.
You shouldn't override canAcitavte method, all you have to do is update the request with proper GraphQL args because in case of API request, Passport automatically gets your credentials from req.body. With GraphQL, execution context is different, so you have to manually set your args in req.body. For that, getRequest method is used.
As the execution context of GraphQL and REST APIs is not same, you have to make sure your guard works in both cases whether it's controller or mutation.
here is a working code snippet
#Injectable()
export class LogInWithCredentialsGuard extends AuthGuard('local') {
// Override this method so it can be used in graphql
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
const gqlReq = ctx.getContext().req;
if (gqlReq) {
const { variables } = ctx.getArgs();
gqlReq.body = variables;
return gqlReq;
}
return context.switchToHttp().getRequest();
}
}
and your mutation will be like
#UseGuards(LogInWithCredentialsGuard)
#Mutation(() => User, { nullable: true })
logIn(
#Args('variables')
_authenticationInput: AuthenticationInput,
#Context() context: any, // <----------- it's not request
) {
return context.req.user;
}
I've been able to get a successful login with a guard like this:
#Injectable()
export class LocalGqlAuthGuard extends AuthGuard('local') {
constructor() {
super();
}
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
const req = ctx.getContext().req;
req.body = ctx.getArgs();
return req;
}
async canActivate(context: ExecutionContext) {
await super.canActivate(context);
const ctx = GqlExecutionContext.create(context);
const req = ctx.getContext().req;
await super.logIn(req);
return true;
}
}
Related
I've tried to implement an oauth method using GraphQL with Google auth and for some reason I'm getting the following error
"res.setHeader is not a function" from within the authenticate method in Google Strategy
I've used passport-google-oauth20 strategy
this is my google-auth.guard.ts
import { ExecutionContext, Injectable } from '#nestjs/common';
import { GqlExecutionContext } from '#nestjs/graphql';
import { AuthGuard } from '#nestjs/passport';
#Injectable()
export class GoogleAuthGuard extends AuthGuard('google') {
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
const gqlReq = ctx.getContext().req;
if (gqlReq) {
const { token } = ctx.getArgs();
gqlReq.body = { token };
return gqlReq;
}
return context.switchToHttp().getRequest();
}
}
this is my google.strategy.ts
import { PassportStrategy } from '#nestjs/passport';
import { Strategy, VerifyCallback } from 'passport-google-oauth20';
import { Injectable, UnauthorizedException } from '#nestjs/common';
import { Profile } from 'passport';
#Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
constructor() {
super({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_SECRET,
callbackURL: process.env.GOOGLE_REDIRECT_URL,
prompt: 'consent',
scope: ['email', 'profile'],
})
}
async validate(
accessToken: string,
refreshToken: string,
profile: Profile,
done: VerifyCallback,
): Promise<any> {
if (!profile) {
return done(new UnauthorizedException(), false);
}
return done(null, profile);
}
}
it's important to point out that since my app is a react SPA, the callbackURL value is the main page of the client and not another path in the server.
and the resolver which I intend to use to generate a jwt token and a refresh token, but the code never gets to this part due to the error in the strategy
#UseGuards(GoogleAuthGuard)
#Query(() => LoginResponseApi)
async googleLogin(
#Args({ name: 'token', type: () => String }) token: string,
#Req() req,
#Context() context
): Promise<LoginResponseApi> {
const res: Response = context.req.res;
const loginResponse: any = await this.authService.googleLogin(req)
const jwtToken = this.authService.createRefreshToken(loginResponse.user)
if (loginResponse.accessToken)
this.authService.sendRefreshToken(res, jwtToken)
return loginResponse;
}
I struggle to upload .csv file to nestjs-graphql-fastify server. Tried following code:
#Mutation(() => Boolean)
async createUsers(
#Args({ name: 'file', type: () => GraphQLUpload })
{ createReadStream, filename }: FileUpload,
): Promise<boolean> {
try {
// backend logic . . .
} catch {
return false;
}
return true;
}
but all I get when testing with postman is this response:
{
"statusCode": 415,
"code": "FST_ERR_CTP_INVALID_MEDIA_TYPE",
"error": "Unsupported Media Type",
"message": "Unsupported Media Type: multipart/form-data; boundary=--------------------------511769018912715357993837"
}
Developing with code-first approach.
Update: Tried to use fastify-multipart but issue still remains. What has changed is response in postman:
POST body missing, invalid Content-Type, or JSON object has no keys.
Found some answer's on Nestjs discord channel.
You had to do following changes:
main.ts
async function bootstrap() {
const adapter = new FastifyAdapter();
const fastify = adapter.getInstance();
fastify.addContentTypeParser('multipart', (request, done) => {
request.isMultipart = true;
done();
});
fastify.addHook('preValidation', async function (request: any, reply) {
if (!request.raw.isMultipart) {
return;
}
request.body = await processRequest(request.raw, reply.raw);
});
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
adapter,
);
await app.listen(apiServerPort, apiServerHost);
}
bootstrap();
upload.scalar.ts
import { Scalar } from '#nestjs/graphql';
import { GraphQLUpload } from 'graphql-upload';
#Scalar('Upload')
export class UploadGraphQLScalar {
protected parseValue(value) {
return GraphQLUpload.parseValue(value);
}
protected serialize(value) {
return GraphQLUpload.serialize(value);
}
protected parseLiteral(ast) {
return GraphQLUpload.parseLiteral(ast, ast.value);
}
}
users.resolver.ts
#Mutation(() => CreateUsersOutput, {name: 'createUsers'})
async createUsers(
#Args('input', new ValidationPipe()) input: CreateUsersInput,
#ReqUser() reqUser: RequestUser,
): Promise<CreateUsersOutput> {
return this.usersService.createUsers(input, reqUser);
}
create-shared.input.ts
#InputType()
export class DataObject {
#Field(() => UploadGraphQLScalar)
#Exclude()
public readonly csv?: Promise<FileUpload>;
}
#InputType()
#ArgsType()
export class CreateUsersInput {
#Field(() => DataObject)
public readonly data: DataObject;
}
Also, I want to mention you should not use global validation pipes (in my case they made files unreadable)
// app.useGlobalPipes(new ValidationPipe({ transform: true }));
You could use graphql-python/gql to try to upload a file:
from gql import Client, gql
from gql.transport.aiohttp import AIOHTTPTransport
transport = AIOHTTPTransport(url='YOUR_URL')
client = Client(transport=transport)
query = gql('''
mutation($file: Upload!) {
createUsers(file: $file)
}
''')
with open("YOUR_FILE_PATH", "rb") as f:
params = {"file": f}
result = client.execute(
query, variable_values=params, upload_files=True
)
print(result)
If you activate logging, you can see some message exchanged between the client and the backend.
Hello I need to check if there is an email in the database already:
with this:
return User.findOne({ where: { email } }).then((user) => {
if (user) return false;
return true;
});
I have the following inputtypes:
#InputType()
export class RegisterInput {
#Field()
#IsEmail({}, { message: 'Invalid email' })
email: string;
#Field()
#Length(1, 255)
name: string;
#Field()
password: string;
}
I would like to know if there is any way for me to validate the email in the inputtype? or just in my resolve:
#Mutation(() => User)
async register(
#Arg('data')
{ email, name, password }: RegisterInput,
): Promise<User> {
const hashedPassword = await bcrypt.hash(password, 12);
const user = await User.create({
email,
name,
password: hashedPassword,
}).save();
return user;
}
Actually you can register your own decorator for class-validator
For example it can look something like this:
isEmailAlreadyExists.ts
import {
registerDecorator,
ValidationOptions,
ValidatorConstraint,
ValidatorConstraintInterface,
} from 'class-validator';
import { UserRepo } from '../../repositories/UserRepo';
import { InjectRepository } from 'typeorm-typedi-extensions';
#ValidatorConstraint({ async: true })
export class isEmailAlreadyExist
implements ValidatorConstraintInterface {
#InjectRepository()
private readonly userRepo: UserRepo;
async validate(email: string) {
const user = await this.userRepo.findOne({ where: { email } });
if (user) return false;
return true;
}
}
export function IsEmailAlreadyExist(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [],
validator: isEmailAlreadyExist,
});
};
}
If you're injecting dependencies than you should in inject it in class-validator too. Simply add to your main file this:
import { Container } from 'typedi';
import * as classValidator from 'class-validator';
classValidator.useContainer(Container);
...
const schema = await buildSchema({
resolvers: [...],
container: Container,
});
Then you can use decorator in your InputType
import { InputType, Field } from 'type-graphql';
import { IsEmailAlreadyExist } from '../../../utils/validators/isEmailAlreadyExist';
#InputType()
export class YourInput {
#Field()
#IsEmailAlreadyExist()
email: string;
}
I actually just figured this out myself for my own project.
You can simply add a validation on the email from RegisterInput argument and throw an error if the email already exists.
import { Repository } from 'typeorm'
import { InjectRepository } from 'typeorm-typedi-extensions'
...
// Use dependency injection in the resolver's constructor
constructor(
#InjectRepository(User) private readonly userRepository: Repository<User>
) {}
...
// Your mutation
#Mutation(() => User)
async register(
#Arg('data')
{ email, name, password }: RegisterInput,
): Promise<User> {
const hashedPassword = await bcrypt.hash(password, 12);
const userWithEmail = this.userRepository.find({ email: email })
// If a user with the email was found
if (userWithEmail) {
throw new Error('A user with that email already exists!')
}
const user = await User.create({
email,
name,
password: hashedPassword,
}).save();
return user;
}
To use the InjectRepository make sure you add a "container" to your buildSchema function:
import { Container } from 'typedi'
...
const schema = await buildSchema({
resolvers: [...],
container: Container
})
Let me know if this works out for you? Thanks!
can someone help me, why is the CONTEXT undefined inside my subscription?
#Subscription(returns => CommentsDto, {
filter: (payload, variables, context) => {
console.log({ payload, variables, context }) // <------------ context context undefined
const isSameCode = variables.code === payload.newComment.code
const isAuthorized = context.req.headers.clientauthorization === payload.clientauthorization
return isSameCode && isAuthorized
},
})
newComment(
#Context() context,
#Args(({ name: 'code', type: () => String })) code: string,
) {
console.log(context) // <------------ undefined
return this.publisherService.asyncIterator('newComment')
}
It is working for Queries and Mutatinos...
Graphql definition is:
const GraphQLDefinition = GraphQLModule.forRoot({
context: ({ req, connection }) => {
// subscriptions
if (connection) {
return { req: connection.context }
}
// queries and mutations
return { req }
},
installSubscriptionHandlers: true,
path: '/graphql',
playground: true,
})
Thank you for any help
Because the Req and Res are undefined in the case of subscriptions so when you try to log the context it is undefined.
For context to be available you need to change the guards that you are using to return the context which can be found in the connection variable.
Basically to summarize:
=> req, res used in http/query & mutations
=> connection used in webSockets/subscriptions
Now to get the context correctly you will have to perform these steps exactly:
Modify App module file to use the GraphqlModuleImport
Modify Extract User Guard and Auth guard (or whatever guards you are using)
to return data for both query/mutation and subscription case.
Receive data using the context in the subscription.
Add jwtTokenPayload extractor function in the Auth service.
Opitonal: Helper Functions and DTOs for Typescript.
1-Detail:
GraphQLModule.forRootAsync({
//import AuthModule for JWT headers at graphql subscriptions
imports: [AuthModule],
//inject Auth Service
inject: [AuthService],
useFactory: async (authService: AuthService) => ({
debug: true,
playground: true,
installSubscriptionHandlers: true,
// pass the original req and res object into the graphql context,
// get context with decorator `#Context() { req, res, payload, connection }: GqlContext`
// req, res used in http/query&mutations, connection used in webSockets/subscriptions
context: ({ req, res, payload, connection }: GqlContext) => ({
req,
res,
payload,
connection,
}),
// subscriptions/webSockets authentication
typePaths: ["./**/*.graphql"],
resolvers: { ...resolvers },
subscriptions: {
// get headers
onConnect: (connectionParams: ConnectionParams) => {
// convert header keys to lowercase
const connectionParamsLowerKeys: Object = mapKeysToLowerCase(
connectionParams,
);
// get authToken from authorization header
let authToken: string | false = false;
const val = connectionParamsLowerKeys["authorization"];
if (val != null && typeof val === "string") {
authToken = val.split(" ")[1];
}
if (authToken) {
// verify authToken/getJwtPayLoad
const jwtPayload: JwtPayload = authService.getJwtPayLoad(
authToken,
);
// the user/jwtPayload object found will be available as context.currentUser/jwtPayload in your GraphQL resolvers
return {
currentUser: jwtPayload.username,
jwtPayload,
headers: connectionParamsLowerKeys,
};
}
throw new AuthenticationError("authToken must be provided");
},
},
definitions: {
path: join(process.cwd(), "src/graphql.classes.ts"),
outputAs: "class",
},
}),
}),
2-Detail:
My getRequest function example from the ExtractUserGuard class that extends the AuthGuard(jwt) class.
Change from:
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
const request = ctx.getContext().req;
return request;}
to this:
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
// req used in http queries and mutations, connection is used in websocket subscription connections, check AppModule
const { req, connection } = ctx.getContext();
// if subscriptions/webSockets, let it pass headers from connection.context to passport-jwt
const requestData =
connection && connection.context && connection.context.headers
? connection.context
: req;
return requestData;
}
3- Now you can get this data in your resolver.
#Subscription("testSubscription")
#UseGuards(ExtractUserGuard)
async testSubscription(
#Context("connection") connection: any,
): Promise<JSONObject> {
const subTopic = `${Subscriptions_Test_Event}.${connection.context.jwtPayload.email}`;
console.log("Listening to the event:", subTopic);
return this.pubSub.asyncIterator(subTopic);
}
4- For getting the jwtPayload using the token add the following function to your AuthService.
getJwtPayLoad(token: string): JwtPayload {
const jwtPayload = this.jwtService.decode(token);
return jwtPayload as JwtPayload;
}
5-Helper Functions and DTOs example (that I used in my project)
DTOs:
export interface JwtPayload {
username?: string;
expiration?: Date;
}
export interface GqlContext {
req: Request;
res: Response;
payload?: JwtPayload;
// required for subscription
connection: any;
}
export interface ConnectionParams {
authorization: string;
}
Helper Function:
export function mapKeysToLowerCase(
inputObject: Record<string, any>,
): Record<string, any> {
let key;
const keys = Object.keys(inputObject);
let n = keys.length;
const newobj: Record<string, any> = {};
while (n--) {
key = keys[n];
newobj[key.toLowerCase()] = inputObject[key];
}
return newobj;
}
I am using the default passport jwt AuthGuard for my project. That works for my post & get routes fine when setting the authentication header.
Now I want to use Nestjs Gateways as well with socket.io on the client-side, but I don't know how to send the access_token to the gateway?
That is basically my Gateway:
#WebSocketGateway()
export class UserGateway {
entityManager = getManager();
#UseGuards(AuthGuard('jwt'))
#SubscribeMessage('getUserList')
async handleMessage(client: any, payload: any) {
const results = await this.entityManager.find(UserEntity);
console.log(results);
return this.entityToClientUser(results);
}
And on the client I'm sending like this:
this.socket.emit('getUserList', users => {
console.log(users);
this.userListSub.next(users);
});
How and where do I add the jwt access_token? The documentation of nestjs misses that point completely for Websockets. All they say is, that the Guards work exactly the same for websockets as they do for post / get etc. See here
While the question is answered, I want to point out the Guard is not usable to prevent unauthorized users from establishing a connection.
It's only usable to guard specific events.
The handleConnection method of a class annotated with #WebSocketGateway is called before canActivate of your Guard.
I end up using something like this in my Gateway class:
async handleConnection(client: Socket) {
const payload = this.authService.verify(
client.handshake.headers.authorization,
);
const user = await this.usersService.findOne(payload.userId);
!user && client.disconnect();
}
For anyone looking for a solution. Here it is:
#UseGuards(WsGuard)
#SubscribeMessage('yourRoute')
async saveUser(socket: Socket, data: any) {
let auth_token = socket.handshake.headers.authorization;
// get the token itself without "Bearer"
auth_token = auth_token.split(' ')[1];
}
On the client side you add the authorization header like this:
this.socketOptions = {
transportOptions: {
polling: {
extraHeaders: {
Authorization: 'your token', // 'Bearer h93t4293t49jt34j9rferek...'
}
}
}
};
// ...
this.socket = io.connect('http://localhost:4200/', this.socketOptions);
// ...
Afterwards you have access to the token on every request serverside like in the example.
Here also the WsGuard I implemented.
#Injectable()
export class WsGuard implements CanActivate {
constructor(private userService: UserService) {
}
canActivate(
context: any,
): boolean | any | Promise<boolean | any> | Observable<boolean | any> {
const bearerToken = context.args[0].handshake.headers.authorization.split(' ')[1];
try {
const decoded = jwt.verify(bearerToken, jwtConstants.secret) as any;
return new Promise((resolve, reject) => {
return this.userService.findByUsername(decoded.username).then(user => {
if (user) {
resolve(user);
} else {
reject(false);
}
});
});
} catch (ex) {
console.log(ex);
return false;
}
}
}
I simply check if I can find a user with the username from the decoded token in my database with my user service. I am sure you could make this implementation cleaner, but it works.
Thanks! At the end i implemented a Guard that like the jwt guard puts the user inside the request. At the end I'm using the query string method from the socket client to pass the auth token This is my implementation:
import { CanActivate, ExecutionContext, Injectable, Logger } from '#nestjs/common';
import { WsException } from '#nestjs/websockets';
import { Socket } from 'socket.io';
import { AuthService } from '../auth/auth.service';
import { User } from '../auth/entity/user.entity';
#Injectable()
export class WsJwtGuard implements CanActivate {
private logger: Logger = new Logger(WsJwtGuard.name);
constructor(private authService: AuthService) { }
async canActivate(context: ExecutionContext): Promise<boolean> {
try {
const client: Socket = context.switchToWs().getClient<Socket>();
const authToken: string = client.handshake?.query?.token;
const user: User = await this.authService.verifyUser(authToken);
client.join(`house_${user?.house?.id}`);
context.switchToHttp().getRequest().user = user
return Boolean(user);
} catch (err) {
throw new WsException(err.message);
}
}
}