NextAuthを使ってSpotifyログイン機能を実装

SpotifyAPIを使ってみようと思い、調べたことをメモとして書いていこうと思います。

## NextAuthとは
Next.js に認証機能を簡単に実装できるライブラリのことです。
今回私が利用した、SPotifyだけでなくGoogleやGitHubのアカウントを使用したソーシャルログインを組み込むことができます。

https://next-auth.js.org/

## Spotifyログイン機能の実装

### [Spotify for Developers](https://developer.spotify.com/dashboard/applications)のDashboradページでAppの作成をする。

赤丸のところをクリックすると作成できます。
![スクリーンショット 2022-01-26 16.23.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2388688/c5111a8e-cd2d-b0b5-8666-bca07ee6c4c4.png)

以下の画面になるので、Appの名前と説明を記入してCREATEをクリックします。



![スクリーンショット 2022-01-26 16.25.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2388688/6bd4cd31-9801-0c3c-15fb-3758e1cc8c12.png)

すると自分のAppが作成されて使えるようになります。
ClientIDとClient Secretがこれからの実装で必要になります。

![スクリーンショット 2022-01-26 16.28.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2388688/8ffde589-c2e0-720c-26c3-11924552d676.png)

### 実際にコードを書いていく

ディレクトリ構造は以下のようになっています。

```
root/
　├ src/
　│　└ pages/
　│　　　　　　　　└ api/
　│　　　　　　　　　　　　└ auth/
　│　　　　　　　　　　　　　└ [...nextauth].js
　│　　　　　　　　└ auth/
　│　　　　　　　　　　　　　└ _signin.tsx
　│　└ _app.tsx
　│　└ index.tsx
　├ .env
```
#### .envのコード

.envファイルには公開しない内容を書いていきます。
Client IDとClient Secretは上のSpotify for developersのページで作成したApp内で発行されたものになります。
NEXTAUTH_URLは開発環境のURLを書いておきます。

```typescript:.env
SPOTIFY_CLIENT_ID=自分の Client Id
SPOTIFY_CLIENT_SECRET=自分のClient secret
NEXTAUTH_URL=http://localhost:3000
```

#### src/pages/api/auth/[...nextauth].jsのコード


最終的には以下のようになりました。
ほとんどNextAuthの公式ドキュメントに従って実装しました。

https://next-auth.js.org/v3/tutorials/refresh-token-rotation

```typescript:src/pages/api/auth/[...nextauth].js
import NextAuth from "next-auth";
import SpotifyProvider from "next-auth/providers/spotify";

//NextAuth Refresh Token Rotationからコピー。
/**
 * Takes a token, and returns a new token with updated
 * `accessToken` and `accessTokenExpires`. If an error occurs,
 * returns the old token and an error property
 */
async function refreshAccessToken(token) {
  try {
    const url =
      "https://accounts.spotify.com/api/token?" +
      new URLSearchParams({
        client_id: process.env.SPOTIFY_CLIENT_ID,
        client_secret: process.env.SPOTIFY_CLIENT_SECRET,
        grant_type: "refresh_token",
        refresh_token: token.refreshToken,
      });

    const response = await fetch(url, {
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
      },
      method: "POST",
    });

    const refreshedTokens = await response.json();

    if (!response.ok) {
      throw refreshedTokens;
    }

    return {
      ...token,
      accessToken: refreshedTokens.access_token,
      accessTokenExpires: Date.now() + refreshedTokens.expires_in * 1000,
      refreshToken: refreshedTokens.refresh_token ?? token.refreshToken, // Fall back to old refresh token
    };
  } catch (error) {
    console.log(error);

    return {
      ...token,
      error: "RefreshAccessTokenError",
    };
  }
}

export default NextAuth({
  // 1つまたは複数の認証プロバイダを設定する
  providers: [
    SpotifyProvider({
      clientId: process.env.SPOTIFY_CLIENT_ID,
      clientSecret: process.env.SPOTIFY_CLIENT_SECRET,
      authorization:
        "https://accounts.spotify.com/authorize?scope=user-read-email,playlist-read-private,user-read-email,streaming,user-read-private,user-library-read,user-library-modify,user-read-playback-state,user-modify-playback-state,user-read-recently-played,user-follow-read",
    }),
    // ...他にも認証プロバイダがあればここから追加する。
  ],
  pages: {
    signIn: "/auth/signin",
  },
  //NextAuth Refresh Token Rotationからコピー。
  callbacks: {
    //JWT(JSON Web Token)
    async jwt({ token, user, account }) {
      // 最初のサインイン
      if (account && user) {
        return {
          accessToken: account.access_token,
          accessTokenExpires: Date.now() + account.expires_in * 1000,
          refreshToken: account.refresh_token,
          user,
        };
      }

      // アクセストークンの期限が切れていなかったら前のトークンを返す。
      if (Date.now() < token.accessTokenExpires) {
        return token;
      }

      // アクセストークンが切れていたら更新する。
      return refreshAccessToken(token);
    },
    async session({ session, token }) {
      session.user = token.user;
      session.accessToken = token.accessToken;
      session.error = token.error;

      return session;
    },
  },
});

```


##### SpotifyProviderのauthorizationとは

ここで認証のスコープオプションをつけることができます。
ここで共有することを設定した情報のみが共有されるようになります。
例えば、ユーザーがお気に入り登録した曲の情報を取得するかどうかの設定をすることができます。

https://developer.spotify.com/documentation/general/guides/authorization/scopes/


##### Callbackとは

![スクリーンショット 2022-01-26 17.43.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2388688/677e12ac-ede1-51e2-2dcd-706aa414bce7.png)

