This repository has been archived by the owner on Jun 30, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 529
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Updated LinkedAccounts with Microsoft.Identity.Web package that conta…
…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
1 parent
05303f7
commit be3dc23
Showing
36 changed files
with
3,537 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
39 changes: 39 additions & 0 deletions
39
solutions/linkedaccounts/Microsoft.Identity.Web/ClaimConstants.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; | ||
} | ||
} |
183 changes: 183 additions & 0 deletions
183
solutions/linkedaccounts/Microsoft.Identity.Web/ClaimPrincipalExtension.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
123 changes: 123 additions & 0 deletions
123
solutions/linkedaccounts/Microsoft.Identity.Web/Client/ITokenAcquisition.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.