Skip to content

Commit 591c511

Browse files
AT PoP Version 1
Fehintolaobafemi/methodanduri (#2751) * Making changes to how httpmethod and uri is processed ---------
1 parent 480fd8c commit 591c511

15 files changed

+169
-46
lines changed

Diff for: docs/authentication.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,9 @@ Before using the provided `-AccessToken` to get Microsoft Graph resources, custo
114114

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

117-
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.
117+
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.
118118

119-
Note: AT PoP requires WAM to function.
119+
Note: AT PoP requires Web Account Manager (WAM) to function.
120120

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

Diff for: src/Authentication/Authentication.Core/Common/GraphSession.cs

+5
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ public class GraphSession : IGraphSession
5656
/// </summary>
5757
public IGraphOption GraphOption { get; set; }
5858

59+
/// <summary>
60+
/// Temporarily stores the user's Graph request details such as Method and Uri. Essential as part of the Proof of Possession efforts.
61+
/// </summary>
62+
public IGraphRequestProofofPossession GraphRequestProofofPossession { get; set; }
63+
5964
/// <summary>
6065
/// Represents a collection of Microsoft Graph PowerShell meta-info.
6166
/// </summary>

Diff for: src/Authentication/Authentication.Core/Interfaces/IGraphOptions.cs

-4
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@
22
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
33
// ------------------------------------------------------------------------------
44

5-
using System;
6-
using System.Security;
7-
using System.Security.Cryptography.X509Certificates;
8-
95
namespace Microsoft.Graph.PowerShell.Authentication
106
{
117
public interface IGraphOption
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// ------------------------------------------------------------------------------
2+
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
3+
// ------------------------------------------------------------------------------
4+
5+
using Azure.Core;
6+
using Azure.Identity;
7+
using System;
8+
using System.Net.Http;
9+
10+
namespace Microsoft.Graph.PowerShell.Authentication
11+
{
12+
public interface IGraphRequestProofofPossession
13+
{
14+
Uri Uri { get; set; }
15+
HttpMethod HttpMethod { get; set; }
16+
AccessToken AccessToken { get; set; }
17+
string ProofofPossessionNonce { get; set; }
18+
PopTokenRequestContext PopTokenContext { get; set; }
19+
Request Request { get; set; }
20+
InteractiveBrowserCredential BrowserCredential { get; set; }
21+
}
22+
}

Diff for: src/Authentication/Authentication.Core/Interfaces/IGraphSession.cs

+1
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@ public interface IGraphSession
1212
IDataStore DataStore { get; set; }
1313
IRequestContext RequestContext { get; set; }
1414
IGraphOption GraphOption { get; set; }
15+
IGraphRequestProofofPossession GraphRequestProofofPossession { get; set; }
1516
}
1617
}

Diff for: src/Authentication/Authentication.Core/Microsoft.Graph.Authentication.Core.csproj

+7-6
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@
1111
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
1212
</PropertyGroup>
1313
<ItemGroup>
14-
<PackageReference Include="Azure.Identity" Version="1.11.0-beta.1" />
15-
<PackageReference Include="Azure.Core" Version="1.38.0" />
16-
<PackageReference Include="Azure.Identity.Broker" Version="1.1.0-beta.1" />
17-
<PackageReference Include="Microsoft.Graph.Core" Version="3.1.8" />
18-
<PackageReference Include="Microsoft.Identity.Client" Version="4.59.0" />
19-
<PackageReference Include="Microsoft.Identity.Client.Broker" Version="4.59.0" />
14+
<PackageReference Include="Azure.Identity" Version="1.12.0-beta.1" />
15+
<PackageReference Include="Azure.Core" Version="1.39.0" />
16+
<PackageReference Include="Azure.Core.Experimental" Version="0.1.0-preview.33" />
17+
<PackageReference Include="Azure.Identity.Broker" Version="1.2.0-beta.1" />
18+
<PackageReference Include="Microsoft.Graph.Core" Version="3.1.10" />
19+
<PackageReference Include="Microsoft.Identity.Client" Version="4.60.3" />
20+
<PackageReference Include="Microsoft.Identity.Client.Broker" Version="4.60.3" />
2021
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
2122
</ItemGroup>
2223
<Target Name="CopyFiles" AfterTargets="Build">

