Gibe / Umbraco.Community.AzureSSO

Azure AD SSO module for Umbraco
MIT License
9 stars 8 forks source link

Fetch Azure AD Profile Image and set it as the Umbraco Avatar #11

Open dalbard opened 1 year ago

dalbard commented 1 year ago

As the title suggests, how would you go about modifying the code to fetch the logged in user's Azure AD profile image and set it as their Umbraco Avatar?

stevetemple commented 1 year ago

Interesting, I'll take a look and see if that would be possible with the data the claims supplies

mistyn8 commented 1 year ago

I had a little look.. doesn't seem to be anything from the claims that can be used, but did find two aproaches.

restapi call.. but I can't seem to get an access_token from the externalLogin, and nothing is being stored in the umbracoExternalLoginToken table? I've added options.SaveTokens = true; and that gives me an id_token but doesn't seem to let me use that against the graphapi

string? accessToken = loginInfo.AuthenticationTokens?.FirstOrDefault(t => t.Name.Equals("id_token"))?.Value;
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var pictureResult = httpClient.GetAsync("https://graph.microsoft.com/v1.0/me/photo/$value").Result;

Then stumbled on maybe using MicrosoftGraph package by adding .AddMicrosoftGraph() after EnableTokenAcquisitionToCallDownstreamApi(..) and changing scopes to var initialScopes = new string[] { "user.read" };

but then get issues with Cannot consume scoped service 'Microsoft.Graph.GraphServiceClient' from singleton 'Microsoft.Extensions.Options.IOptionsMonitor'

using (var photoStream = await _graphServiceClient.Me.Photo.Content.Request().GetAsync())
{
    byte[] photoByte = ((MemoryStream)photoStream).ToArray();
}

beyond my skills at this point. :-)

mistyn8 commented 1 year ago

So turned out just had to update Microsoft.Identity.Web latest is 2.13.0 and then add options.SaveTokens = true to the AddMicrosoftIdentityWebApp config And heh presto access_token is now present. and I can fetch the picture from the msgraph api image

mistyn8 commented 1 year ago

Final piece to the puzzle, though not sure .Result for that GetAsync is correct.. though didn't get far with async await up the method tree before method signatures started complaining.

private readonly AzureSsoSettings _settings;
private readonly IUserService _userService;
private readonly MediaFileManager _mediaFileManager;

public MicrosoftAccountBackOfficeExternalLoginProviderOptions(AzureSsoSettings settings, IUserService userService, MediaFileManager mediaFileManager)
{
    _settings = settings;
    _userService = userService;
    _mediaFileManager = mediaFileManager;
}
if (loginInfo.AuthenticationTokens?.FirstOrDefault(t => t.Name.Equals("access_token"))?.Value is string accessToken)
{
    using (var httpClient = new HttpClient())
    {
        httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        var pictureResult = httpClient.GetAsync("https://graph.microsoft.com/v1.0/me/photo/$value").Result;

        if (pictureResult.IsSuccessStatusCode)
        {
            if (_userService.GetByUsername(user.UserName) is User u && pictureResult.Headers.ETag is EntityTagHeaderValue etag)
            {
                //etag : identifier for a specific version of a resource
                u.Avatar = $"UserAvatars/{etag.ToString().GenerateHash<SHA1>()}.jpg";

                if (u.IsDirty())
                {
                    using (Stream fs = pictureResult.Content.ReadAsStream())
                    {
                        _mediaFileManager.FileSystem.AddFile(u.Avatar, fs, true);
                    }

                    _userService.Save(u);
                }
            }
        }
    }
}

UserAvatars in the core here.. https://github.com/umbraco/Umbraco-CMS/blob/contrib/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs#L185-L196C1

dalbard commented 11 months ago

Final piece to the puzzle, though not sure .Result for that GetAsync is correct.. though didn't get far with async await up the method tree before method signatures started complaining.

private readonly AzureSsoSettings _settings;
private readonly IUserService _userService;
private readonly MediaFileManager _mediaFileManager;

public MicrosoftAccountBackOfficeExternalLoginProviderOptions(AzureSsoSettings settings, IUserService userService, MediaFileManager mediaFileManager)
{
    _settings = settings;
    _userService = userService;
    _mediaFileManager = mediaFileManager;
}
if (loginInfo.AuthenticationTokens?.FirstOrDefault(t => t.Name.Equals("access_token"))?.Value is string accessToken)
{
    using (var httpClient = new HttpClient())
    {
        httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        var pictureResult = httpClient.GetAsync("https://graph.microsoft.com/v1.0/me/photo/$value").Result;

        if (pictureResult.IsSuccessStatusCode)
        {
            if (_userService.GetByUsername(user.UserName) is User u && pictureResult.Headers.ETag is EntityTagHeaderValue etag)
            {
                //etag : identifier for a specific version of a resource
                u.Avatar = $"UserAvatars/{etag.ToString().GenerateHash<SHA1>()}.jpg";

                if (u.IsDirty())
                {
                    using (Stream fs = pictureResult.Content.ReadAsStream())
                    {
                        _mediaFileManager.FileSystem.AddFile(u.Avatar, fs, true);
                    }

                    _userService.Save(u);
                }
            }
        }
    }
}

UserAvatars in the core here.. https://github.com/umbraco/Umbraco-CMS/blob/contrib/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs#L185-L196C1

Great findings! Does this update your avatar? I've just tested the code but it doesn't seem to work for me... maybe I'm not calling the function properly.