next-auth icon indicating copy to clipboard operation
next-auth copied to clipboard

Azure AD Provider - Profile image doesn't work

Open DavidIlie opened this issue 2 years ago • 3 comments

Provider type

Azure Active Directory

Environment

System: OS: macOS 12.5.1 CPU: (8) arm64 Apple M2 Memory: 2.47 GB / 24.00 GB Shell: 3.5.1 - /opt/homebrew/bin/fish Binaries: Node: 16.13.0 - ~/.nvm/versions/node/v16.13.0/bin/node Yarn: 1.22.19 - ~/.nvm/versions/node/v16.13.0/bin/yarn npm: 8.1.0 - ~/.nvm/versions/node/v16.13.0/bin/npm Browsers: Chrome: 105.0.5195.125 Safari: 15.6.1

Reproduction URL

https://github.com/DavidIlie/next-auth-azure-ad-problem

Describe the issue

Profile image request results in an error using the auth token from Microsoft Oauth and therefore there is no profile picture:

image
AzureADProvider({
    clientId: process.env.AUTH_AZURE_CLIENT_ID as string,
    clientSecret: process.env.AUTH_AZURE_CLIENT_SECRET as string,
    tenantId: process.env.AUTH_AZURE_TENANT_ID as string,
    id: "microsoft",
}),

How to reproduce

Simply create a Azure AD Provider from the guide and you will see that there is no profile picture

Expected behavior

There should be a profile picture fetched and saved in base64 in the JWT or the database (depends on the config)

DavidIlie avatar Sep 18 '22 09:09 DavidIlie

Nevermind, apparently the users in the Azure Application "users" tab needed to have the profile pic put on its own, however, how would I be able to get it directly from the Microsoft account?

DavidIlie avatar Sep 18 '22 10:09 DavidIlie

Not sure if you can get it directly from Next Auth, but personally I use @microsoft/microsoft-graph-client and use the Graph API. You can get the user's picture like that as well: /me/photo/$value. I made a custom authentication provider that uses the refresh token from Next Auth to get a new access token and uses that for Graph API calls.

marcelherd avatar Sep 20 '22 13:09 marcelherd

Could you send me a code snippet of your provider? It would help a lot, thank you!

DavidIlie avatar Sep 21 '22 10:09 DavidIlie

My NextAuth config looks something like this:

const adapter = PrismaAdapter(prisma);

export default NextAuth({
  adapter,
  providers: [
    AzureADProvider({
      clientId: env.AZURE_AD_CLIENT_ID,
      clientSecret: env.AZURE_AD_CLIENT_SECRET,
      tenantId: env.AZURE_AD_TENANT_ID,
      authorization: {
        params: {
          scope: "openid profile email offline_access",
        },
      },
    }),
  ],
  session: {
    maxAge: 12 * 60 * 60, // + custom lifetime policy assigned to the application in AzureAD
  },
  callbacks: {
    async session({ session, user }) {
      if (session.user) {
        session.user.id = user.id;
      }
      return session;
    },
    async signIn({ user, account, profile }) {
      if (!profile.preferred_username) {
        return;
      }

      if (user && adapter) {
        const userFromDatabase = await adapter.getUser(user.id);
        if (userFromDatabase) {
          await prisma.account.update({
            where: {
              provider_providerAccountId: {
                provider: account.provider,
                providerAccountId: account.providerAccountId,
              },
            },
            data: {
              access_token: account.access_token,
              expires_at: account.expires_at,
              id_token: account.id_token,
              refresh_token: account.refresh_token,
              session_state: account.session_state,
              scope: account.scope,
            },
          });
        }
      }

      return true;
    },
  },
});

and my authentication provider looks roughly like this:

import { User } from "@prisma/client";

export default class MyAuthenticationProvider
  implements AuthenticationProvider
{
  constructor(private readonly user: User) {}

  public async getAccessToken(): Promise<string> {
    const account = prisma.account.findFirst({
      where: {
        provider: "azure-ad",
        user: {
          id: this.user.id,
        },
      },
    });

    if (!account) {
      throw new Error("...");
    }

    const accessToken = account.access_token;
    const refreshToken = account.refresh_token;

    const requestBody = new URLSearchParams({
      client_id: "...",
      scope: "openid profile email offline_access",
      redirect_uri: "http://localhost:3000/api/auth/callback/azure-ad",
      grant_type: "refresh_token",
      client_secret: "...",
      refresh_token: refreshToken ?? "",
    });

    const response = await axios.post(
      "your-aad-tenant/oauth2/v2.0/token",
      requestBody
    );

    const newToken = response.data?.access_token;

    if (!newToken && !accessToken) throw new Error("...");

    return newToken ?? accessToken;
  }
}

marcelherd avatar Sep 26 '22 14:09 marcelherd

I'm having the same problem. I have a profile picture for my Microsoft account, but it looks like I'm getting a 403 Forbidden response during the profile() option (copied from next-auth's default profile option for AD)

async profile(profile, tokens) {
  // 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/${profilePhotoSize}x${profilePhotoSize}/$value`,
    {
      headers: {
        Authorization: `Bearer ${tokens.access_token}`,
      },
    }
  )

  // Confirm that profile photo was returned
  if (profilePicture.ok) {
    const pictureBuffer = await profilePicture.arrayBuffer()
    const pictureBase64 = Buffer.from(pictureBuffer).toString("base64")
    return {
      id: profile.sub,
      name: profile.name,
      email: profile.email,
      image: `data:image/jpeg;base64, ${pictureBase64}`,
    }
  } else {
    return {
      id: profile.sub,
      name: profile.name,
      email: profile.email,
    }
  }
}

Jsbbvk avatar Oct 20 '22 02:10 Jsbbvk

Solution

/api/auth/[...nextauth].ts

 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: "openid profile user.Read email" } },
  }),
 ],

In order to load profile images you need the 'user.Read' parameter. You can pass the user.Read parameter within the Authorization header.

In order to load the profile image the Microsoft account does need to have a profile picture set.

NOTE: If you are trying to accept all Microsoft Account types (organizations & consumers) you need to set your tenantID to 'common'. More information here: MSAL Client Application Configuration

I wish this was mentioned within the documentation and that this was included as default within the Next Auth Library.

drewgillen avatar Dec 09 '22 23:12 drewgillen

Solution

/api/auth/[...nextauth].ts

 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: "openid profile user.Read email" } },
  }),
 ],

In order to load profile images you need the 'user.Read' parameter. You can pass the user.Read parameter within the Authorization header.

In order to load the profile image the Microsoft account does need to have a profile picture set.

NOTE: If you are trying to accept all Microsoft Account types (organizations & consumers) you need to set your tenantID to 'common'. More information here: MSAL Client Application Configuration

I wish this was mentioned within the documentation and that this was included as default within the Next Auth Library.

Consider using "User.Read" with not "user.Read". That works!

alerimoficial avatar Oct 26 '23 18:10 alerimoficial

does not works for me neither. In the Oauth callback I see the base64 image (and it's the good one because when I decode it I got the correct PP) but when I try to get the info in the JWT callback profile parameter in order to have it in my session, I don't have any image property.

clementAC avatar Mar 25 '24 08:03 clementAC