EY-Office ブログ

Apollo GraphQLに入門してみた(3)

Apollo GraphQLに入門してみた(1)Apollo GraphQLに入門してみた(2)の続きです。今回はApollo GraphQサーバーにログイン(認証、承認、セッション管理)機能を追加してみました。

Apollo

またPostgreSQL接続ライブラリーを変更しました

前回 node-postgres(pg)からPostgres.js(postgres)に変更しました。Postgres.jsには不満はないのですが、セッション管理のライブラリー等を調べると新参者のPostgres.jsをサポートしているものはありません。その中でpg-promiseを発見しました!

Postgres.jsほどモダンではありませんが、node-postgres(pg)の以下の問題は解決されています。

  • API, クラス構成が微妙
  • 簡単にログが取れない
  • SQL文にINが書けない

さらにselect用のAPIが戻り値の件数に対し複数あるのも少し便利です、もちろんTypeScriptにも対応しています。

  • none: 戻り値なし、件数が0でなければエラー
  • one: 戻り値が1つ(配列ではなく変数で受け取れる)、件数が1以外ならエラー
  • oneOrNone: 戻り値が1つ(配列ではなく変数で受け取れる)、件数が0ならnullを戻す、件数が1以上ならエラー
  • many: 戻り値が複数(配列)、件数が0ならエラー
  • any(manyOrNone): 戻り値が複数(配列)、件数が0なら空の配列を戻す

ということで、Postgres.jsからpg-promiseに変更しました 😅

Mutationを追加

前回まではGraphQL APIは情報取得のQueryのみでしたが、今回は変更系のMutationを追加してみました。ログイン認証用のuserLogin, userLogout, userRegisterをす(一部未実装)。

  • スキーマ定義
const schema = gql`
   ・・・

type Mutation {
  userLogin(email: String!, password: String!): User
  userLogout: Void
  userRegister(user: UserInput!): User
}

   ・・・

input UserInput {
  name: String!
  email: String!
  password: String!
  group_id: Int!
}

   ・・・

userLogout(): Void と書いてエラーになり悩みました、引数無しの場合は括弧も書かないんですね。
また戻り値無しの場合の書き方も悩みました、Stack Overflowにnullでも良いBooleanと書いたら?とありましたが、graphql-scalarsにVoidが在ったので、Voidを採用しました。

input UserInput { ... }はuserRegisterで使うユーザー情報の型です。inputはこのように引数でしか使わないデータの型定義です。

  • Resolver
type UserLoginArgs = {email:string, password:string}

const resolvers:Resolvers = {
  Query: {

   ・・・

  },
  Mutation: {
    userLogin: async (_parent, {email, password}: UserLoginArgs, context) => {
      const user = await context.db.oneOrNone(
          'SELECT * FROM users WHERE email = $1', email)
      if (user && bcrypt.compareSync(password, user.password)) {
        context.req.session.userId = user.id
        return user
      } else {
        return null
      }
    },
    userLogout: async (_parent, _args, context) => {
      context.req.session.destroy((err: Error) => console.log("---- logout"))
    },
    userRegister:  async (_parent, args, context) => {
      // 未実装
    }
  }
};

ResolverはQueryと同じように書けます。パスワード認証には、攻撃に強いnode.bcrypt.jsを使う事にしました。
セッション管理用のAPIに付いては次回に説明します。

セッション管理はJWTかcookieか?

GraphQLでの認証やセッション管理を調べるとJWT(JSON Web Token)の記事がたくさんできてきます。
JWTはセッション管理やその他情報の入ったJSONを暗号化し、その文字列をHTTP通信のAuthorizationヘッダーに使う事でセッション管理を行います。

従来のCookieセッション管理と比較しJWTの良い点は、

  • 格納できる情報量に制限がない(?)
  • CORS(Cross-Origin Resource Sharing) 違うドメイン名間での通信でも使える
  • CSRF(Cross-Site Request Forgeries) 攻撃は利用されにくい
  • JWT情報の改ざんが検出できる

などがあります。

ただし、JWT情報はクライアント側で保存し、通信毎に送る必要があります。スマフォアプリ等であれば安全にJWT情報を格納できると思いますが、Webアプリの場合に使える保存先としてはLocalStorageくらいで、これを使う例をネット等に見かけますが・・・
LocalStorageはJavaScriptから読み出せるのでアプリにXSS等の脆弱性があると読み出せる可能性があります。またブラウザーのデベロッパー・ツールを使うと簡単に取得できます。

それに対しCookieでのセッション管理は、以下の対策をすれば安全に運用できます。

  • CORS対策を適切に行い、APIサーバーとクライアントWebアプリのダウンロードサイトのみでCookieやJavaScriptの通信できるようにする
  • セッション用クッキーにはhttpOnly属性を付けJavaScriptからはアクセスを禁止する
  • 通信はHTTPSを使う
  • GraphQLを使う(GraphQLはCORSに対応したJSON通信なのでCSRFは起きないらしいです)

ということで、今回はJWTではなく、古典的なCookieでセッション管理を行うことにしました。
これにより、Express用につくられたセッション管理ライブラリーが使えるようになりました。余計なコードを書かないだけでなく、すに実績のある安全なセッション管理ライブラリーを使えるのは嬉しいことです。😁

次回へ続きます・・・

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