https://next-auth.js.org/configuration/callbacks#jwt-callback

コールバックは、あるアクションが実行されたときに何が起こるかを制御するために使用できる非同期関数のことです。
今回のコードでは、アクセストークンのステータスによって実行される処理を書いています。




#### src/pages/auth/signin.tsxのコード

最終的には以下のようになりました。


```typescript:src/pages/auth/signin.tsx

import { getProviders, signIn, useSession } from "next-auth/react";
import Head from "next/head";
import Image from "next/image";
import { useRouter } from "next/router";
import { useEffect } from "react";

import { Loader } from "../../components/Loader";

function Signin({ providers }: any) {
  // useSessionで session 情報を保持する変数を作成
  const { data: session } = useSession();
  const router = useRouter();

  useEffect(() => {
    if (session) {
      router.push("/");
    }
  }, [session]);

  if (session) {
    <Loader />;
  }

  return (
    <div className="bg-black h-screen flex flex-col items-center pt-40 space-y-8">
      <Head>
        <title>Login Music-player</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <Image
        src="/spotifylogo.png"
        height={250}
        width={600}
        objectFit="contain"
        className="animate-pulse"
      />
      {/* Object.valueでprovidersを配列にする。 */}
      {Object.values(providers).map((provider: any) => (
        <div key={provider.name}>
          <button
            onClick={() => signIn(provider.id)}
            className="text-white py-4 px-6 rounded-full bg-[#1db954] transition duration-300 ease-out border border-transparent uppercase font-bold text-xs md:text-base tracking-wider hover:scale-105 hover:bg-[#0db146]"
          >
            Sign in with {provider.name}
          </button>
        </div>
      ))}
    </div>
  );
}

export default Signin;

// getServerSidePropsはクライアントからのアクセス時にサーバ側でデータを取得しpre-Renderingする。
export async function getServerSideProps() {
  const providers = await getProviders();
  return {
    props: { providers },
  };
}

```

##### useSessionとは
![スクリーンショット 2022-01-26 16.51.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2388688/798c2a92-4d97-5955-c08d-862a76ced52e.png)

https://next-auth.js.org/getting-started/client


useSessionではサインイン情報を取得することができる。
useSessionは`data`と`status`のvalueを返す。
　　・data:Session, undefined, nullの３つのどれかになる。
　　　　　　　・セッションがまだ取得されていない時は、`data`は`undefined`になる。
　　　　　　　・セッションの取得に失敗した時は、`data`は`null`になる。
　　　　　　　・セッションの取得に成功した時は、`data`は`session`になる。
　　・data:loading, authenticated, unauthenticated"の３つのどれかのセッション状態にマッピングする列挙型。
  
##### getgetServerSidePropsとは

![スクリーンショット 2022-01-26 17.13.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2388688/dae04eec-8e6d-cc92-2d3f-d3fa00f0aa42.png)
![スクリーンショット 2022-01-26 17.20.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2388688/714fcbe0-07f1-fcf1-664b-757eceb92c37.png)


https://nextjs-ja-translation-docs.vercel.app/docs/basic-features/data-fetching#getserversideprops-server-side-rendering

##### getProvidersとは

![スクリーンショット 2022-01-26 17.22.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2388688/6c159c5d-5b94-c75b-5164-0e4f7ab37a69.png)

https://next-auth.js.org/v3/getting-started/client#getproviders

getProviders()メソッドは、/api/auth/providers を呼び出し現在サインイン用に設定されている認証プロバイダの一覧を返します。これは、動的なカスタムサインインページを作成している場合に便利です。

#### src/pages/_app.tsxのコード

Providerの設定をします。

```typescript:src/pages/_app.tsx
import "../../styles/globals.css";
import type { AppProps } from "next/app";
import { SessionProvider } from "next-auth/react";

function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
  return (
    <SessionProvider session={session}>
      <Component {...pageProps} />
    </SessionProvider>
  );
}

export default MyApp;


```

#### src/pages/index.tsxのコード

ルートフォルダにアクセスしたユーザーが認証されたユーザーかどうかを調べて認証されていなかったら、認証ページに飛ばすように設定します。

```typescript:src/pages/_app.tsx

import Head from "next/head";

import { useRouter } from "next/router";
import { useSession } from "next-auth/react";
import { Loader } from "../components/Loader";

export default function Home() {
  const router = useRouter();
  // useSessionで session 情報と status 状態を保持する変数を作成
  const { status } = useSession({
    required: true,
    onUnauthenticated() {
      // 認証されていないのでサインインページに飛ばす。
      router.push("/auth/signin");
    },
  });

  if (status === "loading") {
    return <Loader />;
  }

  return (
    <div>
      <Head>
        <title>Music-player</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <Dashboard />
    </div>
  );
}
```

#### useSessionの使い方

![スクリーンショット 2022-01-27 10.16.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2388688/8df2fbdd-badd-c56a-a3fb-0b48e42ed4ff.png)

getServerSidePropsとgetInitialPropsを処理する方法では、セッションが有効かどうかをチェックしてサーバーサイドリクエストを行う必要があり、サーバーの負荷が高まります。useSessionを使えば、常に有効なセッションを持つようにクライアントからリクエストを送ることができるようになります。
デフォルトの動作は、ユーザーをサインインページにリダイレクトし、そこから（ログインに成功した後）、元のページに送り返すことができます。


## 最後に

以上がNextAuthを利用したSpotifyログイン認証の方法です。
調べながら実装したので、間違っているところがあるかもしれません。
プロバイダの記述を変更すれば、他のソーシャルログインも使えるようになるそうです。