# HOW TO GRAPHQLでGraphQLの勉強をはじめました その⑧

# Authentication

サインアップとログインをGraphQLでやってみようってことらしい

# Adding a User model

ということで、PrismaデータモデルにUser定義を追加してデータベースにそれを保存して〜といったところ。またユーザーとリンクを紐付けておきます、と(どのユーザーがどのリンクを登録したのか的な)。 👇 datamodel.prisma。postedByでLinkとUserを紐付ける形。Userは自分が登録したリンクの配列を持つ。

type Link {
  id: ID! @id
  description: String!
  url: String!
  postedBy: User
}

type User {
  id: ID! @id
  name: String!
  email: String! @unique
  password: String!
  links: [Link!]!
}

ということで、これをデプロイしていきます。

$ prisma deploy
Deploying service `hackernews-node` to stage `dev` to server `prisma-us1` 1.2s

Changes:

  User (Type)
  + Created type `User`
  + Created field `id` of type `ID!`
  + Created field `description` of type `String!`
  + Created field `url` of type `String!`
  + Created field `postedBy` of type `User`

  Link (Type)
  + Created field `postedBy` of type `User`

  UserToUser (Relation)
  + Created an inline relation between `User` and `User` in the column `postedBy` of table `User`

  LinkToUser (Relation)
  + Created an inline relation between `Link` and `User` in the column `postedBy` of table `Link`

# Extending the GraphQL schema

スキーマドリブンな開発ということで、今回も signup と login のmutationの定義から。

type Mutation {
  post(url: String!, description: String!): Link!
  signup(email: String!, password: String!, name: String!): AuthPayload
  login(email: String!, password: String!): AuthPayload
}

AuthPayloadってなんぞや?ってところなのですが、Userと一緒にtypeを定義します。

type AuthPayload {
  token: String
  user: User
}

type User {
  id: ID!
  name: String!
  email: String!
  links: [Link!]!
}

signupとloginのmutationは共に似ていて、誰(User)がsignupしたのか?もしくはloginしたのか?をtokenという認証されているかどうかに使うものと一緒にAuthPayloadとしてバンドル。

そして最後にUserとLinkを関連付けさせるためのpostedByを定義。

type Link {
  id: ID!
  description: String!
  url: String!
  postedBy: User
}

# Implementing the resolver functions

スキーマ定義をしたのでリゾルバファンクションの開発を進めていきます。今回もまたリファクタリングから。

リゾルバ用のディレクトリを作ってそれぞれのJSファイルを作っていきます。

mkdir src/resolvers
touch src/resolvers/Query.js
touch src/resolvers/Mutation.js
touch src/resolvers/User.js
touch src/resolvers/Link.js

feedリゾルバをQuery.jsに移していきます。

function feed(parent, args, context, info) {
  return context.prisma.links()
}

module.exports = {
  feed,
}

# Adding authentication resolvers

ここまではただ移してきただけでしたが、いよいよ新しい実装を入れていきます。Mutationのリゾルバはパッ見難しそうですが、解説はソースコードのコメントに載せてみました。

async function signup(parent, args, context, info) {
  // この後インストールするbcryptjsを使って暗号化
  const password = await bcrypt.hash(args.password, 10)

  // Prismaクライアントを使ってDBに永続化
  const user = await context.prisma.createUser({...args, password})

  // JWT(Jason Web Token)のために後でjwtをインストールする。それ用にAPP_SECRETが必要に
  const token = jwt.sign({ userId: user.id}, APP_SECRET)

  // スキーマ定義したAuthPayloadオブジェクトとしてトークンとユーザーを返す
  return {
    token,
    user,
  }
}

async function login(parent, args, context, info) {
  // ログインなので新しいユーザーを作るわけではなくPrismaクライアントを使って既存ユーザーを取得
  const user = await context.prisma.user({ email: args.email })
  if (!user) {
    throw new Error('No such user found')
  }

  // 入力したクレデンシャルが一致しているか確認。違っていればエラー
  const valid = await bcrypt.compare(args.password, user.password)
  if (!valid) {
    throw new Error('Invalid password')
  }

  const token = jwt.sign({ userId: user.id}, APP_SECRET)

  // トークンとユーザーをサインアップと同じようにリターン
  return {
    token,
    user,
  }
}

