-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Document how authentication guards for GraphQL Subscriptions work #394
Comments
@kamilmysliwiec Can you give us a hint on how to use it? |
We're having some success for authentication in combination with guards and subscriptions: GraphQLModule.forRoot({
context: ({ req, connection }) => connection ? { req: connection.context } : { req }
...
}) On the client-side, to configure Apollo client: connectionParams = () => {
return {
isWebSocket: true,
headers: {
authorization: `Bearer ${authService.getBearerToken()}`,
}
}
} This will successfully authenticate and then authorize through guards. The problem is that we don't know whether this is intended, and therefore whether we should depend on this method to work in the future. The only way we can set this straight is by obtaining some information from the Nest team, and one good way would be through documentation. /cc @madfist |
As I am using an auth schema based on http-bearer, to make it work I had to change the context to: context: ({ req, connection }) => connection ? { req: { headers: connection.context } } : { req }, |
hello After some struggle to put my auth guard with apollo subscriptions, I get it with the help of this issue and other info, I leave here the changes that I made in my AppModule, GqlContext, GqlAuthGuard maybe can be useful for other 1. Add connection to GqlContext
export interface GqlContext {
req: Request;
res: Response;
payload?: GqlContextPayload;
// required for subscription
connection: any;
} 2. change AppModule with subscriptions and forRootAsynchere we must inject AuthModule/AuthService to check Authorization headers JWT, for this we must replace
@Module({
imports: [
AuthModule,
UsersModule,
// chaincode modules
ParticipantModule,
PersonModule,
CauseModule,
TransactionModule,
// apolloServer config: use forRootAsync to import AuthModule and inject AuthService
GraphQLModule.forRootAsync({
// import AuthModule
imports: [AuthModule],
// inject authService
useFactory: async (authService: AuthService) => ({
debug: true,
playground: true,
installSubscriptionHandlers: true,
autoSchemaFile: 'schema.gql',
// 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 }),
// configure graphql cors here
cors: {
origin: e.corsOriginReactFrontend,
credentials: true,
},
// subscriptions/webSockets authentication
subscriptions: {
// get headers
onConnect: (connectionParams: ConnectionParams) => {
// convert header keys to lowercase
const connectionParamsLowerKeys = mapKeysToLowerCase(connectionParams);
// get authToken from authorization header
const authToken: string = ('authorization' in connectionParamsLowerKeys)
&& connectionParamsLowerKeys.authorization.split(' ')[1];
if (authToken) {
// verify authToken/getJwtPayLoad
const jwtPayload: GqlContextPayload = 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');
},
},
}),
// inject: AuthService
inject: [AuthService],
}),
],
}) 3. Change GqlAuthGuard to return subscription connection.context.headers to passport-jwt service
@Injectable()
export class GqlAuthGuard extends AuthGuard('jwt') {
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
return (connection && connection.context && connection.context.headers)
? connection.context
: req;
}
} now use it in subscription @UseGuards(GqlAuthGuard)
@Subscription(returns => Person)
personAdded() {
return pubSub.asyncIterator('personAdded');
} fire subscription with graphql playground with header {
"Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ....",
} |
@kamilmysliwiec Any thoughts? |
Any ideas on this subject? The above example by @solidarynetwork is nice but it's very specific to their implementation and IMO not really applicable in general. I would love to know how to make guards work in general for Subscriptions. Not how to make a new guard-like thing inside my GQL configuration. |
Guards for Subscriptions works differently for Nest5 and Nest6. For Nest5 I recommend something like @solidarynetwork 's solution. |
Can't seem to get this working with my custom |
@madfist Is there something required from the client side? I see @lorenzleutgeb has mentioned additional client configuration including I'm using nest 7.0.7. |
In fact I don't think any of these solutions are actually getting an
Here is my context object:
There is a Is there some express middleware magic I should look into? The variables are clearly present when the subscription method is invoked. Do I need to look into wherever nest is getting that information? |
You won't get an IncomingMessage because subscriptions work on websocket not http(s), but since the init message needs to go through your guards, you need to make it look like an http request (at least authorization). If you want to act upon the |
The problem is that I need |
Can you try writing your subscription like this on the client-side? (I don't know if this format is required to get the variables, but I found it works like this for me) {
query: `subscription ($threadId: ID!) {
threadMessageCreated(threadId: $threadId) { id }
}`,
variables: { threadId }
} Otherwise even if they can make the connection you can later filter out messages to them, so they won't get unauthorized information. My other suggestion would be to write your variables next to the authorization header. Whatever you write there on the client-side can be read in the guards. |
This solution worked for me but your client must sent authorization not Authorization in headers
Solution : NB: Express converte all headers to lowercase |
It's been a huge pain but thanks to all the comments in this thread, I was able to figure out how to pass authentication cookies through for a GraphQL subscription to my JWT guard. As a GraphQL subscription is just a websocket under the hood, I was able to pull the headers off the incoming websocket like so:
We return the headers from the In our guard, we then extract the
At this point, the JWT strategy treats the headers in the exact same fashion as a standard GraphQL query/mutation and authenticates appropriately. |
I am using cookie session with redis. Here is a quick and dirty solution for NestJS v7. Write an onConnect handler: import {IncomingMessage, ServerResponse} from 'http';
import cookie from 'cookie';
import {JSONCookies, signedCookies} from 'cookie-parser';
import session from 'express-session';
import redis, {ClientOpts} from 'redis';
import passport from 'passport';
class Request extends IncomingMessage {
cookies: { [ k: string ]: any };
signedCookies: { [ k: string ]: any };
}
export interface Options {
cookie?: {
secret?: string
}
redis: ClientOpts;
session: session.SessionOptions;
}
function graphQlWebsocketMiddleware(opts: Options = null) {
const RedisStore = require('connect-redis')(session),
redisClient = redis.createClient({
host: opts.redis.host
}),
sessionMiddleware = session({
store: new RedisStore({ client: redisClient }),
...opts.session
}),
passportInit = passport.initialize(),
passportSession = passport.session();
return (connectionParams: { [ key: string ]: any }, ws: { [ key: string ]: any }): Promise<any> => {
const req = ws?.upgradeReq as Request;
if (!req) {
return new Promise((resolve => resolve({})));
}
if (req.headers.cookie) {
req.cookies = cookie.parse(req.headers.cookie);
if (opts.cookie?.secret) {
req.signedCookies = signedCookies(req.cookies, opts.cookie.secret);
req.signedCookies = JSONCookies(req.signedCookies);
}
} else {
req.cookies = Object.create(null);
req.signedCookies = Object.create(null);
}
const res = new ServerResponse(req);
return new Promise<any>(resolve => {
sessionMiddleware(req as any, res as any,
() => passportInit(req, res,
() => passportSession(req, res,
() => resolve({ req })
)
)
);
});
};
}; Init GraphQLModule: GraphQLModule.forRoot({
...conf.graphQl,
context : ctx => ctx.connection ? { ...ctx, req: ctx.connection.context?.req } : ctx,
subscriptions: {
onConnect: graphQlWebsocketMiddleware({
redis : {
host: conf.redis.host
},
session: {
name : conf.session.name,
secret : conf.session.secret,
resave : false,
saveUninitialized: false,
rolling : true,
cookie : {
secure: conf.session.cookie.secure,
maxAge: conf.session.cookie.maxAge
}
}
})
}
}) Then you can implement a session reload in your guard: import {CanActivate, ExecutionContext, Injectable} from '@nestjs/common';
import {GqlContextType, GqlExecutionContext} from '@nestjs/graphql';
@Injectable()
export class AuthenticatedGuard implements CanActivate {
async canActivate(context: ExecutionContext) {
switch (context.getType<GqlContextType>()) {
case 'graphql':
const ctx = GqlExecutionContext.create(context).getContext();
if (ctx.connection) {
return new Promise<any>(resolve => ctx.req.session.reload(() => resolve(ctx.req?.isAuthenticated() || false)));
}
return ctx.req?.isAuthenticated() || false;
case 'http':
return context.switchToHttp().getRequest().isAuthenticated();
}
}
} |
Why there are so many ways to do this? @kamilmysliwiec Still not an official way to do this. It's a pain to try all this! |
Reading all of this, I don't know which way is the best 🤔 |
Worked Great. Make sure to set the "authorization" header in lower case, because the jwt passport strategy only parses lower case authorization This means that putting e.g. "Authorization: Bearer " will always be invalid, cause it parses by the property key of "authorization" and therefore it does not find the token. Another solution would be to convert all object keys of the connection context to lowercase. |
Let's track this here #1694 |
Just for future reference, our solution looked like the below. We use passport-jwt, keycloak for authing, urql for gql client (but would work equally with Apollo client).
const subscriptionClient = new SubscriptionClient(
graphqlWsUrl,
{
reconnect: true,
// whatever token works here is fine, we use keycloak
// we use a function to resolve it because keycloak.token doesn't resolve immediately
connectionParams: () => ({
token: keycloak.token,
}),
},
);
GraphQLModule.forRoot({
installSubscriptionHandlers: true,
subscriptions: {
onConnect: (connectionParams: any) => {
const token = connectionParams.token;
// the first few times urql tries to connect, keycloak hasn't got the token yet
// returning false here will cause urql to try to connect again
if (!token) {
return false;
}
// this mocks the headers into the right place in the context
// so none of the rest of our code has to change
return { headers: { authorization: `Bearer ${token}` } };
},
},
context: ({ req, res, connection }) =>
connection
? {
req: {
...req,
...connection.context,
},
res,
}
: { req, res },
}), Because we place the token in the correct place as though it came from a standard web request, |
just made this changes, in your GqlGuard and it works for me. getRequest(context: ExecutionContext) {
if (context.getType<ContextType | 'graphql'>() === 'graphql') {
const ctx = GqlExecutionContext.create(context).getContext();
// required for passport.js for websocket grapqhl subscriptions
if (ctx.websocketHeader?.connectionParams) {
const websocketHeader = ctx.websocketHeader?.connectionParams || {};
return { headers: { ...websocketHeader } };
}
return ctx.req;
}
return context.switchToHttp().getRequest();
} |
For those who are using graphql-ws package, you can do like this
And for the GqlAuthGuard part, you may simply follow the nest.js documentation |
nestjs/graphql#84 (comment) suggests that with Nest 6 it is now possible to use Authentication Guards in combination with GraphQL Subscriptions. It is unclear how this was implemented and also how the API now looks like. Please document it.
The text was updated successfully, but these errors were encountered: