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