26
Authentication with AccountsJS & GraphQL Modules
This article was published on 2018-11-16 by Arda Tanrikulu @ The Guild Blog
When starting a backend project, two of the biggest concerns will usually be the right structure of the project and authentication. If you could skip thinking and planning about these two, starting a new backend project can be much easier.
If you haven't checked out our blog post about authentication and authorization in GraphQL Modules, please read that before!
Internally, we use GraphQL-Modules and AccountsJS to help us with those two decisions, GraphQL-Modules helps us solve our architectural problems in modular, schema-first approaches with the power of GraphQL and AccountsJS helps us create our authentication solutions by providing a simple API together with client and server libraries that saves us a lot of the ground work around authentication.
If you haven't heard about AccountsJS before, it is a set of libraries to provide a full-stack authentication and accounts-management solutions for Javascript.
It is really customizable; so you can write any plugins for your own authentication methods or use the already existing email-password or the Facebook and Twitter OAuth integration packages.
AccountsJS has connector libraries for MongoDB and Redis, but you can write your own database handler by implementing a simple interface.
Accounts JS provide a ready to use GraphQL API if you install their GraphQL library and we are happy to announce that the GraphQL library is now internally built using GraphQL-Modules!
It doesn't affect people who are not using GraphQL Modules, but it helps the maintainers of AccountsJS and simplifies the integration for GraphQL-Modules-based projects.
First install required dependencies from npm or yarn;
yarn add mongodb @accounts/server @accounts/password @accounts/database-manager @accounts/mongo @accounts/graphql-api @graphql-modules/core apollo-server graphql-import-node
Let's assume that we're using MongoDB as our database, password-based authentication and ApolloServer;
import 'graphql-import-node';
import { ApolloServer } from 'apollo-server';
import { MongoClient, ObjectId } from 'mongodb';
import { AccountsServer } from '@accounts/server';
import { AccountsPassword } from '@accounts/password';
import { DatabaseManager } from '@accounts/database-manager';
import MongoDBInterface from '@accounts/mongo';
import { AccountsModule } from '@accounts/graphql-api';
import * as typeDefs from './typeDefs.graphql';
import { resolvers } from './resolvers';
const PORT = process.env['MONGO_URI'] || 4000;
const MONGO_URI = process.env['MONGO_URI'] || 'mongodb://localhost:27017/myDb';
const TOKEN_SECRET = process.env['TOKEN_SECRET'] || 'myTokenSecret';
async function main() {
const mongoClient = await MongoClient.connect(MONGO_URI, {
useNewUrlParser: true,
native_parser: true
});
const db = mongoClient.db();
const userStorage = new MongoDBInterface(db, {
convertUserIdToMongoObjectId: false
});
// Create database manager (create user, find users, sessions etc) for accounts-js
const accountsDb = new DatabaseManager({
sessionStorage: userStorage,
userStorage,
});
// Create accounts server that holds a lower level of all accounts operations
const accountsServer = new AccountsServer(
{
db: accountsDb,
tokenSecret: TOKEN_SECRET
},
{
password: new AccountsPassword(),
}
);
const { schema } = new GraphQLModule({
typeDefs,
resolvers,
imports: [
AccountsModule.forRoot({
accountsServer
});
],
providers: [
{
provide: Db,
useValue: db // Use MongoDB instance inside DI
}
]
});
const apolloServer = new ApolloServer({
schema,
context: session => session,
introspection: true
});
const { url } = await apolloServer.listen(PORT);
console.log(`Server listening: ${url}`);
}
main();
And we can extend User type with custom fields in our schema, and add a mutation which is restricted to authenticated clients.
type Query {
allPosts: [Post]
}
type Mutation {
addPost(title: String, content: String): ID @auth
}
type User {
posts: [Post]
}
type Post {
id: ID
title: String
content: String
author: User
}
Finally let's define some resolvers for it;
export const resolvers = {
User: {
posts: ({ _id }, args, { injector }) => {
const db = injector.get(Db);
const Posts = db.collection('posts');
return Posts.find({ userId: _id }).toArray();
}
},
Post: {
id: ({ _id }) => _id,
author: ({ userId }, args, { injector }) => {
const accountsServer = injector.get(AccountsServer);
return accountsServer.findUserById(userId);
}
},
Query: {
allPosts: (root, args, { injector }) => {
const db = injector.get(Db);
const Posts = db.collection('posts');
return Posts.find().toArray();
}
},
Mutation: {
addPost: (root, { title, content }, { injector, userId }: ModuleContext<AccountsContext>) => {
const db = injector.get(Db);
const Posts = db.collection('posts');
const { insertedId } = Posts.insertOne({ title, content, userId });
return insertedId;
}
}
When you print the whole app's schema, you would see something like above;
type TwoFactorSecretKey {
ascii: String
base32: String
hex: String
qr_code_ascii: String
qr_code_hex: String
qr_code_base32: String
google_auth_qr: String
otpauth_url: String
}
input TwoFactorSecretKeyInput {
ascii: String
base32: String
hex: String
qr_code_ascii: String
qr_code_hex: String
qr_code_base32: String
google_auth_qr: String
otpauth_url: String
}
input CreateUserInput {
username: String
email: String
password: String
}
type Query {
twoFactorSecret: TwoFactorSecretKey
getUser: User
allPosts: [Post]
}
type Mutation {
createUser(user: CreateUserInput!): ID
verifyEmail(token: String!): Boolean
resetPassword(token: String!, newPassword: String!): Boolean
sendVerificationEmail(email: String!): Boolean
sendResetPasswordEmail(email: String!): Boolean
changePassword(oldPassword: String!, newPassword: String!): Boolean
twoFactorSet(secret: TwoFactorSecretKeyInput!, code: String!): Boolean
twoFactorUnset(code: String!): Boolean
impersonate(accessToken: String!, username: String!): ImpersonateReturn
refreshTokens(accessToken: String!, refreshToken: String!): LoginResult
logout: Boolean
authenticate(serviceName: String!, params: AuthenticateParamsInput!): LoginResult
addPost(title: String, content: String): Post
}
type Tokens {
refreshToken: String
accessToken: String
}
type LoginResult {
sessionId: String
tokens: Tokens
}
type ImpersonateReturn {
authorized: Boolean
tokens: Tokens
user: User
}
type EmailRecord {
address: String
verified: Boolean
}
type User {
id: ID!
emails: [EmailRecord!]
username: String
posts: [Post]
}
input UserInput {
id: ID
email: String
username: String
}
input AuthenticateParamsInput {
access_token: String
access_token_secret: String
provider: String
password: String
user: UserInput
code: String
}
type Post {
id: ID
title: String
content: String
author: User
}
Now we can create a simple frontend app by using Apollo-Client and AccountsJS client for this backend app. The example below shows some example code that works on these two.
import React, { Component } from 'react'
import { AccountsClient } from '@accounts/client'
import { AccountsClientPassword } from '@accounts/client-password'
import GraphQLClient from '@accounts/graphql-client'
import ApolloClient from 'apollo-boost'
import { Query, Mutation, ApolloProvider } from 'react-apollo'
import gql from 'graphql-tag'
import ReactDOM from 'react-dom'
const apolloClient = new ApolloClient({
request: async (operation) => {
const tokens = await accountsClient.getTokens()
if (tokens) {
operation.setContext({
headers: {
'accounts-access-token': tokens.accessToken
}
})
}
},
uri: 'http://localhost:4000/graphql'
})
const accountsGraphQL = new GraphQLClient({ graphQLClient: apolloClient })
const accountsClient = new AccountsClient({}, accountsGraphQL)
const accountsPassword = new AccountsClientPassword(accountsClient)
const ALL_POSTS_QUERY = gql`
query AllPosts {
allPosts {
id
title
content
author {
username
}
}
}
`
const ADD_POST_MUTATION = gql`
mutation AddPost($title: String, $content: String) {
addPost(title: $title, content: $content)
}
`
class App extends Component {
state = {
credentials: {
username: '',
password: ''
},
newPost: {
title: '',
content: ''
},
user: null
}
componentDidMount() {
return this.updateUserState()
}
async updateUserState() {
const tokens = await accountsClient.refreshSession()
if (tokens) {
const user = await accountsGraphQL.getUser()
await this.setState({ user })
}
}
renderAllPosts() {
return (
<Query query={ALL_POSTS_QUERY}>
{({ data, loading, error }) => {
if (loading) {
return <p>Loading...</p>
}
if (error) {
return <p>Error: {error}</p>
}
return data.allPosts.map((post: any) => (
<li>
<p>{post.title}</p>
<p>{post.content}</p>
<p>Author: {post.author.username}</p>
</li>
))
}}
</Query>
)
}
renderLoginRegister() {
return (
<fieldset>
<legend>Login - Register</legend>
<form>
<p>
<label>
Username:
<input
value={this.state.credentials.username}
onChange={(e) =>
this.setState({
credentials: {
...this.state.credentials,
username: e.target.value
}
})
}
/>
</label>
</p>
<p>
<label>
Password:
<input
value={this.state.credentials.password}
onChange={(e) =>
this.setState({
credentials: {
...this.state.credentials,
password: e.target.value
}
})
}
/>
</label>
</p>
<p>
<button
onClick={(e) => {
e.preventDefault()
accountsPassword
.login({
password: this.state.credentials.password,
user: {
username: this.state.credentials.username
}
})
.then(() => this.updateUserState())
}}
>
Login
</button>
</p>
<p>
<button
onClick={(e) => {
e.preventDefault()
accountsPassword
.createUser({
password: this.state.credentials.password,
username: this.state.credentials.username
})
.then(() => {
alert('Please login with your new credentials')
this.setState({
credentials: {
username: '',
password: ''
}
})
})
}}
>
Register
</button>
</p>
</form>
</fieldset>
)
}
renderAddPost() {
return (
<Mutation mutation={ADD_POST_MUTATION}>
{(addPost) => {
return (
<fieldset>
<legend>Add Post</legend>
<form>
<p>
<label>
Title:
<input
value={this.state.newPost.title}
onChange={(e) =>
this.setState({
newPost: {
...this.state.newPost,
title: e.target.value
}
})
}
/>
</label>
</p>
<p>
<label>
Content:
<input
value={this.state.newPost.content}
onChange={(e) =>
this.setState({
newPost: {
...this.state.newPost,
content: e.target.value
}
})
}
/>
</label>
</p>
<p>
<input
type="submit"
onClick={(e) => {
e.preventDefault()
addPost({
variables: {
title: this.state.newPost.title,
content: this.state.newPost.content
}
})
}}
/>
</p>
</form>
</fieldset>
)
}}
</Mutation>
)
}
render() {
return (
<div>
<h2>All Posts</h2>
{this.renderAllPosts()}
{!this.state.user && this.renderLoginRegister()}
{this.state.user && this.renderAddPost()}
</div>
)
}
}
ReactDOM.render(
<ApolloProvider client={apolloClient}>
<App />
</ApolloProvider>,
document.getElementById('root')
)
As you can see from the example, it can be really easy to create an application that has authentication in modular and future proof approach.
You can learn more about AccountsJS from the docs of this great library for more features such as Two-Factor Authentication and Facebook and Twitter integration using OAuth.
Also you can learn more about GraphQL-Modules on the website and see how you can add GraphQL Modules features into your system in a gradual and selective way.
If you want strict types based on GraphQL Schema, for each module, GraphQL Code Generator has built-in support for GraphQL-Modules based projects. See the docs for more details.
You can check out our example about this integration; https://github.com/ardatan/graphql-modules-accountsjs-boilerplate
- GraphQL Modules — Feature based GraphQL Modules at scale
- Why is True Modular Encapsulation So Important in Large-Scale GraphQL Projects?
- Why did we implement our own Dependency Injection library for GraphQL-Modules?
- Scoped Providers in GraphQL-Modules Dependency Injection
- Writing a GraphQL TypeScript project w/ GraphQL-Modules and GraphQL-Code-Generator
- Authentication and Authorization in GraphQL (and how GraphQL-Modules can help)
- Authentication with AccountsJS & GraphQL Modules
- Manage Circular Imports Hell with GraphQL-Modules
26