Skip to content
This repository has been archived by the owner on Jun 30, 2022. It is now read-only.

Commit

Permalink
Updated LinkedAccounts with Microsoft.Identity.Web package that conta…
Browse files Browse the repository at this point in the history
…ins helper methods to enable users to authenticate with Azure AD & personal Microsoft accounts. (#1490)

Updated to .Net Core 2.2 framework.
Addressing some stylecop issues with comments
  • Loading branch information
ryanisgrig authored Jun 5, 2019
1 parent 05303f7 commit be3dc23
Show file tree
Hide file tree
Showing 36 changed files with 3,537 additions and 24 deletions.
10 changes: 8 additions & 2 deletions solutions/linkedaccounts/LinkedAccounts.sln
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27004.2005
# Visual Studio Version 16
VisualStudioVersion = 16.0.28803.452
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LinkedAccounts.Web", "linkedaccounts.web\LinkedAccounts.Web.csproj", "{C2C15C6D-720B-46B7-B1B1-F1B015487A9E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.Web", "Microsoft.Identity.Web\Microsoft.Identity.Web.csproj", "{8B15534C-586F-42C8-ACCD-0B300CF83815}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -15,6 +17,10 @@ Global
{C2C15C6D-720B-46B7-B1B1-F1B015487A9E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C2C15C6D-720B-46B7-B1B1-F1B015487A9E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C2C15C6D-720B-46B7-B1B1-F1B015487A9E}.Release|Any CPU.Build.0 = Release|Any CPU
{8B15534C-586F-42C8-ACCD-0B300CF83815}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8B15534C-586F-42C8-ACCD-0B300CF83815}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8B15534C-586F-42C8-ACCD-0B300CF83815}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8B15534C-586F-42C8-ACCD-0B300CF83815}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
39 changes: 39 additions & 0 deletions solutions/linkedaccounts/Microsoft.Identity.Web/ClaimConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/************************************************************************************************
The MIT License (MIT)
Copyright (c) 2015 Microsoft Corporation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
***********************************************************************************************/

