diff --git a/src/Authentication/Authentication.Core/Common/GraphSession.cs b/src/Authentication/Authentication.Core/Common/GraphSession.cs
index 6b893d4d1c..059aa94a4a 100644
--- a/src/Authentication/Authentication.Core/Common/GraphSession.cs
+++ b/src/Authentication/Authentication.Core/Common/GraphSession.cs
@@ -56,6 +56,11 @@ public class GraphSession : IGraphSession
///
public IGraphOption GraphOption { get; set; }
+ ///
+ /// Temporarily stores the user's Graph request details such as Method and Uri. Essential as part of the Proof of Possession efforts.
+ ///
+ public IGraphRequestProofofPossession GraphRequestProofofPossession { get; set; }
+
///
/// Represents a collection of Microsoft Graph PowerShell meta-info.
///
diff --git a/src/Authentication/Authentication.Core/Interfaces/IGraphRequestProofofPossession.cs b/src/Authentication/Authentication.Core/Interfaces/IGraphRequestProofofPossession.cs
new file mode 100644
index 0000000000..1d2d4eef53
--- /dev/null
+++ b/src/Authentication/Authentication.Core/Interfaces/IGraphRequestProofofPossession.cs
@@ -0,0 +1,22 @@
+// ------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
+// ------------------------------------------------------------------------------
+
+using Azure.Core;
+using Azure.Identity;
+using System;
+using System.Net.Http;
+
+namespace Microsoft.Graph.PowerShell.Authentication
+{
+ public interface IGraphRequestProofofPossession
+ {
+ Uri Uri { get; set; }
+ HttpMethod HttpMethod { get; set; }
+ AccessToken AccessToken { get; set; }
+ string ProofofPossessionNonce { get; set; }
+ PopTokenRequestContext PopTokenContext { get; set; }
+ Request Request { get; set; }
+ InteractiveBrowserCredential BrowserCredential { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Authentication/Authentication.Core/Interfaces/IGraphSession.cs b/src/Authentication/Authentication.Core/Interfaces/IGraphSession.cs
index c7f8ba48c3..88c6fe4373 100644
--- a/src/Authentication/Authentication.Core/Interfaces/IGraphSession.cs
+++ b/src/Authentication/Authentication.Core/Interfaces/IGraphSession.cs
@@ -12,5 +12,6 @@ public interface IGraphSession
IDataStore DataStore { get; set; }
IRequestContext RequestContext { get; set; }
IGraphOption GraphOption { get; set; }
+ IGraphRequestProofofPossession GraphRequestProofofPossession { get; set; }
}
}
\ No newline at end of file
diff --git a/src/Authentication/Authentication.Core/Microsoft.Graph.Authentication.Core.csproj b/src/Authentication/Authentication.Core/Microsoft.Graph.Authentication.Core.csproj
index 7cbc04ae08..8c48e367e9 100644
--- a/src/Authentication/Authentication.Core/Microsoft.Graph.Authentication.Core.csproj
+++ b/src/Authentication/Authentication.Core/Microsoft.Graph.Authentication.Core.csproj
@@ -13,7 +13,8 @@
-
+
+
diff --git a/src/Authentication/Authentication.Core/Utilities/AuthenticationHelpers.cs b/src/Authentication/Authentication.Core/Utilities/AuthenticationHelpers.cs
index 923fb66d46..8275d69a7e 100644
--- a/src/Authentication/Authentication.Core/Utilities/AuthenticationHelpers.cs
+++ b/src/Authentication/Authentication.Core/Utilities/AuthenticationHelpers.cs
@@ -1,6 +1,7 @@
// ------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
// ------------------------------------------------------------------------------
+using Azure;
using Azure.Core;
using Azure.Core.Diagnostics;
using Azure.Core.Pipeline;
@@ -16,6 +17,7 @@
using System.IO;
using System.Linq;
using System.Net.Http;
+using System.Net.Http.Headers;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.Text.RegularExpressions;
@@ -91,7 +93,7 @@ private static bool IsWamSupported()
}
//Check to see if ATPoP is Supported
- private static bool IsATPoPSupported()
+ public static bool IsATPoPSupported()
{
return GraphSession.Instance.GraphOption.EnableATPoPForMSGraph;
}
@@ -131,10 +133,17 @@ private static async Task GetInteractiveBrowserCre
interactiveOptions.AuthorityHost = new Uri(GetAuthorityUrl(authContext));
interactiveOptions.TokenCachePersistenceOptions = GetTokenCachePersistenceOptions(authContext);
+ var interactiveBrowserCredential = new InteractiveBrowserCredential(interactiveOptions);
+ if (IsATPoPSupported())
+ {
+ GraphSession.Instance.GraphRequestProofofPossession.PopTokenContext = CreatePopTokenRequestContext(authContext);
+ GraphSession.Instance.GraphRequestProofofPossession.BrowserCredential = interactiveBrowserCredential;
+ }
+
if (!File.Exists(Constants.AuthRecordPath))
{
AuthenticationRecord authRecord;
- var interactiveBrowserCredential = new InteractiveBrowserCredential(interactiveOptions);
+ //var interactiveBrowserCredential = new InteractiveBrowserCredential(interactiveOptions);
if (IsWamSupported())
{
// Adding a scenario to account for Access Token Proof of Possession
@@ -143,45 +152,9 @@ private static async Task GetInteractiveBrowserCre
// Logic to implement ATPoP Authentication
authRecord = await Task.Run(() =>
{
- // Creating a Request to retrieve nonce value
- string popNonce = null;
- var popNonceToken = "nonce=\"";
- Uri resourceUri = new Uri("https://canary.graph.microsoft.com/beta/me"); //PPE (https://graph.microsoft-ppe.com) or Canary (https://canary.graph.microsoft.com) or (https://20.190.132.47/beta/me)
- HttpClient httpClient = new(new HttpClientHandler { ServerCertificateCustomValidationCallback = (_, _, _, _) => true });
- HttpResponseMessage response = httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, resourceUri)).Result;
-
- // Find the WWW-Authenticate header in the response.
- var popChallenge = response.Headers.WwwAuthenticate.First(wa => wa.Scheme == "PoP");
- var nonceStart = popChallenge.Parameter.IndexOf(popNonceToken) + popNonceToken.Length;
- var nonceEnd = popChallenge.Parameter.IndexOf('"', nonceStart);
- popNonce = popChallenge.Parameter.Substring(nonceStart, nonceEnd - nonceStart);
-
- // Refresh token logic --- start
- var popTokenAuthenticationPolicy = new PopTokenAuthenticationPolicy(interactiveBrowserCredential as ISupportsProofOfPossession, $"https://graph.microsoft.com/.default");
- var pipelineOptions = new HttpPipelineOptions(new PopClientOptions()
- {
- Diagnostics =
- {
- IsLoggingContentEnabled = true,
- LoggedHeaderNames = { "Authorization" }
- },
- });
- pipelineOptions.PerRetryPolicies.Add(popTokenAuthenticationPolicy);
-
- var _pipeline = HttpPipelineBuilder.Build(pipelineOptions, new HttpPipelineTransportOptions { ServerCertificateCustomValidationCallback = (_) => true });
-
- using var request = _pipeline.CreateRequest();
- request.Method = RequestMethod.Get;
- request.Uri.Reset(resourceUri);
-
- // Manually invoke the authentication policy's process method
- popTokenAuthenticationPolicy.ProcessAsync(new HttpMessage(request, new ResponseClassifier()), ReadOnlyMemory.Empty);
- // Refresh token logic --- end
-
// Run the thread in MTA.
- var popContext = new PopTokenRequestContext(authContext.Scopes, isProofOfPossessionEnabled: true, proofOfPossessionNonce: popNonce, request: request);
- //var token = interactiveBrowserCredential.GetToken(popContext, cancellationToken);
- return interactiveBrowserCredential.Authenticate(popContext, cancellationToken);
+ //GraphSession.Instance.GraphRequestProofofPossession.AccessToken = interactiveBrowserCredential.GetTokenAsync(GraphSession.Instance.GraphRequestProofofPossession.PopTokenContext, cancellationToken).Result;
+ return interactiveBrowserCredential.AuthenticateAsync(GraphSession.Instance.GraphRequestProofofPossession.PopTokenContext, cancellationToken);
});
}
else
@@ -508,6 +481,64 @@ public static Task DeleteAuthRecordAsync()
File.Delete(Constants.AuthRecordPath);
return Task.CompletedTask;
}
+
+ public static PopTokenRequestContext CreatePopTokenRequestContext(IAuthContext authContext)
+ {
+ // Creating a httpclient that would handle all pop calls
+ Uri popResourceUri = GraphSession.Instance.GraphRequestProofofPossession.Uri ?? new Uri("https://canary.graph.microsoft.com/beta/me"); //PPE (https://graph.microsoft-ppe.com) or Canary (https://canary.graph.microsoft.com) or (https://20.190.132.47/beta/me)
+ HttpClient popHttpClient = new(new HttpClientHandler { ServerCertificateCustomValidationCallback = (_, _, _, _) => true });
+
+ // Find the WWW-Authenticate header in the response.
+ var popMethod = GraphSession.Instance.GraphRequestProofofPossession.HttpMethod ?? HttpMethod.Get;
+ var popResponse = popHttpClient.SendAsync(new HttpRequestMessage(popMethod, popResourceUri)).Result;
+ var popChallenge = popResponse.Headers.WwwAuthenticate.First(wa => wa.Scheme == "PoP");
+ var nonceStart = popChallenge.Parameter.IndexOf("nonce=\"") + "nonce=\"".Length;
+ var nonceEnd = popChallenge.Parameter.IndexOf('"', nonceStart);
+ GraphSession.Instance.GraphRequestProofofPossession.ProofofPossessionNonce = popChallenge.Parameter.Substring(nonceStart, nonceEnd - nonceStart);
+
+ // Refresh token logic --- start
+ var popPipelineOptions = new HttpPipelineOptions(new PopClientOptions()
+ {
+
+ });
+
+ var _popPipeline = HttpPipelineBuilder.Build(popPipelineOptions, new HttpPipelineTransportOptions { ServerCertificateCustomValidationCallback = (_) => true });
+ GraphSession.Instance.GraphRequestProofofPossession.Request = _popPipeline.CreateRequest();
+ GraphSession.Instance.GraphRequestProofofPossession.Request.Method = ConvertToAzureRequestMethod(popMethod);
+ GraphSession.Instance.GraphRequestProofofPossession.Request.Uri.Reset(popResourceUri);
+
+ // Refresh token logic --- end
+ var popContext = new PopTokenRequestContext(authContext.Scopes, isProofOfPossessionEnabled: true, proofOfPossessionNonce: GraphSession.Instance.GraphRequestProofofPossession.ProofofPossessionNonce, request: GraphSession.Instance.GraphRequestProofofPossession.Request);
+ return popContext;
+ }
+ public static RequestMethod ConvertToAzureRequestMethod(HttpMethod httpMethod)
+ {
+ // Mapping known HTTP methods
+ switch (httpMethod.Method.ToUpper())
+ {
+ case "GET":
+ return RequestMethod.Get;
+ case "POST":
+ return RequestMethod.Post;
+ case "PUT":
+ return RequestMethod.Put;
+ case "DELETE":
+ return RequestMethod.Delete;
+ case "HEAD":
+ return RequestMethod.Head;
+ case "OPTIONS":
+ return RequestMethod.Options;
+ case "PATCH":
+ return RequestMethod.Patch;
+ case "TRACE":
+ return RequestMethod.Trace;
+ default:
+ throw new ArgumentException($"Unsupported HTTP method: {httpMethod.Method}");
+ }
+ }
+ }
+ internal class PopClientOptions : ClientOptions
+ {
}
internal class PopClientOptions : ClientOptions
{
diff --git a/src/Authentication/Authentication/Cmdlets/InvokeMgGraphRequest.cs b/src/Authentication/Authentication/Cmdlets/InvokeMgGraphRequest.cs
index d211b0c2aa..4d4e177963 100644
--- a/src/Authentication/Authentication/Cmdlets/InvokeMgGraphRequest.cs
+++ b/src/Authentication/Authentication/Cmdlets/InvokeMgGraphRequest.cs
@@ -1023,6 +1023,8 @@ private async Task ProcessRecordAsync()
try
{
PrepareSession();
+ GraphSession.Instance.GraphRequestProofofPossession.Uri = Uri;
+ GraphSession.Instance.GraphRequestProofofPossession.HttpMethod = GetHttpMethod(Method);
var client = HttpHelpers.GetGraphHttpClient();
ValidateRequestUri();
using (var httpRequestMessage = GetRequest(client, Uri))
diff --git a/src/Authentication/Authentication/Common/GraphSessionInitializer.cs b/src/Authentication/Authentication/Common/GraphSessionInitializer.cs
index 4d3d527da1..5d4d0ff5d3 100644
--- a/src/Authentication/Authentication/Common/GraphSessionInitializer.cs
+++ b/src/Authentication/Authentication/Common/GraphSessionInitializer.cs
@@ -47,7 +47,8 @@ internal static GraphSession CreateInstance(IDataStore dataStore = null)
{
DataStore = dataStore ?? new DiskDataStore(),
RequestContext = new RequestContext(),
- GraphOption = graphOptions ?? new GraphOption()
+ GraphOption = graphOptions ?? new GraphOption(),
+ GraphRequestProofofPossession = new GraphRequestProofofPossession()
};
}
///
diff --git a/src/Authentication/Authentication/Handlers/AuthenticationHandler.cs b/src/Authentication/Authentication/Handlers/AuthenticationHandler.cs
index e57d74186b..c326264a71 100644
--- a/src/Authentication/Authentication/Handlers/AuthenticationHandler.cs
+++ b/src/Authentication/Authentication/Handlers/AuthenticationHandler.cs
@@ -3,8 +3,13 @@
// ------------------------------------------------------------------------------
+using Azure.Core;
+using Azure.Identity;
+using Azure.Identity.Broker;
using Microsoft.Graph.Authentication;
+using Microsoft.Graph.PowerShell.Authentication.Core.Utilities;
using Microsoft.Graph.PowerShell.Authentication.Extensions;
+using Microsoft.Identity.Client;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -63,9 +68,24 @@ private async Task AuthenticateRequestAsync(HttpRequestMessage httpRequestMessag
{
if (AuthenticationProvider != null)
{
- var accessToken = await AuthenticationProvider.GetAuthorizationTokenAsync(httpRequestMessage.RequestUri, additionalAuthenticationContext, cancellationToken: cancellationToken).ConfigureAwait(false);
- if (!string.IsNullOrEmpty(accessToken))
- httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue(BearerAuthenticationScheme, accessToken);
+ if (AuthenticationHelpers.IsATPoPSupported())
+ {
+ GraphSession.Instance.GraphRequestProofofPossession.Request.Method = AuthenticationHelpers.ConvertToAzureRequestMethod(httpRequestMessage.Method);
+ GraphSession.Instance.GraphRequestProofofPossession.Request.Uri.Reset(httpRequestMessage.RequestUri);
+ foreach (var header in httpRequestMessage.Headers)
+ {
+ GraphSession.Instance.GraphRequestProofofPossession.Request.Headers.Add(header.Key, header.Value.First());
+ }
+
+ var accessToken = GraphSession.Instance.GraphRequestProofofPossession.BrowserCredential.GetTokenAsync(GraphSession.Instance.GraphRequestProofofPossession.PopTokenContext, cancellationToken).Result;
+ httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Pop", accessToken.Token);
+ }
+ else
+ {
+ var accessToken = await AuthenticationProvider.GetAuthorizationTokenAsync(httpRequestMessage.RequestUri, additionalAuthenticationContext, cancellationToken: cancellationToken).ConfigureAwait(false);
+ if (!string.IsNullOrEmpty(accessToken))
+ httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue(BearerAuthenticationScheme, accessToken);
+ }
}
}
@@ -87,6 +107,14 @@ private async Task SendRetryAsync(HttpResponseMessage httpR
}
await DrainAsync(httpResponseMessage).ConfigureAwait(false);
+ if (AuthenticationHelpers.IsATPoPSupported())
+ {
+ var popChallenge = httpResponseMessage.Headers.WwwAuthenticate.First(wa => wa.Scheme == "PoP");
+ var nonceStart = popChallenge.Parameter.IndexOf("nonce=\"") + "nonce=\"".Length;
+ var nonceEnd = popChallenge.Parameter.IndexOf('"', nonceStart);
+ GraphSession.Instance.GraphRequestProofofPossession.ProofofPossessionNonce = popChallenge.Parameter.Substring(nonceStart, nonceEnd - nonceStart);
+ }
+
// Authenticate request using auth provider
await AuthenticateRequestAsync(newRequest, additionalRequestInfo, cancellationToken).ConfigureAwait(false);
httpResponseMessage = await base.SendAsync(newRequest, cancellationToken);
diff --git a/src/Authentication/Authentication/Helpers/HttpHelpers.cs b/src/Authentication/Authentication/Helpers/HttpHelpers.cs
index cfd9252f5f..01394d120c 100644
--- a/src/Authentication/Authentication/Helpers/HttpHelpers.cs
+++ b/src/Authentication/Authentication/Helpers/HttpHelpers.cs
@@ -1,16 +1,20 @@
// ------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
// ------------------------------------------------------------------------------
+using Azure.Core;
using Microsoft.Graph.Authentication;
using Microsoft.Graph.PowerShell.Authentication.Core.Interfaces;
using Microsoft.Graph.PowerShell.Authentication.Core.Utilities;
using Microsoft.Graph.PowerShell.Authentication.Handlers;
+using Microsoft.Identity.Client;
using Microsoft.Kiota.Http.HttpClientLibrary.Middleware;
using Microsoft.Kiota.Http.HttpClientLibrary.Middleware.Options;
using System.Collections.Generic;
using System.Globalization;
+using System.Linq;
using System.Net;
using System.Net.Http;
+using System.Net.Http.Headers;
namespace Microsoft.Graph.PowerShell.Authentication.Helpers
{
diff --git a/src/Authentication/Authentication/Models/GraphRequestProofofPossession.cs b/src/Authentication/Authentication/Models/GraphRequestProofofPossession.cs
new file mode 100644
index 0000000000..2c12f8c368
--- /dev/null
+++ b/src/Authentication/Authentication/Models/GraphRequestProofofPossession.cs
@@ -0,0 +1,24 @@
+// ------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
+// ------------------------------------------------------------------------------
+
+using Azure.Core;
+using Azure.Identity;
+using System;
+using System.IO;
+using System.Net.Http;
+
+namespace Microsoft.Graph.PowerShell.Authentication
+{
+ internal class GraphRequestProofofPossession : IGraphRequestProofofPossession
+ {
+ public Uri Uri { get; set; }
+ public HttpMethod HttpMethod { get; set; }
+ public AccessToken AccessToken { get; set; }
+ public string ProofofPossessionNonce { get; set; }
+ public PopTokenRequestContext PopTokenContext { get; set; }
+ public Request Request { get; set; }
+ public InteractiveBrowserCredential BrowserCredential { get; set; }
+ }
+
+}
\ No newline at end of file