Skip to content

Commit

Permalink
Add API and make ROPC call (#3103)
Browse files Browse the repository at this point in the history
* Add API and make ROPC call

* Add silent call before attempting ROPC

* Minor updates to constants and comments

* Undo changes to txt files

* Address comments

* Exclude file ClaimsConstant as it contains the constant Password

* Address comments - Add logging and update constants

* Resolve warnings

* Alternative to GetAccounts

* Update to only add the claim if not already present

* Update to use existing constants

* Add check before setting

* Add comment to the method

* fixing warnings
- public API
- no ConfigureAwait(false) in tests (instable)

* Address comments

---------

Co-authored-by: Jean-Marc Prieur <jmprieur@microsoft.com>
  • Loading branch information
neha-bhargava and jmprieur authored Nov 4, 2024
1 parent 3dc8286 commit 011bd15
Show file tree
Hide file tree
Showing 18 changed files with 246 additions and 87 deletions.
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)
{
try
{
var account = await application.GetAccountAsync(user.GetMsalAccountId()).ConfigureAwait(false);

// Silent flow
return await application.AcquireTokenSilent(
scopes.Except(_scopesRequestedByMsal),
account)
.ExecuteAsync()
.ConfigureAwait(false);
}
catch (MsalException ex)
{
// 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(
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
@@ -0,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
@@ -0,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

0 comments on commit 011bd15

Please sign in to comment.