module.exports = {
  signup,
  login,
  post,
}

jwtとbycryptを入れていきます。

$ yarn add jsonwebtoken bcryptjs

色んなところで使うものはutils.jsに入れていくみたいです。

touch src/utils.js
const jwt = require('jsonwebtoken')
const APP_SECRET = 'GraphQL-is-aw3some'

function getUserId(context) {
  const Authorization = context.request.get('Authorization')
  if (Authorization) {
    const token = Authorization.replace('Bearer ', '')
    const { userId } = jwt.verify(token, APP_SECRET)
    return userId
  }

  throw new Error('Not authenticated')
}

module.exports = {
  APP_SECRET,
  getUserId,
}

APP_SECRETはユーザーに発行するJWTにサインするのに使われるもの。getUserIdはヘルパーファンクションで認証されている必要があるもの(例えばログインしている人だけが出来るpost)に使われる。 Authrizationヘッダー(UserのJWTに含まれる)をcontextから取得して、JWTをverifyしてUser IDを取得する。もしどこかで何かあればexceptionをthrowする。これによって認証が必要なリゾルバを守ることが出来るという流れ。

mutationに諸々インポートしていきます。

const bcrypt = require('bcryptjs')
const jwt = require('jsonwebtoken')
const { APP_SECRET, getUserId } = require('../utils')

ここで一つマイナーな問題があります。contextの中のrequestオブジェクトにアクセスしようとしています。しかし、contextにはイニシャライズのタイミングではprismaクライアントしかありません。なのでアクセス出来ないということになってしまいます。

ということで、index.jsでGraphQLサーバーのイニシャライズを以下のようにしていきます。

const server = new GraphQLServer({
  typeDefs: './src/schema.graphql',
  resolvers,
  context: request => {
    return {
      ...request,
      prisma,
    }
  },
})

オブジェクトをダイレクトにアタッチするのではなく、functionとしてcontextを返すようにしました(個人的にはこの辺の感覚がモダンなJSに馴染みがなくてまだしっくりきてない。笑)。GraphQLクエリやミューテーションが入っているHTTPリクエストをcontextにアタッチというのがミソ的な。これによってAuthorizationヘッダにアクセスできるので、そこでvalidationをしてちゃんとしたリクエストなのかを評価できる。

# Requiring authentication for the post mutation

テストをする前に、schema/resolverのセットアップがちゃんと出来ているか確認していきましょう。今はまだ post resolverがMutation.jsに不足しています。

function post(parent, args, context, info) {
  const userId = getUserId(context)
  return context.prisma.createLink({
    url: args.url,
    description: args.description,
    postedBy: { connect: {id: userId} },
  })
}

2点、以前のindex.jsから変化があります。 1つ目は、getUserIdファンクションを使ってユーザーのIDを取得しているところ。このIDはJWTの中にあって、それはHTTPリクエストのAuthorizationヘッダーに含まれるものです。これによってどのユーザーがLinkを登録したのかが分かります。もしユーザーIDが取得できなければexceptionを投げるのは上記の通りです。 2つ目は、誰がLinkを作成したかという紐付けとして、userIdとLinkがconnectされているところ。nested object writeが行われるとのことです。

# resolving relations

GraphQLサーバーを立ち上げる前にUserとLinkが正しく関係付けされているか確認していきます。

どのように全てのスカラ値をリゾルバの中のUserとLinkから取り除いてきたでしょうか?以下はチュートリアルの最初で見てきたシンプルなパターンです。

Link: {
  id: parent => parent.id,
  url: parent => parent.url,
  description: parent => parent.description,
}

私たちは2つのフィールドをGraphQLのスキーマに追加しました。これは同じやり方では解決できないものです: LinkのpostedByと、Userのlinksです。これらのフィールドはGraphQLサーバーが自身でデータの取得は出来ないので個別の実装が必要になります。Link.jsに以下を。

function postedBy(parent, args, context) {
  return context.prisma.link({ id: parent.id }).postedBy()
}

module.exports = {
  postedBy,
}

