next-auth
next-auth copied to clipboard
Azure AD Provider - Profile image doesn't work
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:

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)
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?
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.
Could you send me a code snippet of your provider? It would help a lot, thank you!
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;
}
}
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,
}
}
}
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.
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!
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.