Authentication with accounts-js & GraphQL Modules
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 accounts-js 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 accounts-js helps us create our authentication solutions by providing a simple API together with client and server libraries that saves us a lot of the groundwork around authentication.
If you haven’t heard about accounts-js 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.
accounts-js has connector libraries for MongoDB and Redis, but you can write your own database handler by implementing a simple interface.
accounts-js provides 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 accounts-js and simplifies the integration for GraphQL-Modules-based projects.
How to Implement Server-Side Using accounts-js, GraphQL Modules and Apollo Server
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 } from 'mongodb'
import { DatabaseManager } from '@accounts/database-manager'
import { AccountsModule } from '@accounts/graphql-api'
import MongoDBInterface from '@accounts/mongo'
import { AccountsPassword } from '@accounts/password'
import { AccountsServer } from '@accounts/server'
import { resolvers } from './resolvers'
import * as typeDefs from './typeDefs.graphql'
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's 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
}
How to Implement Client-Side Using accounts-js, React and Apollo-Client
Now we can create a simple frontend app by using Apollo-Client and accounts-js client for this backend app. The example below shows some example code that works on these two.
import React, { Component } from 'react'
import { render } from 'react-dom'
import ApolloClient from 'apollo-boost'
import gql from 'graphql-tag'
import { ApolloProvider, Mutation, Query } from 'react-apollo'
import { AccountsClient } from '@accounts/client'
import { AccountsClientPassword } from '@accounts/client-password'
import GraphQLClient from '@accounts/graphql-client'
const apolloClient = new ApolloClient({
async request(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 => (
<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.renderAddPost() : this.renderLoginRegister()}
</div>
)
}
}
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 accounts-js 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.
All Posts about GraphQL Modules
- 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 accounts-js & GraphQL Modules
- Manage Circular Imports Hell with GraphQL-Modules
Join our newsletter
Want to hear from us when there's something new?
Sign up and stay up to date!
*By subscribing, you agree with Beehiiv’s Terms of Service and Privacy Policy.