Skip to content
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions docs/AT-Pop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Microsoft Graph PowerShell SDK: Access Token Proof of Possession (AT PoP) Capability

## Overview

This README provides comprehensive details on the Access Token Proof of Possession (AT PoP) functionality introduced in the Microsoft Graph PowerShell SDK. This feature enhances security by binding tokens to specific HTTP methods and URIs, ensuring they are used only for their intended purposes.

## Table of Contents

- [Key Features](#key-features)
- [Installation](#installation)
- [Configuration](#configuration)
- [Usage Examples](#usage-examples)
- [References](#references)

## Key Features

- **Access Token Proof of Possession (AT PoP)**: This feature binds tokens to specific HTTP methods and URIs, preventing misuse of tokens by ensuring they are used only for the intended HTTP requests.
- **Updated Dependencies**: Compatibility improvements with recent library changes.
- **Enhanced Token Acquisition Options**: Users can now specify the HTTP method and URI during token acquisition to further secure token usage.

### Token acquisition behaviors

| Condition | Unbound (default) | Bound (PoP) |
|-----------|-----------|-----------|
| First sign-in | New token, interactive| New token, interactive |
| Existing token, same URI | No new token, silent | No new token, silent |
| Existing token, different URI | No new token, silent | New token, silent |
| Existing expired token, below max token refreshes | New token, silent | New token, silent |
| Existing expired token, exceeded max refreshes | New token, interactive | New token, interactive |

## Installation

To install the Microsoft Graph PowerShell SDK with the latest updates, use the following command:

```powershell
Install-Module -Name Microsoft.Graph -AllowClobber -Force
```

Ensure you are using the latest version to access the AT PoP functionality.

## Configuration

### Enabling Access Token Proof of Possession

To enable AT PoP, configure the Microsoft Graph SDK options as follows:

```powershell
Set-MgGraphOption -EnableATPoP $true
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we release this specific feature as preview / experimental? Does MS Graph PS have this capability?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good idea. @timayabi2020 can this be released as a preview version similar to the version 2.0 roll out?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it can, however I think we should first publish the feature to an internal feed and get a few guys to test internally

Connect-MgGraph
```

This configuration ensures that the acquired token is only valid for the specified HTTP method and URI.

## Usage Examples

### Example 1:

```powershell
Set-MgGraphOption -EnableATPoP $true
Connect-MgGraph
Invoke-MgGraphRequest -Method GET https://graph.microsoft.com/v1.0/me -Debug
```

### Example 2:

```powershell
Set-MgGraphOption -EnableATPoP $true
Connect-MgGraph
Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/v1.0/me/sendMail" -Method POST -Debug
```

## References

This README provides a detailed guide on the new AT PoP functionality, offering users the ability to secure their token usage effectively. If you have any questions or need further assistance, please refer to the official [Microsoft Graph PowerShell SDK documentation](https://docs.microsoft.com/en-us/powershell/microsoftgraph/).
20 changes: 20 additions & 0 deletions docs/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,26 @@ When using `-AccessToken`, we won't have access to the refresh token and the cli

Before using the provided `-AccessToken` to get Microsoft Graph resources, customers should ensure that the access token has the necessary scopes/ permissions needed to access/modify a resource.

### 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 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 Web Account Manager (WAM) to function.

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

- To enable AT PoP on supported devices

```PowerShell
Set-MgGraphOption -EnableATPoP $true
```

- To disable AT PoP on supported devices

```PowerShell
Set-MgGraphOption -EnableATPoP $false
```

## Web Account Manager (WAM)

WAM is a Windows 10+ component that acts as an authentication broker allowing the users of an app benefit from integration with accounts known to Windows, such as the account already signed into an active Windows session.
Expand Down
2 changes: 1 addition & 1 deletion openApiDocs/beta/DeviceManagement.Actions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -487,4 +487,4 @@ components:
tokenUrl: https://login.microsoftonline.com/common/oauth2/v2.0/token
scopes: { }
security:
- azureaadv2: [ ]
- azureaadv2: [ ]
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,11 @@
// 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
{
bool EnableWAMForMSGraph { get; set; }
bool EnableATPoPForMSGraph { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<LangVersion>9.0</LangVersion>
<TargetFrameworks>netstandard2.0;net6.0;net472</TargetFrameworks>
<RootNamespace>Microsoft.Graph.PowerShell.Authentication.Core</RootNamespace>
<Version>2.31.0</Version>
<Version>2.32.0</Version>
<!-- Suppress .NET Target Framework Moniker (TFM) Support Build Warnings -->
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
</PropertyGroup>
Expand All @@ -15,7 +15,9 @@
<ItemGroup>
<PackageReference Include="Azure.Identity" Version="1.13.2" />
<PackageReference Include="Azure.Identity.Broker" Version="1.2.0" />
<PackageReference Include="Microsoft.Graph.Core" Version="3.2.4" />
<PackageReference Include="Microsoft.Graph.Core" Version="3.2.2" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.67.2" />
<PackageReference Include="Microsoft.Identity.Client.Broker" Version="4.67.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.Text.Json" Version="8.0.5" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// ------------------------------------------------------------------------------
using Azure.Core;
using Azure.Core.Diagnostics;
using Azure.Core.Pipeline;
using Azure.Identity;
using Azure.Identity.Broker;
using Microsoft.Graph.Authentication;
Expand Down Expand Up @@ -114,22 +115,24 @@ 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 (!File.Exists(Constants.AuthRecordPath))
{
AuthenticationRecord authRecord;
var interactiveBrowserCredential = new InteractiveBrowserCredential(interactiveOptions);
if (IsWamSupported())
{
authRecord = await Task.Run(() =>
{
// Run the thread in MTA.
return interactiveBrowserCredential.Authenticate(new TokenRequestContext(authContext.Scopes), cancellationToken);
return interactiveBrowserCredential.AuthenticateAsync(new TokenRequestContext(authContext.Scopes), cancellationToken);
});
}
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ public class SetMgGraphOption : PSCmdlet
{
[Parameter]
public bool EnableLoginByWAM { get; set; }

[Parameter]
public bool EnableATPoP { get; set; }

protected override void BeginProcessing()
{
Expand All @@ -27,6 +30,11 @@ protected override void ProcessRecord()
GraphSession.Instance.GraphOption.EnableWAMForMSGraph = EnableLoginByWAM;
WriteDebug($"Signin by Web Account Manager (WAM) is {(EnableLoginByWAM ? "enabled" : "disabled")}.");
}
if (this.IsParameterBound(nameof(EnableATPoP)))
{
GraphSession.Instance.GraphOption.EnableATPoPForMSGraph = EnableATPoP;
WriteDebug($"Access Token Proof of Posession (AT-PoP) is {(EnableATPoP ? "enabled" : "disabled")}.");
}
File.WriteAllText(Constants.GraphOptionsFilePath, JsonConvert.SerializeObject(GraphSession.Instance.GraphOption, Formatting.Indented));
}

Expand Down
124 changes: 121 additions & 3 deletions src/Authentication/Authentication/Handlers/AuthenticationHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@
// ------------------------------------------------------------------------------


using Azure.Core;
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;
using System.Management.Automation;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
Expand All @@ -21,7 +25,10 @@ internal class AuthenticationHandler : DelegatingHandler
{
private const string ClaimsKey = "claims";
private const string BearerAuthenticationScheme = "Bearer";
private const string PopAuthenticationScheme = "Pop";
private int MaxRetry { get; set; } = 1;
private TokenRequestContext popTokenRequestContext;
private string cachedNonce;

public AzureIdentityAccessTokenProvider AuthenticationProvider { get; set; }

Expand All @@ -45,6 +52,24 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage

HttpResponseMessage response = await base.SendAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false);

// Extract nonce from API responses for future PoP requests
if (GraphSession.Instance.GraphOption.EnableATPoPForMSGraph && IsApiRequest(httpRequestMessage.RequestUri))
{
try
{
var wwwAuthParams = WwwAuthenticateParameters.CreateFromAuthenticationHeaders(response.Headers, PopAuthenticationScheme);
if (wwwAuthParams?.Nonce != null && !string.IsNullOrEmpty(wwwAuthParams.Nonce))
{
cachedNonce = wwwAuthParams.Nonce;
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"AuthenticationHandler: Failed to extract PoP nonce: {ex.Message}");
// Don't throw - nonce extraction failure shouldn't break the response
}
}

// Check if response is a 401 & is not a streamed body (is buffered)
if (response.StatusCode == HttpStatusCode.Unauthorized && httpRequestMessage.IsBuffered())
{
Expand All @@ -63,9 +88,55 @@ 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);
// Determine if this is an API request that should use PoP (when enabled)
// vs an authentication request that should always use Bearer
bool isApiRequest = IsApiRequest(httpRequestMessage.RequestUri);
bool shouldUsePoP = GraphSession.Instance.GraphOption.EnableATPoPForMSGraph && isApiRequest;

// Debug logging for flow routing
if (GraphSession.Instance.GraphOption.EnableATPoPForMSGraph)
{
var requestType = isApiRequest ? "API" : "Auth";
var tokenType = shouldUsePoP ? "PoP" : "Bearer";
System.Diagnostics.Debug.WriteLine($"AuthenticationHandler: {requestType} request to {httpRequestMessage.RequestUri?.Host} using {tokenType} token");
}

if (shouldUsePoP)
{
// API Request with PoP enabled - use PoP tokens ONLY
try
{
// Create proper TokenRequestContext for PoP
// Note: cachedNonce may be null for initial requests - this is expected
popTokenRequestContext = new TokenRequestContext(
scopes: GraphSession.Instance.AuthContext.Scopes,
parentRequestId: null,
claims: additionalAuthenticationContext?.ContainsKey(ClaimsKey) == true ? additionalAuthenticationContext[ClaimsKey]?.ToString() : null,
tenantId: null,
isCaeEnabled: false,
isProofOfPossessionEnabled: true,
proofOfPossessionNonce: cachedNonce // May be null for initial requests
);

// Get TokenCredential from existing AuthenticationProvider
var tokenCredential = await AuthenticationHelpers.GetTokenCredentialAsync(
GraphSession.Instance.AuthContext, cancellationToken).ConfigureAwait(false);

var accessToken = await tokenCredential.GetTokenAsync(popTokenRequestContext, cancellationToken).ConfigureAwait(false);
httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue(PopAuthenticationScheme, accessToken.Token);
}
catch (Exception ex) when (!(ex is OperationCanceledException))
{
// Re-throw with context for PoP-specific failures
throw new AuthenticationException($"Failed to acquire PoP token for {httpRequestMessage.RequestUri}: {ex.Message}", ex);
}
}
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 Down Expand Up @@ -128,5 +199,52 @@ private static async Task DrainAsync(HttpResponseMessage response)
}
response.Dispose();
}

/// <summary>
/// Determines if the request is an API request that should use PoP when enabled,
/// vs an authentication/token request that should always use Bearer tokens.
/// This method implements the core routing logic for AT-PoP:
/// - Graph API endpoints → PoP tokens (when enabled)
/// - Authentication endpoints → Bearer tokens (always)
/// - Unknown endpoints → Bearer tokens (safe default)
/// </summary>
/// <param name="requestUri">The request URI to evaluate</param>
/// <returns>True if this is an API request, false if it's an authentication request</returns>
private static bool IsApiRequest(Uri requestUri)
{
if (requestUri == null) return false;

var host = requestUri.Host?.ToLowerInvariant();
var path = requestUri.AbsolutePath?.ToLowerInvariant();

// Microsoft Graph API endpoints that should use PoP
if (host?.Contains("graph.microsoft.com") == true ||
host?.Contains("graph.microsoft.us") == true ||
host?.Contains("microsoftgraph.chinacloudapi.cn") == true ||
host?.Contains("graph.microsoft.de") == true)
{
// Exclude authentication/token endpoints - these should always use Bearer
if (path?.Contains("/oauth2/") == true ||
path?.Contains("/token") == true ||
path?.Contains("/authorize") == true ||
path?.Contains("/devicecode") == true)
{
return false; // Authentication request
}
return true; // API request
}

// Azure AD/authentication endpoints - never use PoP
if (host?.Contains("login.microsoftonline.com") == true ||
host?.Contains("login.microsoft.com") == true ||
host?.Contains("login.chinacloudapi.cn") == true ||
host?.Contains("login.microsoftonline.de") == true ||
host?.Contains("login.microsoftonline.us") == true)
{
return false; // Authentication request
}

return false; // Default to authentication request for unknown endpoints
}
}
}
1 change: 1 addition & 0 deletions src/Authentication/Authentication/Models/GraphOption.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace Microsoft.Graph.PowerShell.Authentication
internal class GraphOption : IGraphOption
{
public bool EnableWAMForMSGraph { get; set; }
public bool EnableATPoPForMSGraph { 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
Loading
Loading