Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nexus Productivty: leveraging the power of Microsoft graph and Azure … #825

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
37 changes: 37 additions & 0 deletions samples/app-nexus-productivity/GraphSample/GraphSample.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 25.0.1705.4
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphSample", "GraphSample\GraphSample.csproj", "{5C9C1BD8-3FAE-4168-AA26-D1295BF8A091}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebBackend", "WebBackend\WebBackend.csproj", "{D2E12BF1-D808-47B6-B9F1-C43D85A5CFF6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedModels", "SharedModels\SharedModels.csproj", "{DF9E15A1-F437-46FF-9CF7-3B8168199A33}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{5C9C1BD8-3FAE-4168-AA26-D1295BF8A091}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5C9C1BD8-3FAE-4168-AA26-D1295BF8A091}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5C9C1BD8-3FAE-4168-AA26-D1295BF8A091}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5C9C1BD8-3FAE-4168-AA26-D1295BF8A091}.Release|Any CPU.Build.0 = Release|Any CPU
{D2E12BF1-D808-47B6-B9F1-C43D85A5CFF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D2E12BF1-D808-47B6-B9F1-C43D85A5CFF6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D2E12BF1-D808-47B6-B9F1-C43D85A5CFF6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D2E12BF1-D808-47B6-B9F1-C43D85A5CFF6}.Release|Any CPU.Build.0 = Release|Any CPU
{DF9E15A1-F437-46FF-9CF7-3B8168199A33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DF9E15A1-F437-46FF-9CF7-3B8168199A33}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DF9E15A1-F437-46FF-9CF7-3B8168199A33}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DF9E15A1-F437-46FF-9CF7-3B8168199A33}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {7A561DA2-F67E-4149-92BF-6BA5D1A8E086}
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Azure;
using Azure.AI.OpenAI;
using Microsoft.Extensions.Options;
using static System.Environment;

namespace GraphSample.AI
{
public class OpenAIService
{
public readonly string engine = "PlannerOpenAI";
public readonly string endpoint = "https://plannerai.openai.azure.com/";
public readonly string key = "63448da86da048d49845249803372807";

public readonly OpenAIClient client = new OpenAIClient(new Uri("https://plannerai.openai.azure.com/"), new AzureKeyCredential("63448da86da048d49845249803372807"));

}
}
32 changes: 32 additions & 0 deletions samples/app-nexus-productivity/GraphSample/GraphSample/App.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
@inject IJSRuntime jsRuntime


<CascadingAuthenticationState>
<Blazored.Modal.CascadingBlazoredModal>
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
<NotAuthorized>
@if (context.User.Identity?.IsAuthenticated != true)
{
<RedirectToLogin />
}
else
{
<p role="alert">You are not authorized to access this resource.</p>
}
</NotAuthorized>
</AuthorizeRouteView>
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</Blazored.Modal.CascadingBlazoredModal>
</CascadingAuthenticationState>


Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
using Microsoft.Kiota.Abstractions;
using Microsoft.Kiota.Abstractions.Authentication;

