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

Add API and make ROPC call #3103

Merged
merged 25 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
424d0c4
Add API and make ROPC call
neha-bhargava Oct 23, 2024
bebff84
Add silent call before attempting ROPC
neha-bhargava Oct 23, 2024
ad6d4f6
Minor updates to constants and comments
neha-bhargava Oct 23, 2024
3c20f84
Undo changes to txt files
neha-bhargava Oct 23, 2024
129d159
Merge branch 'master' into nebharg/ROPC
neha-bhargava Oct 23, 2024
a96b9d8
Address comments
neha-bhargava Oct 24, 2024
bd67c49
Merge branch 'nebharg/ROPC' of https://github.com/AzureAD/microsoft-i…
neha-bhargava Oct 24, 2024
ca9d3b6
Merge branch 'master' into nebharg/ROPC
neha-bhargava Oct 24, 2024
584929e
Merge branch 'master' into nebharg/ROPC
neha-bhargava Oct 25, 2024
5129cf1
Exclude file ClaimsConstant as it contains the constant Password
neha-bhargava Oct 25, 2024
5f5768f
Merge branch 'nebharg/ROPC' of https://github.com/AzureAD/microsoft-i…
neha-bhargava Oct 25, 2024
e1a88c3
Merge branch 'master' into nebharg/ROPC
neha-bhargava Oct 28, 2024
229b190
Address comments - Add logging and update constants
neha-bhargava Oct 28, 2024
7c80db3
Resolve warnings
neha-bhargava Oct 28, 2024
42ad9a8
Alternative to GetAccounts
neha-bhargava Oct 29, 2024
8860671
Update to only add the claim if not already present
neha-bhargava Oct 29, 2024
9a0df38
Update to use existing constants
neha-bhargava Oct 29, 2024
f7407e8
Add check before setting
neha-bhargava Oct 29, 2024
85eb1a4
Add comment to the method
neha-bhargava Oct 30, 2024
efba561
Merge branch 'master' into nebharg/ROPC
jmprieur Oct 31, 2024
fe4e4d5
fixing warnings
jmprieur Oct 31, 2024
913fdf9
Merge branch 'nebharg/ROPC' of http://github.com/AzureAD/microsoft-id…
jmprieur Oct 31, 2024
f4dfc21
Merge branch 'master' into nebharg/ROPC
neha-bhargava Nov 1, 2024
9c5b14d
Merge branch 'master' into nebharg/ROPC
neha-bhargava Nov 4, 2024
7a93b94
Address comments
neha-bhargava Nov 4, 2024
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
4 changes: 4 additions & 0 deletions build/credscan-exclusion.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
{
"file": "*InternalAPI.Unshipped.txt",
"_justification": "Unshipped public API."
},
{
"file": "ClaimConstants.cs",
"_justification": "Constant contains the word Password as it is for the ROPC flow password claim constant."
}
]
}
10 changes: 10 additions & 0 deletions src/Microsoft.Identity.Web.TokenAcquisition/ClaimConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,5 +99,15 @@ public static class ClaimConstants
/// Name Identifier ID claim: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier".
/// </summary>
public const string NameIdentifierId = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier";

/// <summary>
/// Username claims for ROPC flow.
/// </summary>
public const string Username = "xms_username";

