AzureのProfile画像をnext-authで取得する(javascript)

JavaScript

ProviderがGitHubの場合はすんなりavatorの画像URLを取得できるのですが、
microsoftアカウントの場合に取得にちょっと苦労しました。

next-authのデフォルト設定のままでProfileのphotoのURLにアクセスしようとするとエラーになります。

{
  "error": {
    "code": "ErrorInsufficientPermissionsInAccessToken",
    "message": "Exception of type 'Microsoft.Fast.Profile.Core.Exception.ProfileAccessDeniedException' was thrown.",
    "innerError": {
      "date": "2022-12-05T08:03:30",
      "request-id": "XXXXXXXX-YYYY-ZZZZZ-a6cf-XXXXXXXXXXXX",
      "client-request-id": "XXXXXXXX-YYYY-ZZZZZ-a6cf-XXXXXXXXXXXX"
    }
  }
}

結論から言うと、以下のscopeの内 User.Readが足りないためにErrorInsufficientPermissionsInAccessTokenが発生します。

authorization: { params: { scope: 'email openid profile User.Read' } },

必ずしもUser.Readじゃなくてもいいのですが、読み取りできるscopeが必要です。
デフォルトの場合、’email openid profile’ が指定されています。


以下、サンプルコードを載せておきます。
async profile(profile, tokens) の部分はAwaitableクラスを返す必要があるため、このコードそのままの場合はtypescript的にはエラーとなります。が、Awaitableはnext-auth内部のクラスっぽく、ここで型の整合性と取るメリットが乏しいため、私はignoreするなどでエラーを無視します。

pages/api/auth/[...nextAuth].ts

import NextAuth, { NextAuthOptions } from 'next-auth';
import AzureADProvider from 'next-auth/providers/azure-ad';

import { PrismaAdapter } from '@next-auth/prisma-adapter';

import { prisma } from '../../../modules/db';

export const authOptions: NextAuthOptions = {
  adapter: PrismaAdapter(prisma),
  providers: [
    AzureADProvider({
      clientId: process.env.AZURE_AD_CLIENT_ID,
      clientSecret: process.env.AZURE_AD_CLIENT_SECRET,
      tenantId: process.env.AZURE_AD_TENANT_ID,
      authorization: { params: { scope: 'email openid profile User.Read' } },
      async profile(profile, tokens) {
        const profileSize = 48;
        // https://docs.microsoft.com/en-us/graph/api/profilephoto-get?view=graph-rest-1.0#examples
        const profilePicture = await fetch(`https://graph.microsoft.com/v1.0/me/photos/${profileSize}x${profileSize}/$value`, {
          headers: {
            Authorization: `Bearer ${tokens.access_token}`,
          },
        });

        let image: string | null = null;
        if (profilePicture.ok) {
          const pictureBuffer = await profilePicture.arrayBuffer();
          const pictureBase64 = Buffer.from(pictureBuffer).toString('base64');
          image = `data:image/jpeg;base64, ${pictureBase64}`;
        }

        return {
          id: profile.sub,
          name: profile.name,
          email: profile.email,
          image,
        };
      },
    }),
  ],

  callbacks: {
    async session({ session, user }) {
      session.user.image = user.image ?? '';
      return session;
    },
  },
};

export default NextAuth(authOptions);
next-auth.d.ts

import { DefaultSession } from 'next-auth';

declare module 'next-auth' {
  interface Session {
    user: {
      image: string;
    } & DefaultSession['user'];
  }
}

型定義ファイルにはuseSessionで戻ったオブジェクトに正確な型情報が欲しいため、上記を追加します。

コメント