Authentication with accounts-js & GraphQL Modules

TheGuildBot - Jul 22 '21 - - Dev Community

This article was published on Friday, November 16, 2018 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
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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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')
)
Enter fullscreen mode Exit fullscreen mode

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

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player