Skip to content

Commit

Permalink
AT PoP Version 1
Browse files Browse the repository at this point in the history
Fehintolaobafemi/methodanduri (#2751)

* Making changes to how httpmethod and uri is processed

---------
  • Loading branch information
FehintolaObafemi committed Jul 9, 2024
1 parent 510d558 commit cc658f6
Show file tree
Hide file tree
Showing 14 changed files with 162 additions and 40 deletions.
4 changes: 2 additions & 2 deletions docs/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,9 @@ Before using the provided `-AccessToken` to get Microsoft Graph resources, custo

### Access Token Proof of Possession (AT PoP)

AT PoP is a security mechanism that binds an access token to a cryptographic key that only the intended recipient has. This prevents unauthorized use of the token by malicious actors. AT PoP enhances data protection, reduces token replay attacks, and enables fine-grained authorization policies.
AT PoP is a security mechanism that binds an access token to a cryptographic key that only the token requestor has. This prevents unauthorized use of the token by malicious actors. AT PoP enhances data protection, reduces token replay attacks, and enables fine-grained authorization policies.

Note: AT PoP requires WAM to function.
Note: AT PoP requires Web Account Manager (WAM) to function.

Microsoft Graph PowerShell module supports AT PoP in the following scenario:

Expand Down
5 changes: 5 additions & 0 deletions src/Authentication/Authentication.Core/Common/GraphSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ public class GraphSession : IGraphSession
/// </summary>
public IGraphOption GraphOption { get; set; }

/// <summary>
/// Temporarily stores the user's Graph request details such as Method and Uri. Essential as part of the Proof of Possession efforts.
/// </summary>
public IGraphRequestProofofPossession GraphRequestProofofPossession { get; set; }

/// <summary>
/// Represents a collection of Microsoft Graph PowerShell meta-info.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
// ------------------------------------------------------------------------------

using System;
using System.Security;
using System.Security.Cryptography.X509Certificates;

namespace Microsoft.Graph.PowerShell.Authentication
{
public interface IGraphOption
Expand Down
Original file line number Diff line number Diff line change
@@ -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; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ public interface IGraphSession
IDataStore DataStore { get; set; }
IRequestContext RequestContext { get; set; }
IGraphOption GraphOption { get; set; }
IGraphRequestProofofPossession GraphRequestProofofPossession { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -88,7 +89,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;
}
Expand Down Expand Up @@ -120,16 +121,25 @@ private static async Task<InteractiveBrowserCredential> GetInteractiveBrowserCre
{
if (authContext is null)
throw new AuthenticationException(ErrorConstants.Message.MissingAuthContext);
var interactiveOptions = IsWamSupported() ? new InteractiveBrowserCredentialBrokerOptions(WindowHandleUtlities.GetConsoleOrTerminalWindow()) : new InteractiveBrowserCredentialOptions();
var interactiveOptions = IsWamSupported() ?
new InteractiveBrowserCredentialBrokerOptions(WindowHandleUtlities.GetConsoleOrTerminalWindow()) :
new InteractiveBrowserCredentialOptions();
interactiveOptions.ClientId = authContext.ClientId;
interactiveOptions.TenantId = authContext.TenantId ?? "common";
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
Expand All @@ -138,29 +148,9 @@ private static async Task<InteractiveBrowserCredential> GetInteractiveBrowserCre
// Logic to implement ATPoP Authentication
authRecord = await Task.Run(() =>
{
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(new Uri("https://20.190.132.47/beta/me"));
var response = _pipeline.SendRequest(request, cancellationToken);
var message = new HttpMessage(request, new ResponseClassifier());
// Manually invoke the authentication policy's process method
popTokenAuthenticationPolicy.ProcessAsync(message, ReadOnlyMemory<HttpPipelinePolicy>.Empty);
// Run the thread in MTA.
return interactiveBrowserCredential.Authenticate(new TokenRequestContext(authContext.Scopes), cancellationToken);
//GraphSession.Instance.GraphRequestProofofPossession.AccessToken = interactiveBrowserCredential.GetTokenAsync(GraphSession.Instance.GraphRequestProofofPossession.PopTokenContext, cancellationToken).Result;
return interactiveBrowserCredential.AuthenticateAsync(GraphSession.Instance.GraphRequestProofofPossession.PopTokenContext, cancellationToken);
});
}
else
Expand Down Expand Up @@ -487,6 +477,61 @@ 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://graph.microsoft.com/beta/organization"); //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());

// 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());
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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.2" />
<!-- As described in this post https://devblogs.microsoft.com/powershell/depending-on-the-right-powershell-nuget-package-in-your-net-project, reference the SDK for dotnetcore-->
<PackageReference Include="Microsoft.PowerShell.SDK" Version="7.2.2" PrivateAssets="all" Condition="'$(TargetFramework)' == 'net6.0'" />
<PackageReference Include="Moq" Version="4.20.69" />
<PackageReference Include="Moq" Version="4.20.1" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
};
}
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
// ------------------------------------------------------------------------------