namespace GraphSample.Graph
{
public class BlazorAuthProvider : IAuthenticationProvider
{
private readonly IAccessTokenProviderAccessor accessor;

public BlazorAuthProvider(IAccessTokenProviderAccessor accessor)
{
this.accessor = accessor;
}

// Function called every time the GraphServiceClient makes a call
public async Task AuthenticateRequestAsync(
RequestInformation request,
Dictionary<string, object>? additionalAuthenticationContext = null,
CancellationToken cancellationToken = default)
{
// Request the token from the accessor
var result = await accessor.TokenProvider.RequestAccessToken();
try{
if (result.TryGetToken(out var token))
{
// Add the token to the Authorization header
request.Headers.Add("Authorization", $"Bearer {token.Value}");
}
}
catch(Exception ex){
Console.WriteLine(ex.Message);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

using System.Security.Authentication;
using System.Security.Claims;
using Microsoft.Graph.Models;

namespace GraphSample
{
public static class GraphClaimTypes {
public const string DateFormat = "graph_dateformat";
public const string Email = "graph_email";
public const string Photo = "graph_photo";
public const string TimeZone = "graph_timezone";
public const string TimeFormat = "graph_timeformat";
}

// Helper methods to access Graph user data stored in
// the claims principal
public static class GraphClaimsPrincipalExtensions
{
public static string GetUserGraphDateFormat(this ClaimsPrincipal claimsPrincipal)
{
var claim = claimsPrincipal.FindFirst(GraphClaimTypes.DateFormat);
return claim == null ? string.Empty : claim.Value;
}

public static string GetUserGraphEmail(this ClaimsPrincipal claimsPrincipal)
{
var claim = claimsPrincipal.FindFirst(GraphClaimTypes.Email);
return claim == null ? string.Empty : claim.Value;
}

public static string? GetUserGraphPhoto(this ClaimsPrincipal claimsPrincipal)
{
var claim = claimsPrincipal.FindFirst(GraphClaimTypes.Photo);
return claim == null ? null : claim.Value;
}

public static string GetUserGraphTimeZone(this ClaimsPrincipal claimsPrincipal)
{
var claim = claimsPrincipal.FindFirst(GraphClaimTypes.TimeZone);
return claim == null ? string.Empty : claim.Value;
}

public static string GetUserGraphTimeFormat(this ClaimsPrincipal claimsPrincipal)
{
var claim = claimsPrincipal.FindFirst(GraphClaimTypes.TimeFormat);
return claim == null ? string.Empty : claim.Value;
}

// Adds claims from the provided User object
public static void AddUserGraphInfo(this ClaimsPrincipal claimsPrincipal, User user)
{
var identity = claimsPrincipal.Identity as ClaimsIdentity;
if (identity == null)
{
throw new AuthenticationException(
"ClaimsIdentity is null inside provided ClaimsPrincipal");
}

identity.AddClaim(
new Claim(GraphClaimTypes.DateFormat,
user.MailboxSettings?.DateFormat ?? "MMMM dd, yyyy"));
identity.AddClaim(
new Claim(GraphClaimTypes.Email,
user.Mail ?? user.UserPrincipalName ?? ""));
identity.AddClaim(
new Claim(GraphClaimTypes.TimeZone,
user.MailboxSettings?.TimeZone ?? "UTC"));
identity.AddClaim(
new Claim(GraphClaimTypes.TimeFormat,
user.MailboxSettings?.TimeFormat ?? "HH:mm"));
}

// Converts a photo Stream to a Data URI and stores it in a claim
public static void AddUserGraphPhoto(this ClaimsPrincipal claimsPrincipal, Stream? photoStream)
{
var identity = claimsPrincipal.Identity as ClaimsIdentity;
if (identity == null)
{
throw new AuthenticationException(
"ClaimsIdentity is null inside provided ClaimsPrincipal");
}

if (photoStream != null)
{
// Copy the photo stream to a memory stream
// to get the bytes out of it
var memoryStream = new MemoryStream();
photoStream.CopyTo(memoryStream);
var photoBytes = memoryStream.ToArray();

// Generate a date URI for the photo
var photoUri = $"data:image/png;base64,{Convert.ToBase64String(photoBytes)}";

identity.AddClaim(
new Claim(GraphClaimTypes.Photo, photoUri));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
using Microsoft.Graph;
using Microsoft.Kiota.Http.HttpClientLibrary;

namespace GraphSample.Graph
{
public class GraphClientFactory
{
private readonly IAccessTokenProviderAccessor accessor;
private readonly HttpClient httpClient;
private readonly ILogger<GraphClientFactory> logger;
private GraphServiceClient? graphClient;

public GraphClientFactory(IAccessTokenProviderAccessor accessor,
HttpClient httpClient,
ILogger<GraphClientFactory> logger)
{
this.accessor = accessor;
this.httpClient = httpClient;
this.logger = logger;
}

public GraphServiceClient GetAuthenticatedClient()
{
// Use the existing one if it's there
if (graphClient == null)
{
// Create a GraphServiceClient using a scoped
// HttpClient
var requestAdapter = new HttpClientRequestAdapter(
new BlazorAuthProvider(accessor), null, null, httpClient);
graphClient = new GraphServiceClient(requestAdapter);
}

return graphClient;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
using Microsoft.Graph;
using Microsoft.Graph.Models.ODataErrors;

namespace GraphSample.Graph
{
// Extends the AccountClaimsPrincipalFactory that builds
// a user identity from the identity token.
// This class adds additional claims to the user's ClaimPrincipal
// that hold values from Microsoft Graph
public class GraphUserAccountFactoryBeta
: AccountClaimsPrincipalFactory<RemoteUserAccount>
{
private readonly IAccessTokenProviderAccessor accessor;
private readonly ILogger<GraphUserAccountFactoryBeta> logger;

private readonly GraphClientFactory clientFactory;

public GraphUserAccountFactoryBeta(IAccessTokenProviderAccessor accessor,
GraphClientFactory clientFactory,
ILogger<GraphUserAccountFactoryBeta> logger)
: base(accessor)
{
this.accessor = accessor;
this.clientFactory = clientFactory;
this.logger = logger;
}

public async override ValueTask<ClaimsPrincipal?> CreateUserAsync(
RemoteUserAccount account,
RemoteAuthenticationUserOptions options)
{
// Create the base user
var initialUser = await base.CreateUserAsync(account, options);

// If authenticated, we can call Microsoft Graph
if (initialUser?.Identity?.IsAuthenticated ?? false)
{
try
{
// Add additional info from Graph to the identity
await AddGraphInfoToClaims(accessor, initialUser);
}
catch (AccessTokenNotAvailableException exception)
{
logger.LogError($"Graph API access token failure: {exception.Message}");
}
catch (ServiceException exception)
{
logger.LogError($"Graph API error: {exception.Message}");
logger.LogError($"Response body: {exception.RawResponseBody}");
}
}

return initialUser;
}

private async Task AddGraphInfoToClaims(
IAccessTokenProviderAccessor accessor,
ClaimsPrincipal claimsPrincipal)
{
var graphClient = clientFactory.GetAuthenticatedClient();

// Get user profile including mailbox settings
// GET /me?$select=displayName,mail,mailboxSettings,userPrincipalName
var user = await graphClient.Me.GetAsync(config =>
{
// Request only the properties used to
// set claims
config.QueryParameters.Select = new [] { "displayName", "mail", "mailboxSettings", "userPrincipalName" };
});

if (user == null)
{
throw new Exception("Could not retrieve user from Microsoft Graph.");
}

logger.LogInformation($"Got user: {user.DisplayName}");

claimsPrincipal.AddUserGraphInfo(user);

// Get user's photo
// GET /me/photos/48x48/$value
try
{
var photo = await graphClient.Me
.Photos["48x48"] // Smallest standard size
.Content
.GetAsync();

claimsPrincipal.AddUserGraphPhoto(photo);
}
catch (ODataError err)
{
Console.WriteLine($"Photo error: ${err?.Error?.Code}");
if (err?.Error?.Code != "ImageNotFound")
{
throw err ?? new Exception("Unknown error getting user photo.");
}
}
}
}
}
Loading