Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Access Token Proof of Possession Capability #2512

Open
wants to merge 7 commits into
base: dev
Choose a base branch
from
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
FehintolaObafemi marked this conversation as resolved.
Show resolved Hide resolved
```

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:
FehintolaObafemi marked this conversation as resolved.
Show resolved Hide resolved

```powershell
Set-MgGraphOption -EnableATPoP $true
FehintolaObafemi marked this conversation as resolved.
Show resolved Hide resolved
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

FehintolaObafemi marked this conversation as resolved.
Show resolved Hide resolved
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
FehintolaObafemi marked this conversation as resolved.
Show resolved Hide resolved
```

- 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 @@ -47129,4 +47129,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 @@ -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 IGraphRequestPopContext GraphRequestPopContext { 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,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; }
FehintolaObafemi marked this conversation as resolved.
Show resolved Hide resolved
FehintolaObafemi marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// ------------------------------------------------------------------------------
// 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.Core.Pipeline;
using Azure.Identity;
using System;
using System.Net.Http;

namespace Microsoft.Graph.PowerShell.Authentication
{
public interface IGraphRequestPopContext
{
Uri Uri { get; set; }
HttpMethod HttpMethod { get; set; }
AccessToken AccessToken { get; set; }
HttpPipeline PopPipeline { get; set; }
InteractiveBrowserCredential PopInteractiveBrowserCredential { 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; }
IGraphRequestPopContext GraphRequestPopContext { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="$(MSBuildThisFileDirectory)..\..\..\Repo.props" />
<PropertyGroup>
<LangVersion>9.0</LangVersion>
<TargetFrameworks>netstandard2.0;net6.0;net472</TargetFrameworks>
<RootNamespace>Microsoft.Graph.PowerShell.Authentication.Core</RootNamespace>
<Version>2.18.0</Version>
<Version>2.12.0</Version>
</PropertyGroup>
<PropertyGroup>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" Version="1.11.4" />
<PackageReference Include="Azure.Identity.Broker" Version="1.1.0" />
<PackageReference Include="Microsoft.Graph.Core" Version="3.1.13" />
<PackageReference Include="Azure.Core.Experimental" Version="0.1.0-preview.34" />
<PackageReference Include="Azure.Identity" Version="1.13.0-beta.1" />
<PackageReference Include="Azure.Identity.Broker" Version="1.2.0-beta.1" />
Copy link
Contributor

Choose a reason for hiding this comment

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

In previous conversations, we had wondered if Azure.Identity is actually bringing any value here over using Microsoft.Identity.Client directly. Azure.Identity is intended to limit the surface area exposed to 3P developers. This is not relevant for PowerShell scenarios. I am especially concerned when I see the introduction of Azure.Core.Pipeline. We explicitly don't use the Azure Pipelining because it is only there to abstract away language differences, and again that isn't relevant because our PowerShell modules are only implemented in C#.

<PackageReference Include="Microsoft.Graph.Core" Version="3.1.14" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.62.0" />
<PackageReference Include="Microsoft.Identity.Client.Broker" Version="4.62.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<Target Name="CopyFiles" AfterTargets="Build">
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 All @@ -14,6 +15,8 @@
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -113,23 +116,44 @@ 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()) :
FehintolaObafemi marked this conversation as resolved.
Show resolved Hide resolved
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);
var popTokenRequestContext = new PopTokenRequestContext();
if (GraphSession.Instance.GraphOption.EnableATPoPForMSGraph)
{
popTokenRequestContext = await CreatePopTokenRequestContext(authContext);
GraphSession.Instance.GraphRequestPopContext.PopInteractiveBrowserCredential = interactiveBrowserCredential;
}

if (!File.Exists(Constants.AuthRecordPath))
{
AuthenticationRecord authRecord;
var interactiveBrowserCredential = new InteractiveBrowserCredential(interactiveOptions);
if (IsWamSupported())
FehintolaObafemi marked this conversation as resolved.
Show resolved Hide resolved
{
authRecord = await Task.Run(() =>
// Adding a scenario to account for Access Token Proof of Possession
if (GraphSession.Instance.GraphOption.EnableATPoPForMSGraph)
{
// Run the thread in MTA.
return interactiveBrowserCredential.Authenticate(new TokenRequestContext(authContext.Scopes), cancellationToken);
});
authRecord = await Task.Run(() =>
{
// Run the thread in MTA.
return interactiveBrowserCredential.AuthenticateAsync(popTokenRequestContext, cancellationToken);
});
}
else
{
authRecord = await Task.Run(() =>
{
// Run the thread in MTA.
return interactiveBrowserCredential.Authenticate(new TokenRequestContext(authContext.Scopes), cancellationToken);
FehintolaObafemi marked this conversation as resolved.
Show resolved Hide resolved
});
}
}
else
{
Expand Down Expand Up @@ -446,5 +470,34 @@ public static Task DeleteAuthRecordAsync()
File.Delete(Constants.AuthRecordPath);
return Task.CompletedTask;
}

private static async Task<PopTokenRequestContext> CreatePopTokenRequestContext(IAuthContext authContext)
{
// Creating a httpclient that would handle all pop calls
Uri popResourceUri = GraphSession.Instance.GraphRequestPopContext.Uri ?? new Uri("https://graph.microsoft.com/beta/organization");
HttpClient popHttpClient = new(new HttpClientHandler());
FehintolaObafemi marked this conversation as resolved.
Show resolved Hide resolved

// Find the nonce in the WWW-Authenticate header in the response.
var popMethod = GraphSession.Instance.GraphRequestPopContext.HttpMethod ?? HttpMethod.Get;
var popResponse = await popHttpClient.SendAsync(new HttpRequestMessage(popMethod, popResourceUri));

// Refresh token logic --- start
var popPipelineOptions = new HttpPipelineOptions(new PopClientOptions()
{

});

GraphSession.Instance.GraphRequestPopContext.PopPipeline = HttpPipelineBuilder.Build(popPipelineOptions, new HttpPipelineTransportOptions());
var popRequest = GraphSession.Instance.GraphRequestPopContext.PopPipeline.CreateRequest();
popRequest.Method = RequestMethod.Parse(popMethod.Method.ToUpper());
popRequest.Uri.Reset(popResourceUri);

// Refresh token logic --- end
var popContext = new PopTokenRequestContext(authContext.Scopes, isProofOfPossessionEnabled: true, proofOfPossessionNonce: WwwAuthenticateParameters.CreateFromAuthenticationHeaders(popResponse.Headers, "Pop").Nonce, request: popRequest);
return popContext;
}
}
internal class PopClientOptions : ClientOptions
FehintolaObafemi marked this conversation as resolved.
Show resolved Hide resolved
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1030,6 +1030,8 @@ private async Task ProcessRecordAsync()
try
{
PrepareSession();
GraphSession.Instance.GraphRequestPopContext.Uri = Uri;
GraphSession.Instance.GraphRequestPopContext.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 @@ -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
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(),
GraphRequestPopContext = new GraphRequestPopContext()
};
}
/// <summary>
Expand Down
Loading