nextauthjs / next-auth

Authentication for the Web.
https://authjs.dev
ISC License
24.46k stars 3.43k forks source link

Azure AD Provider - Profile image doesn't work #5376

Open DavidIlie opened 2 years ago

DavidIlie commented 2 years ago

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 commented 2 years ago

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?

marcelherd commented 2 years ago

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.

DavidIlie commented 2 years ago

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

marcelherd commented 2 years ago

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;
  }
}
Jsbbvk commented 1 year ago

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,
    }
  }
}
drewgillen commented 1 year ago

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.

alerimoficial commented 11 months ago

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!

clementAC commented 6 months ago

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.