Apollo Security
Install
npm i -S @bluelibs/apollo-bundle @bluelibs/apollo-security-bundle passport
import { ApolloSecurityBundle } from "@bluelibs/apollo-security-bundle";
const kernel = new Kernel({ bundles: [new ApolloSecurityBundle()],});
Purpose
This is used to detect the userId
based on the authentication token (bluelibs-token
) and inject it inside the GraphQL's context so you have access to it in your resolvers giving you the ability to perform authorization. On top of that it offers a seamless integration with Passport
which gives you the ability to benefit of over 500+ authentication strategies.
This package features:
- Support for websocket authentication
- Customisable token names
- Extends
IGraphQLContext
to autocompleteuserId
export interface IApolloSecurityBundleConfig { // All true by default support: { headers?: boolean; cookies?: boolean; websocket?: boolean; }; // bluelibs-token is the default for all identifiers: { headers?: string; // Has priority over cookies cookies?: string; // If no header is present it will read from here // For websocket you have to send the connection params in order to work websocket?: string; };}
Context
IGraphQLContext
is properly extended by this package:
import { IResolverMap } from "@bluelibs/graphql-bundle";
load({ resolvers: { Query: { findMyPosts(_, args, context) { // Context should have authenticationToken and userId if (!context.userId) { // You can throw an error. } }, } as IResolverMap, },});
Passport
Benefit of over 500+ authentication strategies, by offering plug-in support for most popular library: passport
npm i -S passport passport-facebook
Authenticator
We define our methods of authentication through Authenticator
classes. Read through it as the comments will explain the behavior.
import * as passport from "passport";import { Strategy as FacebookStrategy } from "passport-facebook";import { ApolloPassportStrategy } from "../models/ApolloPassportStrategy";
export class FacebookAuthenticator extends PassportAuthenticator { createStrategy() { // This is documented in the passport-facebook package: http://www.passportjs.org/docs/facebook/ return new FacebookStrategy( { // Read this either from process.env or inject them inside the classes clientID: "XXX", clientSecret: "XXX", callbackURL: "http://localhost:4000/auth/facebook/callback", }, async (accesstoken, refreshToken, profile, done) => { try { // If the user is newly created, `isNew` will be true, so you can adapt the profile const { isNew, user } = await this.findOrCreate(profile.id);
// By default we store the "profile.id" inside "facebookId" at user level which is derived from strategy name // You can customise the name by overriding get name()
if (isNew) { this.securityService.updateUser(user._id, { // other things }); }
done(null, user); } catch (err) { done(err); } } ); }
route() { // This will redirect to facebook to ask for permissions // this.app is an express application this.app.get("/auth/facebook", passport.authenticate(this.name));
// This is a helper function to allow easy handle of success this.get( "/auth/facebook/callback", {}, async (err, user, req, res, next) => { // this creates the authentication token for the user const token = await this.getToken(user._id);
res.redirect(`https://uihost.com/facebook/success?token={token}`); } ); }}
Note, if you are using X-Framework
, you can inject the AppRouter which generates urls for the app:
import { APP_ROUTER, Router } from "@bluelibs/x-bundle";
class FacebookAuthenticator extends PassportAuthenticator { @Inject(APP_ROUTER) router: Router;
function getRedirectURL(token: string) { return this.router.path("/facebook/success/:token", { token }); }}
// register it inside the passport serviceclass AppBundle extends Bundle { async init() { const passportService = this.container.get(PassportService); passportService.register(FacebookAuthenticator); }}
Token Security
You have two ways to store the token, in localStorage
or as a HTTP-server only cookie. The most secure alternative is the cookie
because your JS code won't have access to it, and subsequently you cannot be subject to XSS or other bogus plugins that your browser may have to expose your token.
However, we currently do not have a way to make that work with websockets, thus, our approach is to use localStorage
to offer the seamless approach. If you do not need websockets and want benefit of HTTP-server Cookie
, just return the cookie in your response.
By using getToken()
we create an actual token for authentication for the user. The problem is that when we pass that to the ui
microservice, the url can be sniffed, leading to a security whole in the system.
The solution is that once you arrive at that specific url call the mutation reissueToken(token)
which will return instead a newly freshly created token that you can store in localStorage
or where you prefer.