namespace Microsoft.Identity.Web
{
/// <summary>
/// Constants for claim types.
/// </summary>
public static class ClaimConstants
{
public const string Name = "name";
public const string ObjectId = "http://schemas.microsoft.com/identity/claims/objectidentifier";
public const string Oid = "oid";
public const string PreferredUserName = "preferred_username";
public const string TenantId = "http://schemas.microsoft.com/identity/claims/tenantid";
public const string Tid = "tid";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/************************************************************************************************
The MIT License (MIT)
Copyright (c) 2015 Microsoft Corporation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
***********************************************************************************************/

using Microsoft.Identity.Client;
using System.Security.Claims;

namespace Microsoft.Identity.Web
{
public static class ClaimsPrincipalExtension
{
/// <summary>
/// Gets the Account identifier for an MSAL.NET account from a <see cref="ClaimsPrincipal"/>
/// </summary>
/// <param name="claimsPrincipal">Claims principal</param>
/// <returns>A string corresponding to an account identifier as defined in <see cref="Microsoft.Identity.Client.AccountId.Identifier"/></returns>
public static string GetMsalAccountId(this ClaimsPrincipal claimsPrincipal)
{
string userObjectId = GetObjectId(claimsPrincipal);
string tenantId = GetTenantId(claimsPrincipal);
if (!string.IsNullOrWhiteSpace(userObjectId) && !string.IsNullOrWhiteSpace(tenantId))
{
return $"{userObjectId}.{tenantId}";
}

return null;
}

/// <summary>
/// Gets the unique object ID associated with the <see cref="ClaimsPrincipal"/>
/// </summary>
/// <param name="claimsPrincipal">the <see cref="ClaimsPrincipal"/> from which to retrieve the unique object id</param>
/// <returns>Unique object ID of the identity, or <c>null</c> if it cannot be found</returns>
public static string GetObjectId(this ClaimsPrincipal claimsPrincipal)
{
string userObjectId = claimsPrincipal.FindFirstValue(ClaimConstants.Oid);
if (string.IsNullOrEmpty(userObjectId))
userObjectId = claimsPrincipal.FindFirstValue(ClaimConstants.ObjectId);

return userObjectId;
}

/// <summary>
/// Gets the Tenant ID associated with the <see cref="ClaimsPrincipal"/>
/// </summary>
/// <param name="claimsPrincipal">the <see cref="ClaimsPrincipal"/> from which to retrieve the tenant id</param>
/// <returns>Tenant ID of the identity, or <c>null</c> if it cannot be found</returns>
public static string GetTenantId(this ClaimsPrincipal claimsPrincipal)
{
string tenantId = claimsPrincipal.FindFirstValue(ClaimConstants.Tid);
if (string.IsNullOrEmpty(tenantId))
tenantId = claimsPrincipal.FindFirstValue(ClaimConstants.TenantId);

return tenantId;
}

/// <summary>
/// Gets the login-hint associated with a <see cref="ClaimsPrincipal"/>
/// </summary>
/// <param name="claimsPrincipal">Identity for which to complete the login-hint</param>
/// <returns>login-hint for the identity, or <c>null</c> if it cannot be found</returns>
public static string GetLoginHint(this ClaimsPrincipal claimsPrincipal)
{
return GetDisplayName(claimsPrincipal);
}

/// <summary>
/// Gets the domain-hint associated with an identity
/// </summary>
/// <param name="claimsPrincipal">Identity for which to compte the domain-hint</param>
/// <returns>domain-hint for the identity, or <c>null</c> if it cannot be found</returns>
public static string GetDomainHint(this ClaimsPrincipal claimsPrincipal)
{
// Tenant for MSA accounts
const string msaTenantId = "9188040d-6c67-4c5b-b112-36a304b66dad";

var tenantId = GetTenantId(claimsPrincipal);
return string.IsNullOrWhiteSpace(tenantId) ? null : tenantId == msaTenantId ? "consumers" : "organizations";
}

/// <summary>
/// Get the display name for the signed-in user, from the <see cref="ClaimsPrincipal"/>
/// </summary>
/// <param name="claimsPrincipal">Claims about the user/account</param>
/// <returns>A string containing the display name for the user, as brought by Azure AD v1.0 and v2.0 tokens,
/// or <c>null</c> if the claims cannot be found</returns>
/// <remarks>See https://docs.microsoft.com/en-us/azure/active-directory/develop/id-tokens#payload-claims </remarks>
public static string GetDisplayName(this ClaimsPrincipal claimsPrincipal)
{
// Attempting the claims brought by an Azure AD v2.0 token first
string displayName = claimsPrincipal.FindFirstValue(ClaimConstants.PreferredUserName);

// Otherwise falling back to the claims brought by an Azure AD v1.0 token
if (string.IsNullOrWhiteSpace(displayName))
{
displayName = claimsPrincipal.FindFirstValue(ClaimsIdentity.DefaultNameClaimType);
}

// Finally falling back to name
if (string.IsNullOrWhiteSpace(displayName))
{
displayName = claimsPrincipal.FindFirstValue(ClaimConstants.Name);
}

return displayName;
}

/// <summary>
/// Instantiate a ClaimsPrincipal from an account objectId and tenantId. This can
/// we 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 his 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 ClaimsPrincipal containing these two claims</returns>
/// <example>
/// <code>
/// private async Task GetChangedMessagesAsync(IEnumerable<Notification> notifications)
/// {
/// foreach (var notification in notifications)
/// {
/// SubscriptionStore subscription =
/// subscriptionStore.GetSubscriptionInfo(notification.SubscriptionId);
/// HttpContext.User = ClaimsPrincipalExtension.FromTenantIdAndObjectId(subscription.TenantId,
/// subscription.UserId);
/// string accessToken = await tokenAcquisition.GetAccessTokenOnBehalfOfUser(HttpContext, scopes);,
/// </code>
/// </example>
public static ClaimsPrincipal FromTenantIdAndObjectId(string tenantId, string objectId)
{
return new ClaimsPrincipal(
new ClaimsIdentity(new Claim[]
{
new Claim(ClaimConstants.Tid, tenantId),
new Claim(ClaimConstants.Oid, objectId)
})
);
}

/// <summary>
/// Creates the <see cref="ClaimsPrincipal"/> from the values found in an <see cref="IAccount"/>
/// </summary>
/// <param name="account">The IAccount instance</param>
/// <returns>A <see cref="ClaimsPrincipal"/> built from IAccount</returns>
public static ClaimsPrincipal ToClaimsPrincipal(this IAccount account)
{
if (account != null)
{
return new ClaimsPrincipal(
new ClaimsIdentity(new Claim[]
{
new Claim(ClaimConstants.Oid, account.HomeAccountId.ObjectId),
new Claim(ClaimConstants.Tid, account.HomeAccountId.TenantId),
new Claim(ClaimTypes.Upn, account.Username)
})
);
}

return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Http;
using Microsoft.Identity.Client;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Microsoft.Identity.Web.Client
{
public interface ITokenAcquisition
{
/// <summary>
/// In a Web App, adds, to the MSAL.NET cache, the account of the user authenticating to the Web App, when the authorization code is received (after the user
/// signed-in and consented)
/// An On-behalf-of token contained in the <see cref="AuthorizationCodeReceivedContext"/> is added to the cache, so that it can then be used to acquire another token on-behalf-of the
/// same user in order to call to downstream APIs.
/// </summary>
/// <param name="context">The context used when an 'AuthorizationCode' is received over the OpenIdConnect protocol.</param>
/// <example>
/// From the configuration of the Authentication of the ASP.NET Core Web API:
/// <code>OpenIdConnectOptions options;</code>
///
/// Subscribe to the authorization code recieved event:
/// <code>
/// options.Events = new OpenIdConnectEvents();
/// options.Events.OnAuthorizationCodeReceived = OnAuthorizationCodeReceived;
/// }
/// </code>
///
/// And then in the OnAuthorizationCodeRecieved method, call <see cref="AddAccountToCacheFromAuthorizationCode"/>:
/// <code>
/// private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedContext context)
/// {
/// var tokenAcquisition = context.HttpContext.RequestServices.GetRequiredService<ITokenAcquisition>();
/// await _tokenAcquisition.AddAccountToCacheFromAuthorizationCode(context, new string[] { "user.read" });
/// }
/// </code>
/// </example>
Task AddAccountToCacheFromAuthorizationCode(AuthorizationCodeReceivedContext context, IEnumerable<string> scopes);

/// <summary>
/// Typically used from an ASP.NET Core Web App or Web API controller, this method gets an access token
/// for a downstream API on behalf of the user account which claims are provided in the <see cref="HttpContext.User"/>
/// member of the <paramref name="context"/> parameter
/// </summary>
/// <param name="context">HttpContext associated with the Controller or auth operation</param>
/// <param name="scopes">Scopes to request for the downstream API to call</param>
/// <param name="tenantId">Enables to override the tenant/account for the same identity. This is useful in the
/// cases where a given account is guest in other tenants, and you want to acquire tokens for a specific tenant</param>
/// <returns>An access token to call on behalf of the user, the downstream API characterized by its scopes</returns>
Task<string> GetAccessTokenOnBehalfOfUser(HttpContext context, IEnumerable<string> scopes, string tenantId=null);

/// <summary>
/// In a Web API, adds to the MSAL.NET cache, the account of the user for which a bearer token was received when the Web API was called.
/// An access token and a refresh token are added to the cache, so that they can then be used to acquire another token on-behalf-of the
/// same user in order to call to downstream APIs.
/// </summary>
/// <param name="tokenValidationContext">Token validation context passed to the handler of the OnTokenValidated event
/// for the JwtBearer middleware</param>
/// <param name="scopes">[Optional] scopes to pre-request for a downstream API</param>
/// <example>
/// From the configuration of the Authentication of the ASP.NET Core Web API (for example in the Startup.cs file)
/// <code>JwtBearerOptions option;</code>
///
/// Subscribe to the token validated event:
/// <code>
/// options.Events = new JwtBearerEvents();
/// options.Events.OnTokenValidated = async context =>
/// {
/// var tokenAcquisition = context.HttpContext.RequestServices.GetRequiredService<ITokenAcquisition>();
/// tokenAcquisition.AddAccountToCacheFromJwt(context);
/// };
/// </code>
/// </example>
void AddAccountToCacheFromJwt(AspNetCore.Authentication.JwtBearer.TokenValidatedContext tokenValidationContext, IEnumerable<string> scopes = null);

/// <summary>
/// [not recommended] In a Web App, adds, to the MSAL.NET cache, the account of the user authenticating to the Web App.
/// An On-behalf-of token is added to the cache, so that it can then be used to acquire another token on-behalf-of the
/// same user in order for the Web App to call a Web APIs.
/// </summary>
/// <param name="tokenValidationContext">Token validation context passed to the handler of the OnTokenValidated event
/// for the OpenIdConnect middleware</param>
/// <param name="scopes">[Optional] scopes to pre-request for a downstream API</param>
/// <remarks>In a Web App, it's preferable to not request an access token, but only a code, and use the <see cref="AddAccountToCacheFromAuthorizationCode"/></remarks>
/// <example>
/// From the configuration of the Authentication of the ASP.NET Core Web API:
/// <code>OpenIdConnectOptions options;</code>
///
/// Subscribe to the token validated event:
/// <code>
/// options.Events.OnAuthorizationCodeReceived = OnTokenValidated;
/// </code>
///
/// And then in the OnTokenValidated method, call <see cref="AddAccountToCacheFromJwt(OpenIdConnect.TokenValidatedContext)"/>:
/// <code>
/// private async Task OnTokenValidated(TokenValidatedContext context)
/// {
/// var tokenAcquisition = context.HttpContext.RequestServices.GetRequiredService<ITokenAcquisition>();
/// _tokenAcquisition.AddAccountToCache(tokenValidationContext);
/// }
/// </code>
/// </example>
void AddAccountToCacheFromJwt(TokenValidatedContext tokenValidationContext, IEnumerable<string> scopes = null);

/// <summary>
/// Removes the account associated with context.HttpContext.User from the MSAL.NET cache
/// </summary>
/// <param name="context">RedirectContext passed-in to a <see cref="OnRedirectToIdentityProviderForSignOut"/>
/// Openidconnect event</param>
/// <returns></returns>
Task RemoveAccount(RedirectContext context);

/// <summary>
/// Used in Web APIs (which therefore cannot have an interaction with the user).
/// Replies to the client through the HttpReponse by sending a 403 (forbidden) and populating wwwAuthenticateHeaders so that
/// the client can trigger an iteraction with the user so that the user consents to more scopes
/// </summary>
/// <param name="httpContext">HttpContext</param>
/// <param name="scopes">Scopes to consent to</param>
/// <param name="msalSeviceException"><see cref="MsalUiRequiredException"/> triggering the challenge</param>
void ReplyForbiddenWithWwwAuthenticateHeader(HttpContext httpContext, IEnumerable<string> scopes, MsalUiRequiredException msalSeviceException);
}
}
Loading

0 comments on commit be3dc23

Please sign in to comment.