postedByリゾルバでは、最初にLinkをprismaを使って取得してからpostedByを実行します。リゾルバがpostedByを呼ばなければならないのは、postedByフィールドはLinkタイプとしてschema.graphqlで定義されているからです。

linksのリレーションについても同じようなアプローチで解決できます。

function links(parent, args, context) {
  return context.prisma.user({ id: parent.id }).links()
}

module.exports = {
  links,
}

# Putting it all together

とういことで、最後にindex.jsを今まで作ってきたモノたちを使うようにしていきます

const Query = require('./resolvers/Query')
const Mutation = require('./resolvers/Mutation')
const User = require('./resolvers/User')
const Link = require('./resolvers/Link')

const resolvers = {
  Query,
  Mutation,
  User,
  Link
}

# Testing the authentication flow

サーバーを再起動してやっていきます。

mutation {
  signup(
    name: "Alice"
    email: "alice@prisma.io"
    password: "graphql"
  ) {
    token
    user {
      id
    }
  }
}

👆のMutationをしたら、エラーになってしまいました。笑

{
  "data": {
    "signup": null
  },
  "errors": [
    {
      "message": "Variable '$data' expected value of type 'UserCreateInput!' but got: {\"email\":\"alice@prisma.io\",\"password\":\"$2a$10$Ccd1Fx/qXeBi6wWAx7kuh.wh48da6Sn88MdLac6eLQUQPfBBcqxBW\",\"name\":\"Alice\"}. Reason: 'description' Expected non-null value, found null. (line 1, column 11):\nmutation ($data: UserCreateInput!) {\n          ^",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "signup"
      ]
    }
  ]
}

datamodel.prismaのUserのemailの定義が自分のコードで激しく間違ってた。。笑

email: String! @unique

prisma deployをやり直してようやく👇のようにユーザー登録できました〜

createuser

サーバーからのレスポンスからトークンをコピーして

{
  "data": {
    "signup": {
      "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjazd3c2dmeGc1MGpuMDk4MXFmd2RobnFjIiwiaWF0IjoxNTg0NTAzNDcyfQ.bu7Y8nSW-1dcDE4NELW_gZuhoCiIcIquJTVonhsLj88",
      "user": {
        "id": "ck7wsgfxg50jn0981qfwdhnqc"
      }
    }
  }
}

👇の TOKEN を置き換えて、Prisma Playgroundの左下のHTTP HEADERSに加えておけば、HTTPリクエストのAuthorizationヘッダーにのる感じになります。

{
  "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjazd3c2dmeGc1MGpuMDk4MXFmd2RobnFjIiwiaWF0IjoxNTg0NTAzNDcyfQ.bu7Y8nSW-1dcDE4NELW_gZuhoCiIcIquJTVonhsLj88"
}

ということで、postのMutationをしてみましょう。

mutation {
  post(
    url: "www.graphqlconf.org"
    description: "An awesome GraphQL conference"
  ) {
    id
  }
}

Authorizationヘッダがない場合は、"Not authenticated"エラーが返りますが、

Authorization

としてやることで、記事の登録がされて、IDが返ってきます。

{
  "data": {
    "post": {
      "id": "ck7wtmk2451wt0981jym4fc5x"
    }
  }
}

今まで行われたことを確認するために、ログインmutationを叩いてみます。

mutation {
  login(
    email: "alice@prisma.io"
    password: "graphql"
  ) {
    token
    user {
      email
      links {
        url
        description
      }
    }
  }
}

レスポンスは以下のように。

{
  "data": {
    "login": {
      "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjazd3c2dmeGc1MGpuMDk4MXFmd2RobnFjIiwiaWF0IjoxNTg0NTA1NTczfQ.CFfAWX1uD-XJxlBH64ZTTDOJBINIbn3nd6URoTRgFnA",
      "user": {
        "email": "alice@prisma.io",
        "links": [
          {
            "url": "www.graphqlconf.org",
            "description": "An awesome GraphQL conference"
          }
        ]
      }
    }
  }
}

今回は結構内容も高度だったし、復習が必要かもしれない予感 😃

このエントリーをはてなブックマークに追加

Algolia検索からの流入のみConversionボタン表示