Fullstack React GraphQL TypeScript Tutorial をやってみた #4 ~redisにsession情報を保存し、cookieを使ってログイン状態を保持するまで~

# はじめに
30代未経験からエンジニアを目指して勉強中のYNと申します。
この記事は`Ben Awad`さんの[Fullstack React GraphQL TypeScript Tutorial](https://youtu.be/I6ypD7qv3Z8)を初学者が進めていく、という内容です。
`Ben`さんの動画は本当に質が高く、とても学びが多いのですが、自分のような初学者は躓きが多く、なかなか前に進まなかったので、振り返りのメモとして書きます。

# 今回の対象
動画の下記内容までです。
1:41:11 [Session Authentication](https://youtu.be/I6ypD7qv3Z8?t=6071)
2:03:06 [Sessions Explained](https://youtu.be/I6ypD7qv3Z8?t=7386)

前回 => https://qiita.com/theFirstPenguin/items/6dde647c01dad4e96e22

#始める前に

### ブランチをコピー
動画の内容ごとに細かく[ブランチ](https://github.com/benawad/lireddit/tree/6_sessions)を切ってくれています。ありがたや。
まずはブランチをローカルにコピーして、そのブランチに移ります。

```
git pull origin 6_sessions:6_sessions
git checkout 6_sessions
```

### まずは全体像を把握する。
今回はsession情報をredisに保存し、ブラウザにcookieを保存してもらうことで、一定時間ログイン状態を保持する実装を行います。
今回はsession情報の保存にredisを用いていますが、key-valueストアであれば、mongoDBまどでも構いません。redisはインメモリデータベースなので応答が速いというメリットがあります。
下図に今回の全体像を描いてみました。
![スクリーンショット 2020-11-12 18.55.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572098/3cf997c4-96fc-154d-2b5a-9c47800c2fbf.png)
![スクリーンショット 2020-11-12 18.55.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572098/56da430a-f5a5-5122-41e8-ba3c4fc74d54.png)


# Session Authenticationを実装する
上の全体図の通り、session情報をredisに保存して、そのkeyをcookieとしてクライアントに渡すことで、Authenticationを実装できます。

### redisとexpress-sessionをインストール
[動画のこの部分です](https://youtu.be/I6ypD7qv3Z8?t=6175)

まずはredisとexpress-sessionをインストールします。

```
brew install redis
yarn add redis connect-redis express-session
yarn add -D @types/redis @types/express-session @types/connect-redis
redis-server #redisを起動
```

### express/sessionとconnect-redisをexpressのミドルウェアに設定
[express-session](https://github.com/expressjs/session)の使い方は[こちらの記事](https://qiita.com/hika7719/items/3282ab2ebcdaf080912e)が分かりやすかったです。

> * secret属性は指定した文字列を使ってクッキーIDを暗号化しクッキーIDが書き換えらているかを判断する。
> * resaveはセッションにアクセスすると上書きされるオプションらしい。今回はfalse.
> * saveUninitializedは未初期化状態のセッションも保存するようなオプション。今回はfalse.
> * httpOnlyはクライアント側でクッキー値を見れない、書きかえれないようにするオプション
> * secureオプションはhttpsで使用する場合はtrueにする。今回はhttp通信なのでfalse
> * maxageはセッションの消滅時間。単位はミリ秒。

```src/index.ts
import redis from "redis";
import session from "express-session";
import connectRedis from "connect-redis";

...
const app = express();

  const RedisStore = connectRedis(session);
  const redisClient = redis.createClient();

  app.use(
    session({
      name: "qid",
      store: new RedisStore({
        client: redisClient,
        disableTouch: true,
      }),
      cookie: {
        maxAge: 1000 * 60 * 60 * 24 * 365 * 10, // 10 years
        httpOnly: true,
        sameSite: "lax", // csrf
        secure: __prod__, // cookie only works in https
      },
      saveUninitialized: false,
      secret: "qowiueojwojfalksdjoqiwueo",
      resave: false,
    })
  );

...
  apolloServer.applyMiddleware({ app });
...

```
上のコードを全体図に当てはめるとこんな感じでしょうか。
![スクリーンショット 2020-11-12 20.42.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572098/75ec93f4-4bb5-c731-f1e8-2885be07f3de.png)


### contextプロパティにreq/resを追加してresolver間で共有する
[動画のこの部分です](https://youtu.be/I6ypD7qv3Z8?t=6652)

ApolloServerミドルウェアのcontextプロパティは関数をしていすることができ、その返り値のオブジェクトはresolver間で共有できます。さらに、関数の引数はクライアントからのリクエストとレスポンスをとることができます。

```src/index.ts
...
 const apolloServer = new ApolloServer({
    schema: await buildSchema({
      resolvers: [HelloResolver, PostResolver, UserResolver],
      validate: false,
    }),
    context: ({ req, res }): MyContext => ({ em: orm.em, req, res }), // contextプロパティからリクエスト情報を取り込む
  });

  apolloServer.applyMiddleware({ app });
...

```
そして取り込んだ`req`と`res`にも型を規定します。

```src/types.ts
import { EntityManager, IDatabaseDriver, Connection } from "@mikro-orm/core";
import { Request, Response } from "express";

export type MyContext = {
  em: EntityManager<any> & EntityManager<IDatabaseDriver<Connection>>;
  req: Request;
  res: Response;
};
```

### resolverでsession情報を保存する
[動画のこの部分です](https://youtu.be/I6ypD7qv3Z8?t=6827)
resolverで、user登録後にログイン情報を保持するための実装をします。
ここでいきなり`req.session`が出てきて戸惑いましたが、`req: Request & { session: Express.Session }`によって`Request`型に`session`プロパティを追加しています。つまり`req.session`は`Express.Session`と同じ型になります。

```src/types.ts
import { EntityManager, IDatabaseDriver, Connection } from "@mikro-orm/core";
import { Request, Response } from "express";

export type MyContext = {
  em: EntityManager<any> & EntityManager<IDatabaseDriver<Connection>>;
  req: Request & { session: Express.Session }; // ここでsessionの型を規定
  res: Response;
};
```
そしてユーザー登録時にredisにsession情報を格納し、その情報を
cookieとしてクライアントに返信する処理を実装します。
express-sessionをミドルウェアとして使えば、この一連の作業は驚くほど簡単に実装でき、中で何が起きているかを意識する必要がありません。
![スクリーンショット 2020-11-12 22.02.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572098/75aa78db-3228-4a42-8b55-7d05b6f9c904.png)

```src/resolvers/users.ts
...

  @Mutation(() => UserResponse)
  async register(
    @Arg("options") options: UsernamePasswordInput,
    @Ctx() { em, req }: MyContext // contextプロパティからリクエスト情報をとってくる
  ): Promise<UserResponse> {

   ...

    const hashedPassword = await argon2.hash(options.password);
    const user = em.create(User, {
      username: options.username,
      password: hashedPassword,
    });
    try {
      await em.persistAndFlush(user);
    } catch (err) {
      ...
    }

    // store user id session
    // this will set a cookie on the user
    // keep them logged in
    req.session.userId = user.id; // このたった１行で上図の④と⑤をやってくれる

    return { user };
  }
...
```
ところで、`req.session.userId`の型は指定しなくていいの？と思いましたが、これを右クリックして`Go to Type Definition`を押してみると、node_modulesの中の`index.d.ts`にジャンプします。これを見ると、`req.session`はSession型(interface)であり、Session型はSessionData型を継承しており、SessionData型は`[key: string]: any`という、プロパティを自由に受け付ける構造になっていることが分かります。TypeScriptっておもしろいですね。

```index.d.ts
...
declare global {
  namespace Express {
    ...

    interface SessionData {
      [key: string]: any; // ここでuserIdのような型を自由に追加できる構造になっている
      cookie: SessionCookieData;
    }
    ...

    interface Session extends SessionData {
      id: string;
      regenerate(callback: (err: any) => void): void;
      destroy(callback: (err: any) => void): void;
      reload(callback: (err: any) => void): void;
      save(callback: (err: any) => void): void;
      touch(): void;
      cookie: SessionCookie;
    }
...
```
### ブラウザからcookie情報を受け取りログイン保持する
[動画のこの部分です](https://youtu.be/I6ypD7qv3Z8?t=7119)
一度ログインしたあと、cookieの有効期限(`maxAge`)が過ぎるまでブラウザにcookieが保存されるため、passwordなしでユーザを認証することができます。
このとき、cookieの復号など面倒なことは裏でexpress-sessionミドルウェアがやってくれるみたいです。ありがたいことです。

![スクリーンショット 2020-11-12 21.49.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572098/a2c387f1-155b-3bdc-b369-4379ee4ff912.png)

```src/resolvers/user.ts
...
@Resolver()
export class UserResolver {
  @Query(() => User, { nullable: true })
  async me(@Ctx() { req, em }: MyContext) { // contextオブジェクトからリクエストに含まれる情報を取り込む
    // you are not logged in
    if (!req.session.userId) {
      return null;
    }

    const user = await em.findOne(User, { id: req.session.userId }); // リクエストに含まれるcookieを秘密鍵で復号し、redisからuserIdを読み込む
    return user;
  }
...
```

# 最後に
今回は`Sessions Explained`まで書きました。
今さらsessionやcookieの有用性を知り、学びが多かったです。(~~[今までLocalStorageでログイン情報保持してました。。。](https://techracho.bpsinc.jp/hachi8833/2019_10_09/80851)~~)


チュートリアル全14時間のうち、2時間ちょっと進みました。
先は長い。。。

次回=>