using Azure.Core;
using Microsoft.Graph.Authentication;
using Microsoft.Graph.PowerShell.Authentication.Core.Utilities;
using Microsoft.Graph.PowerShell.Authentication.Extensions;
using System;
using System.Collections.Generic;
Expand Down Expand Up @@ -63,9 +65,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);
}
}
}

Expand All @@ -87,6 +104,15 @@ private async Task<HttpResponseMessage> 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);
GraphSession.Instance.GraphRequestProofofPossession.PopTokenContext = new PopTokenRequestContext(GraphSession.Instance.AuthContext.Scopes, isProofOfPossessionEnabled: true, proofOfPossessionNonce: GraphSession.Instance.GraphRequestProofofPossession.ProofofPossessionNonce, request: GraphSession.Instance.GraphRequestProofofPossession.Request);
}

// Authenticate request using auth provider
await AuthenticateRequestAsync(newRequest, additionalRequestInfo, cancellationToken).ConfigureAwait(false);
httpResponseMessage = await base.SendAsync(newRequest, cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#
# Generated by: Microsoft
#
# Generated on: 12/28/2023
# Generated on: 21/09/2023
#

@{
Expand All @@ -12,7 +12,7 @@
RootModule = './Microsoft.Graph.Authentication.psm1'

# Version number of this module.
ModuleVersion = '2.11.1'
ModuleVersion = '2.6.1'

# Supported PSEditions
CompatiblePSEditions = 'Core', 'Desktop'
Expand Down
Original file line number Diff line number Diff line change
@@ -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; }
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Describe "Get-MgGraphOption Command" {
$GetMgGraphOptionCommand = Get-Command Set-MgGraphOption
$GetMgGraphOptionCommand | Should -Not -BeNullOrEmpty
$GetMgGraphOptionCommand.ParameterSets | Should -HaveCount 1
$GetMgGraphOptionCommand.ParameterSets.Parameters | Should -HaveCount 13 # PS common parameters.
$GetMgGraphOptionCommand.ParameterSets.Parameters | Should -HaveCount 14 # PS common parameters.
}

It 'Executes successfully' {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Describe "Set-MgGraphOption" {
$SetMgGraphOptionCommand = Get-Command Set-MgGraphOption
$SetMgGraphOptionCommand | Should -Not -BeNullOrEmpty
$SetMgGraphOptionCommand.ParameterSets | Should -HaveCount 1
$SetMgGraphOptionCommand.ParameterSets.Parameters | Should -HaveCount 13 # PS common parameters.
$SetMgGraphOptionCommand.ParameterSets.Parameters | Should -HaveCount 14 # PS common parameters.
}

It 'Executes successfully when toggling WAM on' {
Expand Down

0 comments on commit cc658f6

Please sign in to comment.