Passport auth strategy
This is the last active development release of this package as Keystone 5 is now in a 6 to 12 month active maintenance phase. For more information please read our Keystone 5 and beyond post.
This feature is currently in progress. While passport works for the
GraphQLApp
, only password authentication is supported for theAdminUIApp
. Please see this issue for more details.
Enable Keystone authentication via services such as Google, Twitter, Facebook, GitHub, and any others supported by passport.js.
Authentication flows
This package enables three authentication flows;
- Single Step Account Creation & Authentication
- Multi Step Account Creation & Authentication
- Existing Account Authentication
Single-step account creation and authentication
When creating a new account in Keystone, the service (Google, Twitter, etc) provides some basic user information such as name and, if enabled, email. Often this information alone is enough to create a new Keystone account, so it is the default authentication flow known as Single Step Account Creation.
For example, when logging in with Google, the user will;
- Click "Login with Google"
- Be redirected to google.com's authentication page if not already logged in
- Be asked to grant permission for your Keystone app
- Be redirected to your application's callback URL
- This package will create a new account & authenticate the user, then trigger
onAuthenticated({ token, item, isNewItem })
withisNewItem = true
(see the API docs below)
Multi-step account creation and authentication
Sometimes the information provided by the service is not enough to create a new account in Keystone. For example, your application may require the user's age, or want to confirm the email address provided by the service.
The default Single Step Flow can be extended to pause account creation while we gather the extra information from the user. Pausing even works across page refreshes. This is known as Multi Step Account Creation.
For example, when logging in with Google, the user will (differences from the Single Step Flow are bolded):
- Click "Login with Google"
- Be redirected to google.com's authentication page if not already logged in
- Be asked to grant permission for your Keystone app
- Be redirected to your application's callback URL
- Be redirected to a form to gather more information
- Submit the form which will resume account creation & authentication
- This package will create a new account & authenticate the user, then trigger
onAuthenticated({ token, item, isNewItem })
withisNewItem = true
(see the API docs below)
Existing Account Authentication
When a user has previously created an account via a given service, this package
will use information stored in the idField
(see the API docs below) to
match the account and authenticate them:
- Click "Login with Google"
- Be redirected to google.com's authentication page if not already logged in
- Be redirected to your application's callback URL
- This package will authenticate the existing user, then trigger
onAuthenticated({ token, item, isNewItem })
withisNewItem = false
(see the API docs below)
API
The API needs documentation.
Usage
Single-step with google
Note: The documentation below applies to any of the supported strategies (Twitter, Facebook, etc)
To run this example: keystone dev
, then visit
http://localhost:3000/auth/google
to start the Google authentication process.
index.js
const { Keystone } = require('@keystonejs/keystone');
const { MongooseAdapter } = require('@keystonejs/adapter-mongoose');
const { GraphQLApp } = require('@keystonejs/app-graphql');
const { AdminUIApp } = require('@keystonejs/app-admin-ui');
const { GoogleAuthStrategy } = require('@keystonejs/auth-passport');
const cookieSecret = '<Something super secret>';
const keystone = new Keystone({
adapter: new MongooseAdapter(),
cookieSecret,
});
keystone.createList('User', {
fields: {
name: { type: Text },
email: { type: Text },
// This field name must match the `idField` setting passed to the auth
// strategy constructor below
googleId: { type: Text },
},
});
const googleStrategy = keystone.createAuthStrategy({
type: GoogleAuthStrategy,
list: 'User',
config: {
idField: 'googleId',
appId: '<Your Google App Id>',
appSecret: '<Your Google App Secret>',
loginPath: '/auth/google',
callbackPath: '/auth/google/callback',
callbackHost: 'http://localhost:3000',
// Once a user is found/created and successfully matched to the
// googleId, they are authenticated, and the token is returned here.
// NOTE: By default Keystone sets a `keystone.sid` which authenticates the
// user for the API domain. If you want to authenticate via another domain,
// you must pass the `token` as a Bearer Token to GraphQL requests.
onAuthenticated: ({ token, item, isNewItem }, req, res) => {
console.log(token);
res.redirect('/');
},
// If there was an error during any of the authentication flow, this
// callback is executed
onError: (error, req, res) => {
console.error(error);
res.redirect('/?error=Uh-oh');
},
},
});
module.exports = {
keystone,
apps: [
new GraphQLApp(),
new AdminUIApp({
name: 'Login With Google Example',
authStrategy: googleStrategy,
}),
],
};
Multi-step with google
Note: The documentation below applies to any of the supported strategies (Twitter, Facebook, etc)
Due to the extra route used for gathering the user's name, this example implements
an All-in-one Custom Server
and should be run with node server.js
, then visit http://localhost:3000/auth/google
to start
the Google authentication process.
server.js
const { Keystone } = require('@keystonejs/keystone');
const { MongooseAdapter } = require('@keystonejs/adapter-mongoose');
const { GraphQLApp } = require('@keystonejs/app-graphql');
const { AdminUIApp } = require('@keystonejs/app-admin-ui');
const express = require('express');
const { GoogleAuthStrategy } = require('@keystonejs/auth-passport');
const cookieSecret = '<Something super secret>';
const keystone = new Keystone({
adapter: new MongooseAdapter(),
cookieSecret,
});
keystone.createList('User', {
fields: {
name: { type: Text },
email: { type: Text },
// This field name must match the `idField` setting passed to the auth
// strategy constructor below
googleId: { type: Text },
},
});
const googleStrategy = keystone.createAuthStrategy({
type: GoogleAuthStrategy,
list: 'User',
config: {
idField: 'googleId',
appId: '<Your Google App Id>',
appSecret: '<Your Google App Secret>',
loginPath: '/auth/google',
callbackPath: '/auth/google/callback',
callbackHost: 'http://localhost:3000',
loginPathMiddleware: (req, res, next) => {
// An express middleware executed before the Passport social signin flow
// begins. Useful for setting cookies, etc.
// Don't forget to call `next()`!
next();
},
callbackPathMiddleware: (req, res, next) => {
// An express middleware executed before the callback route is run. Useful
// for logging, etc.
// Don't forget to call `next()`!
next();
},
// Called when there's no existing user for the given googleId
// Default: resolveCreateData: () => ({})
resolveCreateData: ({ createData, actions: { pauseAuthentication } }, req, res) => {
// If we don't have the right data to continue with a creation
if (!createData.name) {
// then we pause the flow
pauseAuthentication();
// And redirect the user to a page where they can enter the data.
// Later, the `resolveCreateData()` method will be re-executed this
// time with the complete data.
res.redirect(`/auth/google/step-2`);
return;
}
return createData;
},
// Once a user is found/created and successfully matched to the
// googleId, they are authenticated, and the token is returned here.
// NOTE: By default Keystone sets a `keystone.sid` which authenticates the
// user for the API domain. If you want to authenticate via another domain,
// you must pass the `token` as a Bearer Token to GraphQL requests.
onAuthenticated: ({ token, item, isNewItem }, req, res) => {
console.log(token);
res.redirect('/');
},
// If there was an error during any of the authentication flow, this
// callback is executed
onError: (error, req, res) => {
console.error(error);
res.redirect('/?error=Uh-oh');
},
},
});
keystone
.prepare({
apps: [
new GraphQLApp(),
new AdminUIApp({
name: 'Login With Google Example',
authStrategy: googleStrategy,
}),
],
dev: process.env.NODE_ENV !== 'production',
})
.then(async ({ middlewares }) => {
await keystone.connect();
const app = express();
app.use(middlewares);
// Sample page to collect a name, submits to the completion step which will
// create a user
app.use(`/auth/google/step-2`, express.urlencoded({ extended: true }), (req, res, next) => {
if (req.method === 'POST') {
const { name } = req.body;
// Continue the authentication flow with additional data the user
// submitted.
// This data is merged into other data required by Keystone and will
// trigger the resolveCreateData() method again.
return googleStrategy.resumeAuthentication({ name }, req, res, next);
}
res.send(`
<form method="post">
<label>
What is your name? <input type="text" name="name" />
</label>
<button type="submit">Submit</button>
</form>
`);
});
app.listen(port, error => {
if (error) throw error;
console.log(`Ready. App available at http://localhost:${port}`);
});
})
.catch(error => {
console.error(error);
process.exit(1);
});
Sometimes you just need to get the profile data provided by the auth provider. In that case, you
have to call the resolveCreateData
with serviceProfile
property. See an example below:
// Here we wait for our serviceProfile to get the data provided by the auth provider
resolveCreateData: ({ createData, serviceProfile }) => {
// Once we had our seviceProfile, we set it on our list fields
// Google will return the profile data inside the _json key
// Each provider can return the user profile data in a different way.
// Check how it's returned on your provider documentation
createData.name = serviceProfile._json.name;
createData.profilePicture = serviceProfile._json.picture;
return createData;
};
Using other Passport strategies
You can create your own strategies to work with Keystone by extending the
PassportAuthStrategy
class:
const PassportWordPress = require('passport-wordpress').Strategy;
const { PassportAuthStrategy } = require('@keystonejs/auth-passport');
class WordPressAuthStrategy extends PassportAuthStrategy {
constructor(keystone, listKey, config) {
super(WordPressAuthStrategy.authType, keystone, listKey, config, PassportWordPress);
}
}
WordPressAuthStrategy.authType = 'wordpress';
module.exports = WordPressAuthStrategy;