Diff for: src/Authentication/Authentication.Core/Utilities/AuthenticationHelpers.cs

+70-25
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
using System.Globalization;
1616
using System.IO;
1717
using System.Linq;
18+
using System.Net.Http;
1819
using System.Security.Cryptography.X509Certificates;
1920
using System.Threading;
2021
using System.Threading.Tasks;
@@ -88,7 +89,7 @@ private static bool IsWamSupported()
8889
}
8990

9091
//Check to see if ATPoP is Supported
91-
private static bool IsATPoPSupported()
92+
public static bool IsATPoPSupported()
9293
{
9394
return GraphSession.Instance.GraphOption.EnableATPoPForMSGraph;
9495
}
@@ -120,16 +121,25 @@ private static async Task<InteractiveBrowserCredential> GetInteractiveBrowserCre
120121
{
121122
if (authContext is null)
122123
throw new AuthenticationException(ErrorConstants.Message.MissingAuthContext);
123-
var interactiveOptions = IsWamSupported() ? new InteractiveBrowserCredentialBrokerOptions(WindowHandleUtlities.GetConsoleOrTerminalWindow()) : new InteractiveBrowserCredentialOptions();
124+
var interactiveOptions = IsWamSupported() ?
125+
new InteractiveBrowserCredentialBrokerOptions(WindowHandleUtlities.GetConsoleOrTerminalWindow()) :
126+
new InteractiveBrowserCredentialOptions();
124127
interactiveOptions.ClientId = authContext.ClientId;
125128
interactiveOptions.TenantId = authContext.TenantId ?? "common";
126129
interactiveOptions.AuthorityHost = new Uri(GetAuthorityUrl(authContext));
127130
interactiveOptions.TokenCachePersistenceOptions = GetTokenCachePersistenceOptions(authContext);
128131

132+
var interactiveBrowserCredential = new InteractiveBrowserCredential(interactiveOptions);
133+
if (IsATPoPSupported())
134+
{
135+
GraphSession.Instance.GraphRequestProofofPossession.PopTokenContext = CreatePopTokenRequestContext(authContext);
136+
GraphSession.Instance.GraphRequestProofofPossession.BrowserCredential = interactiveBrowserCredential;
137+
}
138+
129139
if (!File.Exists(Constants.AuthRecordPath))
130140
{
131141
AuthenticationRecord authRecord;
132-
var interactiveBrowserCredential = new InteractiveBrowserCredential(interactiveOptions);
142+
//var interactiveBrowserCredential = new InteractiveBrowserCredential(interactiveOptions);
133143
if (IsWamSupported())
134144
{
135145
// Adding a scenario to account for Access Token Proof of Possession
@@ -138,29 +148,9 @@ private static async Task<InteractiveBrowserCredential> GetInteractiveBrowserCre
138148
// Logic to implement ATPoP Authentication
139149
authRecord = await Task.Run(() =>
140150
{
141-
var popTokenAuthenticationPolicy = new PopTokenAuthenticationPolicy(interactiveBrowserCredential as ISupportsProofOfPossession, $"https://graph.microsoft.com/.default");
142-
143-
var pipelineOptions = new HttpPipelineOptions(new PopClientOptions()
144-
{
145-
Diagnostics =
146-
{
147-
IsLoggingContentEnabled = true,
148-
LoggedHeaderNames = { "Authorization" }
149-
},
150-
});
151-
pipelineOptions.PerRetryPolicies.Add(popTokenAuthenticationPolicy);
152-
153-
var _pipeline = HttpPipelineBuilder.Build(pipelineOptions, new HttpPipelineTransportOptions { ServerCertificateCustomValidationCallback = (_) => true });
154-
using var request = _pipeline.CreateRequest();
155-
request.Method = RequestMethod.Get;
156-
request.Uri.Reset(new Uri("https://20.190.132.47/beta/me"));
157-
var response = _pipeline.SendRequest(request, cancellationToken);
158-
var message = new HttpMessage(request, new ResponseClassifier());
159-
160-
// Manually invoke the authentication policy's process method
161-
popTokenAuthenticationPolicy.ProcessAsync(message, ReadOnlyMemory<HttpPipelinePolicy>.Empty);
162151
// Run the thread in MTA.
163-
return interactiveBrowserCredential.Authenticate(new TokenRequestContext(authContext.Scopes), cancellationToken);
152+
//GraphSession.Instance.GraphRequestProofofPossession.AccessToken = interactiveBrowserCredential.GetTokenAsync(GraphSession.Instance.GraphRequestProofofPossession.PopTokenContext, cancellationToken).Result;
153+
return interactiveBrowserCredential.AuthenticateAsync(GraphSession.Instance.GraphRequestProofofPossession.PopTokenContext, cancellationToken);
164154
});
165155
}
166156
else
@@ -487,6 +477,61 @@ public static Task DeleteAuthRecordAsync()
487477
File.Delete(Constants.AuthRecordPath);
488478
return Task.CompletedTask;
489479
}
480+
481+
public static PopTokenRequestContext CreatePopTokenRequestContext(IAuthContext authContext)
482+
{
483+
// Creating a httpclient that would handle all pop calls
484+
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)
485+
HttpClient popHttpClient = new(new HttpClientHandler());
486+
487+
// Find the WWW-Authenticate header in the response.
488+
var popMethod = GraphSession.Instance.GraphRequestProofofPossession.HttpMethod ?? HttpMethod.Get;
489+
var popResponse = popHttpClient.SendAsync(new HttpRequestMessage(popMethod, popResourceUri)).Result;
490+
var popChallenge = popResponse.Headers.WwwAuthenticate.First(wa => wa.Scheme == "PoP");
491+
var nonceStart = popChallenge.Parameter.IndexOf("nonce=\"") + "nonce=\"".Length;
492+
var nonceEnd = popChallenge.Parameter.IndexOf('"', nonceStart);
493+
GraphSession.Instance.GraphRequestProofofPossession.ProofofPossessionNonce = popChallenge.Parameter.Substring(nonceStart, nonceEnd - nonceStart);
494+
495+
// Refresh token logic --- start
496+
var popPipelineOptions = new HttpPipelineOptions(new PopClientOptions()
497+
{
498+
499+
});
500+
501+
var _popPipeline = HttpPipelineBuilder.Build(popPipelineOptions, new HttpPipelineTransportOptions());
502+
GraphSession.Instance.GraphRequestProofofPossession.Request = _popPipeline.CreateRequest();
503+
GraphSession.Instance.GraphRequestProofofPossession.Request.Method = ConvertToAzureRequestMethod(popMethod);
504+
GraphSession.Instance.GraphRequestProofofPossession.Request.Uri.Reset(popResourceUri);
505+
506+
// Refresh token logic --- end
507+
var popContext = new PopTokenRequestContext(authContext.Scopes, isProofOfPossessionEnabled: true, proofOfPossessionNonce: GraphSession.Instance.GraphRequestProofofPossession.ProofofPossessionNonce, request: GraphSession.Instance.GraphRequestProofofPossession.Request);
508+
return popContext;
509+
}
510+
public static RequestMethod ConvertToAzureRequestMethod(HttpMethod httpMethod)
511+
{
512+
// Mapping known HTTP methods
513+
switch (httpMethod.Method.ToUpper())
514+
{
515+
case "GET":
516+
return RequestMethod.Get;
517+
case "POST":
518+
return RequestMethod.Post;
519+
case "PUT":
520+
return RequestMethod.Put;
521+
case "DELETE":
522+
return RequestMethod.Delete;
523+
case "HEAD":
524+
return RequestMethod.Head;
525+
case "OPTIONS":
526+
return RequestMethod.Options;
527+
case "PATCH":
528+
return RequestMethod.Patch;
529+
case "TRACE":
530+
return RequestMethod.Trace;
531+
default:
532+
throw new ArgumentException($"Unsupported HTTP method: {httpMethod.Method}");
533+
}
534+
}
490535
}
491536
internal class PopClientOptions : ClientOptions
492537
{

Diff for: src/Authentication/Authentication.Test/Microsoft.Graph.Authentication.Test.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.2" />
99
<!-- 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-->
1010
<PackageReference Include="Microsoft.PowerShell.SDK" Version="7.2.2" PrivateAssets="all" Condition="'$(TargetFramework)' == 'net6.0'" />
11-
<PackageReference Include="Moq" Version="4.20.69" />
11+
<PackageReference Include="Moq" Version="4.20.1" />
1212
<PackageReference Include="xunit" Version="2.4.2" />
1313
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
1414
<PrivateAssets>all</PrivateAssets>

Diff for: src/Authentication/Authentication/Cmdlets/InvokeMgGraphRequest.cs

+2
Original file line numberDiff line numberDiff line change
@@ -1023,6 +1023,8 @@ private async Task ProcessRecordAsync()
10231023
try
10241024
{
10251025
PrepareSession();
1026+
GraphSession.Instance.GraphRequestProofofPossession.Uri = Uri;
1027+
GraphSession.Instance.GraphRequestProofofPossession.HttpMethod = GetHttpMethod(Method);
10261028
var client = HttpHelpers.GetGraphHttpClient();
10271029
ValidateRequestUri();
10281030
using (var httpRequestMessage = GetRequest(client, Uri))

Diff for: src/Authentication/Authentication/Common/GraphSessionInitializer.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ internal static GraphSession CreateInstance(IDataStore dataStore = null)
4747
{
4848
DataStore = dataStore ?? new DiskDataStore(),
4949
RequestContext = new RequestContext(),
50-
GraphOption = graphOptions ?? new GraphOption()
50+
GraphOption = graphOptions ?? new GraphOption(),
51+
GraphRequestProofofPossession = new GraphRequestProofofPossession()
5152
};
5253
}
5354
/// <summary>

Diff for: src/Authentication/Authentication/Handlers/AuthenticationHandler.cs

+29-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
// ------------------------------------------------------------------------------
44

55

6+
using Azure.Core;
67
using Microsoft.Graph.Authentication;
8+
using Microsoft.Graph.PowerShell.Authentication.Core.Utilities;
79
using Microsoft.Graph.PowerShell.Authentication.Extensions;
810
using System;
911
using System.Collections.Generic;
@@ -63,9 +65,24 @@ private async Task AuthenticateRequestAsync(HttpRequestMessage httpRequestMessag
6365
{
6466
if (AuthenticationProvider != null)
6567
{
66-
var accessToken = await AuthenticationProvider.GetAuthorizationTokenAsync(httpRequestMessage.RequestUri, additionalAuthenticationContext, cancellationToken: cancellationToken).ConfigureAwait(false);
67-
if (!string.IsNullOrEmpty(accessToken))
68-
httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue(BearerAuthenticationScheme, accessToken);
68+
if (AuthenticationHelpers.IsATPoPSupported())
69+
{
70+
GraphSession.Instance.GraphRequestProofofPossession.Request.Method = AuthenticationHelpers.ConvertToAzureRequestMethod(httpRequestMessage.Method);
71+
GraphSession.Instance.GraphRequestProofofPossession.Request.Uri.Reset(httpRequestMessage.RequestUri);
72+
foreach (var header in httpRequestMessage.Headers)
73+
{
74+
GraphSession.Instance.GraphRequestProofofPossession.Request.Headers.Add(header.Key, header.Value.First());
75+
}
76+
77+
var accessToken = GraphSession.Instance.GraphRequestProofofPossession.BrowserCredential.GetTokenAsync(GraphSession.Instance.GraphRequestProofofPossession.PopTokenContext, cancellationToken).Result;
78+
httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Pop", accessToken.Token);
79+
}
80+
else
81+
{
82+
var accessToken = await AuthenticationProvider.GetAuthorizationTokenAsync(httpRequestMessage.RequestUri, additionalAuthenticationContext, cancellationToken: cancellationToken).ConfigureAwait(false);
83+
if (!string.IsNullOrEmpty(accessToken))
84+
httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue(BearerAuthenticationScheme, accessToken);
85+
}
6986
}
7087
}
7188

@@ -87,6 +104,15 @@ private async Task<HttpResponseMessage> SendRetryAsync(HttpResponseMessage httpR
87104
}
88105
await DrainAsync(httpResponseMessage).ConfigureAwait(false);
89106

107+
if (AuthenticationHelpers.IsATPoPSupported())
108+
{
109+
var popChallenge = httpResponseMessage.Headers.WwwAuthenticate.First(wa => wa.Scheme == "PoP");
110+
var nonceStart = popChallenge.Parameter.IndexOf("nonce=\"") + "nonce=\"".Length;
111+
var nonceEnd = popChallenge.Parameter.IndexOf('"', nonceStart);
112+
GraphSession.Instance.GraphRequestProofofPossession.ProofofPossessionNonce = popChallenge.Parameter.Substring(nonceStart, nonceEnd - nonceStart);
113+
GraphSession.Instance.GraphRequestProofofPossession.PopTokenContext = new PopTokenRequestContext(GraphSession.Instance.AuthContext.Scopes, isProofOfPossessionEnabled: true, proofOfPossessionNonce: GraphSession.Instance.GraphRequestProofofPossession.ProofofPossessionNonce, request: GraphSession.Instance.GraphRequestProofofPossession.Request);
114+
}
115+
90116
// Authenticate request using auth provider
91117
await AuthenticateRequestAsync(newRequest, additionalRequestInfo, cancellationToken).ConfigureAwait(false);
92118
httpResponseMessage = await base.SendAsync(newRequest, cancellationToken);

Diff for: src/Authentication/Authentication/Microsoft.Graph.Authentication.psd1

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
#
44
# Generated by: Microsoft
55
#
6-
# Generated on: 12/28/2023
6+
# Generated on: 21/09/2023
77
#
88

99
@{
@@ -12,7 +12,7 @@
1212
RootModule = './Microsoft.Graph.Authentication.psm1'
1313

1414
# Version number of this module.
15-
ModuleVersion = '2.11.1'
15+
ModuleVersion = '2.6.1'
1616

1717
# Supported PSEditions
1818
CompatiblePSEditions = 'Core', 'Desktop'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// ------------------------------------------------------------------------------
2+
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
3+
// ------------------------------------------------------------------------------
4+
5+
using Azure.Core;
6+
using Azure.Identity;
7+
using System;
8+
using System.IO;
9+
using System.Net.Http;
10+
11+
namespace Microsoft.Graph.PowerShell.Authentication
12+
{
13+
internal class GraphRequestProofofPossession : IGraphRequestProofofPossession
14+
{
15+
public Uri Uri { get; set; }
16+
public HttpMethod HttpMethod { get; set; }
17+
public AccessToken AccessToken { get; set; }
18+
public string ProofofPossessionNonce { get; set; }
19+
public PopTokenRequestContext PopTokenContext { get; set; }
20+
public Request Request { get; set; }
21+
public InteractiveBrowserCredential BrowserCredential { get; set; }
22+
}
23+
24+
}

Diff for: src/Authentication/Authentication/test/Get-MgGraphOption.Tests.ps1

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Describe "Get-MgGraphOption Command" {
1313
$GetMgGraphOptionCommand = Get-Command Set-MgGraphOption
1414
$GetMgGraphOptionCommand | Should -Not -BeNullOrEmpty
1515
$GetMgGraphOptionCommand.ParameterSets | Should -HaveCount 1
16-
$GetMgGraphOptionCommand.ParameterSets.Parameters | Should -HaveCount 13 # PS common parameters.
16+
$GetMgGraphOptionCommand.ParameterSets.Parameters | Should -HaveCount 14 # PS common parameters.
1717
}
1818

1919
It 'Executes successfully' {

0 commit comments

Comments
 (0)