/// <summary>
/// Password claims for ROPC flow.
/// </summary>
public const string Password = "xms_password";
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#nullable enable
const Microsoft.Identity.Web.ClaimConstants.Password = "xms_password" -> string!
const Microsoft.Identity.Web.ClaimConstants.Username = "xms_username" -> string!
Microsoft.Identity.Web.BeforeTokenAcquisitionForApp
Microsoft.Identity.Web.TokenAcquisitionExtensionOptions
Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeTokenAcquisitionForApp -> Microsoft.Identity.Web.BeforeTokenAcquisitionForApp?
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#nullable enable
const Microsoft.Identity.Web.ClaimConstants.Password = "xms_password" -> string!
const Microsoft.Identity.Web.ClaimConstants.Username = "xms_username" -> string!
Microsoft.Identity.Web.BeforeTokenAcquisitionForApp
Microsoft.Identity.Web.TokenAcquisitionExtensionOptions
Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeTokenAcquisitionForApp -> Microsoft.Identity.Web.BeforeTokenAcquisitionForApp?
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#nullable enable
const Microsoft.Identity.Web.ClaimConstants.Password = "xms_password" -> string!
const Microsoft.Identity.Web.ClaimConstants.Username = "xms_username" -> string!
Microsoft.Identity.Web.BeforeTokenAcquisitionForApp
Microsoft.Identity.Web.TokenAcquisitionExtensionOptions
Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeTokenAcquisitionForApp -> Microsoft.Identity.Web.BeforeTokenAcquisitionForApp?
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#nullable enable
const Microsoft.Identity.Web.ClaimConstants.Password = "xms_password" -> string!
const Microsoft.Identity.Web.ClaimConstants.Username = "xms_username" -> string!
Microsoft.Identity.Web.BeforeTokenAcquisitionForApp
Microsoft.Identity.Web.TokenAcquisitionExtensionOptions
Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeTokenAcquisitionForApp -> Microsoft.Identity.Web.BeforeTokenAcquisitionForApp?
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#nullable enable
const Microsoft.Identity.Web.ClaimConstants.Password = "xms_password" -> string!
const Microsoft.Identity.Web.ClaimConstants.Username = "xms_username" -> string!
Microsoft.Identity.Web.BeforeTokenAcquisitionForApp
Microsoft.Identity.Web.TokenAcquisitionExtensionOptions
Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeTokenAcquisitionForApp -> Microsoft.Identity.Web.BeforeTokenAcquisitionForApp?
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const Microsoft.Identity.Web.ClaimConstants.Password = "xms_password" -> string!
const Microsoft.Identity.Web.ClaimConstants.Username = "xms_username" -> string!
Microsoft.Identity.Web.BeforeTokenAcquisitionForApp
Microsoft.Identity.Web.TokenAcquisitionExtensionOptions
Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeTokenAcquisitionForApp -> Microsoft.Identity.Web.BeforeTokenAcquisitionForApp?
Expand Down
68 changes: 68 additions & 0 deletions src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,21 @@ public async Task<AuthenticationResult> GetAuthenticationResultForUserAsync(
try
{
AuthenticationResult? authenticationResult;

// If the user is not null and has claims xms-username and xms-password, perform ROPC for CCA
authenticationResult = await TryGetAuthenticationResultForConfidentialClientUsingRopcAsync(
application,
scopes,
user,
mergedOptions,
tokenAcquisitionOptions).ConfigureAwait(false);

if (authenticationResult != null)
{
LogAuthResult(authenticationResult);
return authenticationResult;
}

// Access token will return if call is from a web API
authenticationResult = await GetAuthenticationResultForWebApiToCallDownstreamApiAsync(
application,
Expand Down Expand Up @@ -313,6 +328,59 @@ public async Task<AuthenticationResult> GetAuthenticationResultForUserAsync(
}
}

// This method mutate the user claims to include claims uid and utid to perform the silent flow for subsequent calls.
private async Task<AuthenticationResult?> TryGetAuthenticationResultForConfidentialClientUsingRopcAsync(IConfidentialClientApplication application, IEnumerable<string> scopes, ClaimsPrincipal? user, MergedOptions mergedOptions, TokenAcquisitionOptions? tokenAcquisitionOptions)
{
if (user != null && user.HasClaim(c => c.Type == ClaimConstants.Username) && user.HasClaim(c => c.Type == ClaimConstants.Password))
{
string username = user.FindFirst(ClaimConstants.Username)?.Value ?? string.Empty;
string password = user.FindFirst(ClaimConstants.Password)?.Value ?? string.Empty;

if (user.GetMsalAccountId() != null)
jmprieur marked this conversation as resolved.
Show resolved Hide resolved
{
try
{
var account = await application.GetAccountAsync(user.GetMsalAccountId()).ConfigureAwait(false);
neha-bhargava marked this conversation as resolved.
Show resolved Hide resolved

// Silent flow
return await application.AcquireTokenSilent(
scopes.Except(_scopesRequestedByMsal),
account)
.ExecuteAsync()
.ConfigureAwait(false);
}
catch (Exception ex)
neha-bhargava marked this conversation as resolved.
Show resolved Hide resolved
{
// Log a message when the silent flow fails and try acquisition through ROPC.
Logger.TokenAcquisitionError(_logger, ex.Message, ex);
}

}

// ROPC flow
var authenticationResult = await ((IByUsernameAndPassword)application).AcquireTokenByUsernamePassword(
neha-bhargava marked this conversation as resolved.
Show resolved Hide resolved
scopes.Except(_scopesRequestedByMsal),
username,
password)
.ExecuteAsync()
.ConfigureAwait(false);

if (user.GetMsalAccountId() == null)
{
// Add the account id to the user (in case of ROPC flow)
user.AddIdentity(new CaseSensitiveClaimsIdentity(new[]
{
new Claim(ClaimConstants.UniqueObjectIdentifier, authenticationResult.Account.HomeAccountId.ObjectId),
new Claim(ClaimConstants.UniqueTenantIdentifier, authenticationResult.Account.HomeAccountId.TenantId),
}));
}

return authenticationResult;
}

return null;
}

private void LogAuthResult(AuthenticationResult? authenticationResult)
{
if (authenticationResult != null)
Expand Down
191 changes: 104 additions & 87 deletions src/Microsoft.Identity.Web/ClaimsPrincipalFactory.cs
Original file line number Diff line number Diff line change
@@ -1,111 +1,128 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Security.Claims;
using Microsoft.IdentityModel.Tokens;

namespace Microsoft.Identity.Web
{
/// <summary>
/// Factory class to create <see cref="ClaimsPrincipal"/> objects.
/// </summary>
public static class ClaimsPrincipalFactory
{
/// <summary>
/// Instantiate a <see cref="ClaimsPrincipal"/> from a home account object ID and home tenant ID. This can
/// be useful when the web app subscribes to another service on behalf of the user
/// and then is called back by a notification where the user is identified by their home tenant
/// ID and home object ID (like in Microsoft Graph Web Hooks).
/// </summary>
/// <param name="homeTenantId">Home tenant ID of the account.</param>
/// <param name="homeObjectId">Home object ID of the account in this tenant ID.</param>
/// <returns>A <see cref="ClaimsPrincipal"/> containing these two claims.</returns>
///
/// <example>
/// <code>
/// private async Task GetChangedMessagesAsync(IEnumerable&lt;Notification&gt; notifications)
/// {
/// HttpContext.User = ClaimsPrincipalExtension.FromHomeTenantIdAndHomeObjectId(subscription.HomeTenantId,
/// subscription.HomeUserId);
/// foreach (var notification in notifications)
/// {
/// SubscriptionStore subscription =
/// subscriptionStore.GetSubscriptionInfo(notification.SubscriptionId);
/// string accessToken = await tokenAcquisition.GetAccessTokenForUserAsync(scopes);
/// ...}
/// }
/// </code>
/// </example>
public static ClaimsPrincipal FromHomeTenantIdAndHomeObjectId(string homeTenantId, string homeObjectId)
{
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System.Security.Claims;
using Microsoft.IdentityModel.Tokens;
namespace Microsoft.Identity.Web
{
/// <summary>
/// Factory class to create <see cref="ClaimsPrincipal"/> objects.
/// </summary>
public static class ClaimsPrincipalFactory
{
/// <summary>
/// Instantiate a <see cref="ClaimsPrincipal"/> from a home account object ID and home tenant ID. This can
/// be useful when the web app subscribes to another service on behalf of the user
/// and then is called back by a notification where the user is identified by their home tenant
/// ID and home object ID (like in Microsoft Graph Web Hooks).
/// </summary>
/// <param name="homeTenantId">Home tenant ID of the account.</param>
/// <param name="homeObjectId">Home object ID of the account in this tenant ID.</param>
/// <returns>A <see cref="ClaimsPrincipal"/> containing these two claims.</returns>
///
/// <example>
/// <code>
/// private async Task GetChangedMessagesAsync(IEnumerable&lt;Notification&gt; notifications)
/// {
/// HttpContext.User = ClaimsPrincipalExtension.FromHomeTenantIdAndHomeObjectId(subscription.HomeTenantId,
/// subscription.HomeUserId);
/// foreach (var notification in notifications)
/// {
/// SubscriptionStore subscription =
/// subscriptionStore.GetSubscriptionInfo(notification.SubscriptionId);
/// string accessToken = await tokenAcquisition.GetAccessTokenForUserAsync(scopes);
/// ...}
/// }
/// </code>
/// </example>
public static ClaimsPrincipal FromHomeTenantIdAndHomeObjectId(string homeTenantId, string homeObjectId)
{
if (AppContextSwitches.UseClaimsIdentityType)
{
#pragma warning disable RS0030 // Do not use banned APIs
return new ClaimsPrincipal(
new ClaimsIdentity(new[]
{
new Claim(ClaimConstants.UniqueTenantIdentifier, homeTenantId),
{
new Claim(ClaimConstants.UniqueTenantIdentifier, homeTenantId),
new Claim(ClaimConstants.UniqueObjectIdentifier, homeObjectId),
}));
#pragma warning restore RS0030 // Do not use banned APIs
}
else
{
return new ClaimsPrincipal(
new CaseSensitiveClaimsIdentity(new[]
{
new Claim(ClaimConstants.UniqueTenantIdentifier, homeTenantId),
new Claim(ClaimConstants.UniqueObjectIdentifier, homeObjectId),
}));
}
}

/// <summary>
/// Instantiate a <see cref="ClaimsPrincipal"/> from an account object ID and tenant ID. This can
/// be useful when the web app subscribes to another service on behalf of the user
/// and then is called back by a notification where the user is identified by their tenant
/// ID and object ID (like in Microsoft Graph Web Hooks).
/// </summary>
/// <param name="tenantId">Tenant ID of the account.</param>
/// <param name="objectId">Object ID of the account in this tenant ID.</param>
/// <returns>A <see cref="ClaimsPrincipal"/> containing these two claims.</returns>
///
/// <example>
/// <code>
/// private async Task GetChangedMessagesAsync(IEnumerable&lt;Notification&gt; notifications)
/// {
/// HttpContext.User = ClaimsPrincipalExtension.FromTenantIdAndObjectId(subscription.TenantId,
/// subscription.UserId);
/// foreach (var notification in notifications)
/// {
/// SubscriptionStore subscription =
/// subscriptionStore.GetSubscriptionInfo(notification.SubscriptionId);
/// string accessToken = await tokenAcquisition.GetAccessTokenForUserAsync(scopes);
/// ...}
/// }
/// </code>
/// </example>
public static ClaimsPrincipal FromTenantIdAndObjectId(string tenantId, string objectId)
{
{
return new ClaimsPrincipal(
new CaseSensitiveClaimsIdentity(new[]
{
new Claim(ClaimConstants.UniqueTenantIdentifier, homeTenantId),
new Claim(ClaimConstants.UniqueObjectIdentifier, homeObjectId),
}));
}
}
/// <summary>
/// Instantiate a <see cref="ClaimsPrincipal"/> from an account object ID and tenant ID. This can
/// be useful when the web app subscribes to another service on behalf of the user
/// and then is called back by a notification where the user is identified by their tenant
/// ID and object ID (like in Microsoft Graph Web Hooks).
/// </summary>
/// <param name="tenantId">Tenant ID of the account.</param>
/// <param name="objectId">Object ID of the account in this tenant ID.</param>
/// <returns>A <see cref="ClaimsPrincipal"/> containing these two claims.</returns>
///
/// <example>
/// <code>
/// private async Task GetChangedMessagesAsync(IEnumerable&lt;Notification&gt; notifications)
/// {
/// HttpContext.User = ClaimsPrincipalExtension.FromTenantIdAndObjectId(subscription.TenantId,
/// subscription.UserId);
/// foreach (var notification in notifications)
/// {
/// SubscriptionStore subscription =
/// subscriptionStore.GetSubscriptionInfo(notification.SubscriptionId);
/// string accessToken = await tokenAcquisition.GetAccessTokenForUserAsync(scopes);
/// ...}
/// }
/// </code>
/// </example>
public static ClaimsPrincipal FromTenantIdAndObjectId(string tenantId, string objectId)
{
if (AppContextSwitches.UseClaimsIdentityType)
{
#pragma warning disable RS0030 // Do not use banned APIs
return new ClaimsPrincipal(
new ClaimsIdentity(new[]
{
new Claim(ClaimConstants.Tid, tenantId),
{
new Claim(ClaimConstants.Tid, tenantId),
new Claim(ClaimConstants.Oid, objectId),
}));
#pragma warning restore RS0030 // Do not use banned APIs
} else
{
return new ClaimsPrincipal(
new CaseSensitiveClaimsIdentity(new[]
{
new Claim(ClaimConstants.Tid, tenantId),
{
new Claim(ClaimConstants.Tid, tenantId),
new Claim(ClaimConstants.Oid, objectId),
}));
}
}
}
}
}
}

/// <summary>
/// Instantiate a <see cref="ClaimsPrincipal"/> from a username and password.
/// This can be used for ROPC flow for testing purposes.
/// </summary>
/// <param name="username">UPN of the user for example username@domain.</param>
/// <param name="password">Password for the user.</param>
/// <returns>A <see cref="ClaimsPrincipal"/> containing these two claims.</returns>
public static ClaimsPrincipal FromUsernamePassword(string username, string password)
{
return new ClaimsPrincipal(
new CaseSensitiveClaimsIdentity(new[]
{
new Claim(ClaimConstants.Username, username),
new Claim(ClaimConstants.Password, password),
}));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,0 +1,2 @@
#nullable enable
static Microsoft.Identity.Web.ClaimsPrincipalFactory.FromUsernamePassword(string! username, string! password) -> System.Security.Claims.ClaimsPrincipal!

Check warning on line 2 in src/Microsoft.Identity.Web/PublicAPI/net462/PublicAPI.Unshipped.txt

View workflow job for this annotation

GitHub Actions / Build and run unit tests

Symbol 'static Microsoft.Identity.Web.ClaimsPrincipalFactory.FromUsernamePassword(string! username, string! password) -> System.Security.Claims.ClaimsPrincipal!' is part of the declared API, but is either not public or could not be found (https://github.com/dotnet/roslyn-analyzers/blob/main/src/PublicApiAnalyzers/PublicApiAnalyzers.Help.md)
Original file line number Diff line number Diff line change
@@ -1,0 +1,2 @@
#nullable enable
static Microsoft.Identity.Web.ClaimsPrincipalFactory.FromUsernamePassword(string! username, string! password) -> System.Security.Claims.ClaimsPrincipal!

Check warning on line 2 in src/Microsoft.Identity.Web/PublicAPI/net472/PublicAPI.Unshipped.txt

View workflow job for this annotation

GitHub Actions / Build and run unit tests

Symbol 'static Microsoft.Identity.Web.ClaimsPrincipalFactory.FromUsernamePassword(string! username, string! password) -> System.Security.Claims.ClaimsPrincipal!' is part of the declared API, but is either not public or could not be found (https://github.com/dotnet/roslyn-analyzers/blob/main/src/PublicApiAnalyzers/PublicApiAnalyzers.Help.md)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#nullable enable
static Microsoft.Identity.Web.ClaimsPrincipalFactory.FromUsernamePassword(string! username, string! password) -> System.Security.Claims.ClaimsPrincipal!
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#nullable enable
static Microsoft.Identity.Web.ClaimsPrincipalFactory.FromUsernamePassword(string! username, string! password) -> System.Security.Claims.ClaimsPrincipal!
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#nullable enable
static Microsoft.Identity.Web.ClaimsPrincipalFactory.FromUsernamePassword(string! username, string! password) -> System.Security.Claims.ClaimsPrincipal!
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#nullable enable
static Microsoft.Identity.Web.ClaimsPrincipalFactory.FromUsernamePassword(string! username, string! password) -> System.Security.Claims.ClaimsPrincipal!
Loading
Loading