Last active
April 1, 2025 22:51
-
-
Save sadukie/d492626c0dd15f21e2bc8c1f950b0b92 to your computer and use it in GitHub Desktop.
ASP-NET-Core-Identity-in-Action-Implementing-Individual-Accounts
This file contains hidden or 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
<div class="col-md-6 col-md-offset-2"> | |
<section> | |
<h3>Use another service to log in.</h3> | |
<hr /> | |
@{ | |
if ((Model.ExternalLogins?.Count ?? 0) == 0) | |
{ | |
<div> | |
<p> | |
There are no external authentication services configured. See this <a href="https://go.microsoft.com/fwlink/?LinkID=532715">article | |
about setting up this ASP.NET application to support logging in via external services</a>. | |
</p> | |
</div> | |
} | |
else | |
{ | |
<form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal"> | |
<div> | |
<p> | |
@foreach (var provider in Model.ExternalLogins!) | |
{ | |
<button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button> | |
} | |
</p> | |
</div> | |
</form> | |
} | |
} | |
</section> | |
</div> |
This file contains hidden or 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
var gitHubClientId = builder.Configuration["GitHub:ClientId"] ?? string.Empty; | |
if (!string.IsNullOrEmpty(gitHubClientId)) | |
{ | |
builder.Services.AddAuthentication() | |
.AddOAuth("GitHub", "GitHub", options => | |
{ | |
options.ClientId = gitHubClientId; | |
options.ClientSecret = builder.Configuration["GitHub:ClientSecret"] ?? string.Empty; | |
options.CallbackPath = "/signin-github"; | |
options.AuthorizationEndpoint = "https://github.com/login/oauth/authorize"; | |
options.TokenEndpoint = "https://github.com/login/oauth/access_token"; | |
options.UserInformationEndpoint = "https://api.github.com/user"; | |
options.UsePkce = false; // PKCE not supported by GitHub | |
options.SaveTokens = true; | |
options.ClaimsIssuer = "GitHub"; | |
options.Events = new Microsoft.AspNetCore.Authentication.OAuth.OAuthEvents | |
{ | |
OnCreatingTicket = GitHubClaimsHelper.OnOAuthCreatingTicket | |
}; | |
}); | |
} |
This file contains hidden or 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
builder.Services.AddAuthentication() | |
.AddOAuth("GitHub", "GitHub", options => | |
{ | |
options.ClientId = builder.Configuration["GitHub:ClientId"] ?? string.Empty; | |
options.ClientSecret = builder.Configuration["GitHub:ClientSecret"] ?? string.Empty; | |
options.CallbackPath = builder.Configuration["GitHub:CallbackURL"] ?? string.Empty; | |
options.AuthorizationEndpoint = "https://github.com/login/oauth/authorize"; | |
options.TokenEndpoint = "https://github.com/login/oauth/access_token"; | |
options.UserInformationEndpoint = "https://api.github.com/user"; | |
options.UsePkce = false; // PKCE not supported by GitHub | |
options.SaveTokens = true; | |
options.ClaimsIssuer = "GitHub"; | |
options.Events = new Microsoft.AspNetCore.Authentication.OAuth.OAuthEvents | |
{ | |
OnCreatingTicket = GitHubClaimsHelper.OnOAuthCreatingTicket | |
}; | |
}); |
This file contains hidden or 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
services.AddAuthorization(options => | |
{ | |
options.AddPolicy("ListEditor", policy => | |
policy.RequireClaim("CanEditWishlists", "true")); | |
}); |
This file contains hidden or 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
builder.Services.AddAuthorization(options => | |
{ | |
options.AddPolicy("GitHubUser", policy => | |
{ | |
policy.RequireClaim("urn:github:name"); | |
}); | |
}); |
This file contains hidden or 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
public static string GetProductManagerUserToken() | |
{ | |
string userName = "[email protected]"; | |
string[] roles = { "Product Managers" }; | |
return CreateToken(userName, roles); | |
} |
This file contains hidden or 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
[TestMethod] | |
public async Task ReturnsForbiddenGivenAdminUserToken() | |
{ | |
var jsonContent = GetValidNewItemJson(); | |
var token = ApiTokenHelper.GetAdminUserToken(); | |
var client = ProgramTest.NewClient; | |
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); | |
var response = await client.PostAsync("api/catalog-items", jsonContent); | |
Assert.AreEqual(HttpStatusCode.Forbidden, response.StatusCode); | |
} |
This file contains hidden or 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
@section Scripts { | |
<partial name="_ValidationScriptsPartial" /> | |
<script type="text/javascript" src="~/lib/qrcode.min.js"></script> | |
<script type="text/javascript" src="~/js/generate-qr.js"></script> | |
} |
This file contains hidden or 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
var productManager = new ApplicationUser { UserName = "[email protected]", Email = "[email protected]" }; | |
await userManager.CreateAsync(productManager, AuthorizationConstants.DEFAULT_PASSWORD); | |
productManager = await userManager.FindByNameAsync(productManager.UserName); | |
if (productManager != null) | |
{ | |
await userManager.AddToRoleAsync(productManager, BlazorShared.Authorization.Constants.Roles.PRODUCT_MANAGERS); | |
} |
This file contains hidden or 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
using System.Net.Http.Headers; | |
using System.Security.Claims; | |
using Ardalis.GuardClauses; | |
using Microsoft.AspNetCore.Authentication; | |
using Microsoft.AspNetCore.Authentication.OAuth; | |
using Newtonsoft.Json.Linq; | |
namespace Microsoft.eShopWeb.Web.Areas.Identity.Helpers; | |
public class GitHubClaimsHelper | |
{ | |
public static async Task OnOAuthCreatingTicket(OAuthCreatingTicketContext context) | |
{ | |
// No JWT coming back from GitHub | |
// Need to call the UserInformationEndpoint manually | |
// And then build the claims from there. | |
Guard.Against.Null(context); | |
Guard.Against.Null(context.Identity); | |
if (context.Identity.IsAuthenticated) | |
{ | |
// Store the tokens | |
var tokens = context.Properties.GetTokens().ToList(); | |
context.Properties.StoreTokens(tokens); | |
// Get the GitHub user | |
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint); | |
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken); | |
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); | |
var response = await context.Backchannel.SendAsync(request, context.HttpContext.RequestAborted); | |
response.EnsureSuccessStatusCode(); | |
var user = JObject.Parse(await response.Content.ReadAsStringAsync()); | |
AddClaims(context, user); | |
} | |
} | |
private static void AddClaims(OAuthCreatingTicketContext context, JObject user) | |
{ | |
Guard.Against.Null(context.Identity); | |
var identifier = user.Value<string>("id"); | |
if (!string.IsNullOrEmpty(identifier)) | |
{ | |
context.Identity.AddClaim(new Claim( | |
ClaimTypes.NameIdentifier, identifier, | |
ClaimValueTypes.String, context.Options.ClaimsIssuer)); | |
} | |
var userName = user.Value<string>("login"); | |
if (!string.IsNullOrEmpty(userName)) | |
{ | |
context.Identity.AddClaim(new Claim( | |
ClaimsIdentity.DefaultNameClaimType, userName, | |
ClaimValueTypes.String, context.Options.ClaimsIssuer)); | |
} | |
var name = user.Value<string>("name"); | |
if (!string.IsNullOrEmpty(name)) | |
{ | |
context.Identity.AddClaim(new Claim( | |
"urn:github:name", name, | |
ClaimValueTypes.String, context.Options.ClaimsIssuer)); | |
} | |
var link = user.Value<string>("url"); | |
if (!string.IsNullOrEmpty(link)) | |
{ | |
context.Identity.AddClaim(new Claim( | |
"urn:github:url", link, | |
ClaimValueTypes.String, context.Options.ClaimsIssuer)); | |
} | |
} | |
} |
This file contains hidden or 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
@if (User.IsInRole(BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS) | |
|| User.IsInRole(BlazorShared.Authorization.Constants.Roles.PRODUCT_MANAGERS)) | |
{ | |
<a class="esh-identity-item" | |
asp-page="/Admin/Index"> | |
<div class="esh-identity-name esh-identity-name--upper">Admin</div> | |
</a> | |
} |
This file contains hidden or 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
using System.Security.Claims; | |
using Ardalis.GuardClauses; | |
using Microsoft.AspNetCore.Identity; | |
using Microsoft.eShopWeb.Infrastructure.Identity; | |
namespace Microsoft.eShopWeb.Web.Extensions; | |
public static class UserManagerExtensions | |
{ | |
public static async Task<bool> SaveClaimsAsync(this UserManager<ApplicationUser> userManager, Dictionary<string, string> claimsToSync, ExternalLoginInfo info, ApplicationUser user) | |
{ | |
Guard.Against.Null(user); | |
Guard.Against.Null(info); | |
Guard.Against.Null(info.Principal); | |
var refreshSignIn = false; | |
var userClaims = await userManager.GetClaimsAsync(user); | |
foreach (var addedClaim in claimsToSync) | |
{ | |
var userClaim = userClaims | |
.FirstOrDefault(c => c.Type == addedClaim.Key); | |
var externalClaim = info.Principal.FindFirst(addedClaim.Key); | |
Guard.Against.Null(externalClaim); | |
if (userClaim is null) | |
{ | |
await userManager.AddClaimAsync(user, | |
new Claim(addedClaim.Key, externalClaim.Value)); | |
refreshSignIn = true; | |
} | |
else if (userClaim.Value != externalClaim.Value) | |
{ | |
await userManager | |
.ReplaceClaimAsync(user, userClaim, externalClaim); | |
refreshSignIn = true; | |
} | |
} | |
return refreshSignIn; | |
} | |
} |
This file contains hidden or 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
window.addEventListener("load", () => { | |
const uri = document.getElementById("qrCodeData").getAttribute('data-url'); | |
new QRCode(document.getElementById("qrCode"), | |
{ | |
text: uri, | |
width: 150, | |
height: 150 | |
}); | |
}); |
This file contains hidden or 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
using Microsoft.AspNetCore.Authentication; | |
using Microsoft.AspNetCore.Authentication.OAuth; | |
using Microsoft.AspNetCore.Identity; | |
using Newtonsoft.Json.Linq; | |
using System.Net.Http.Headers; | |
using System.Security.Claims; | |
namespace IndividualAccountsRazorDemo.Areas.Identity.Helpers; | |
public class GitHubClaimsHelper | |
{ | |
public static async Task OnOAuthCreatingTicket(OAuthCreatingTicketContext context) | |
{ | |
// No JWT coming back from GitHub | |
// Need to call the UserInformationEndpoint manually | |
// And then build the claims from there. | |
if (context.Identity != null && context.Identity.IsAuthenticated) | |
{ | |
// Store the tokens | |
List<AuthenticationToken> tokens = context.Properties.GetTokens().ToList(); | |
context.Properties.StoreTokens(tokens); | |
// Get the GitHub user | |
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint); | |
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken); | |
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); | |
var response = await context.Backchannel.SendAsync(request, context.HttpContext.RequestAborted); | |
response.EnsureSuccessStatusCode(); | |
var user = JObject.Parse(await response.Content.ReadAsStringAsync()); | |
AddClaims(context, user); | |
} | |
} | |
private static void AddClaims(OAuthCreatingTicketContext context, JObject user) | |
{ | |
if (context is null) | |
{ | |
return; | |
} | |
if (context.Identity != null) | |
{ | |
var identifier = user.Value<string>("id"); | |
if (!string.IsNullOrEmpty(identifier)) | |
{ | |
context.Identity.AddClaim(new Claim( | |
ClaimTypes.NameIdentifier, identifier, | |
ClaimValueTypes.String, context.Options.ClaimsIssuer)); | |
} | |
var userName = user.Value<string>("login"); | |
if (!string.IsNullOrEmpty(userName)) | |
{ | |
context.Identity.AddClaim(new Claim( | |
ClaimsIdentity.DefaultNameClaimType, userName, | |
ClaimValueTypes.String, context.Options.ClaimsIssuer)); | |
} | |
var name = user.Value<string>("name"); | |
if (!string.IsNullOrEmpty(name)) | |
{ | |
context.Identity.AddClaim(new Claim( | |
"urn:github:name", name, | |
ClaimValueTypes.String, context.Options.ClaimsIssuer)); | |
} | |
var link = user.Value<string>("url"); | |
if (!string.IsNullOrEmpty(link)) | |
{ | |
context.Identity.AddClaim(new Claim( | |
"urn:github:url", link, | |
ClaimValueTypes.String, context.Options.ClaimsIssuer)); | |
} | |
} | |
} | |
} |
This file contains hidden or 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
using Microsoft.AspNetCore.Identity.UI.Services; | |
namespace IndividualAccountsRazorDemo.Infrastructure; | |
public class LogEmailSender : IEmailSender | |
{ | |
private ILogger _logger; | |
public LogEmailSender(ILogger<LogEmailSender> logger) | |
{ | |
_logger = logger; | |
} | |
public Task SendEmailAsync(string email, string subject, string htmlMessage) | |
{ | |
_logger.LogInformation($"Sending email to {email}\nSubject:{subject}\nMessage:{htmlMessage}"); | |
return Task.CompletedTask; | |
} | |
} |
This file contains hidden or 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
[HttpGet] | |
public async Task<IActionResult> LinkLoginCallback() | |
{ | |
var user = await _userManager.GetUserAsync(User); | |
if (user == null) | |
{ | |
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); | |
} | |
var info = await _signInManager.GetExternalLoginInfoAsync(user.Id); | |
if (info == null) | |
{ | |
throw new ApplicationException($"Unexpected error occurred loading external login info for user with ID '{user.Id}'."); | |
} | |
var result = await _userManager.AddLoginAsync(user, info); | |
if (!result.Succeeded) | |
{ | |
throw new ApplicationException($"Unexpected error occurred adding external login for user with ID '{user.Id}'."); | |
} | |
if (info.Principal.Claims.Any()) | |
{ | |
var claimsToSync = new Dictionary<string, string>(); | |
foreach (var claim in info.Principal.Claims) | |
{ | |
claimsToSync.Add(claim.Type, claim.Value); | |
} | |
var refreshSignIn = await _userManager.SaveClaimsAsync(claimsToSync, info, user); | |
if (refreshSignIn) | |
{ | |
await _signInManager.RefreshSignInAsync(user); | |
} | |
} | |
StatusMessage = "The external login was added."; | |
return RedirectToAction(nameof(ExternalLogins)); | |
} |
This file contains hidden or 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
// Build your policies | |
builder.Services.AddAuthorization(options => | |
{ | |
options.AddPolicy("GitHubUser", policy => | |
{ | |
policy.RequireClaim("urn:github:name"); | |
}); | |
options.AddPolicy("DevCheck", policy => | |
{ | |
policy.RequireAssertion(context => context.User.HasClaim(claim => claim.Type == "urn:github:name") | |
|| context.User.IsInRole("Developer") | |
); | |
}); | |
}); |
This file contains hidden or 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
options.AddPolicy("DevCheck", policy => | |
{ | |
policy.RequireAssertion(context => context.User.HasClaim(claim => claim.Type == "urn:github:name") | |
|| context.User.IsInRole("Developer") | |
); | |
}); |
This file contains hidden or 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
if (result.Succeeded) | |
{ | |
if (info.Principal.Claims.Any()) | |
{ | |
var claimsToSync = new Dictionary<string, string>(); | |
foreach (var claim in info.Principal.Claims) | |
{ | |
claimsToSync.Add(claim.Type, claim.Value); | |
} | |
var refreshSignIn = await _userManager.SaveClaimsAsync(claimsToSync, info, user); | |
if (refreshSignIn) | |
{ | |
await _signInManager.RefreshSignInAsync(user); | |
} | |
} | |
} |
This file contains hidden or 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
builder.Services.AddDefaultIdentity<IdentityUser>(options => | |
{ | |
options.Password.RequireDigit = false; | |
options.Password.RequireNonAlphanumeric = true; | |
options.Password.RequireUppercase = false; | |
options.Password.RequiredLength = 10; | |
options.Password.RequiredUniqueChars = 4; | |
} | |
) | |
.AddEntityFrameworkStores<ApplicationDbContext>(); |
This file contains hidden or 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
private string GenerateQrCodeUri(string email, string unformattedKey) | |
{ | |
return string.Format( | |
CultureInfo.InvariantCulture, | |
AuthenticatorUriFormat, | |
_urlEncoder.Encode("ASP.NET Identity 2FA Demo"), | |
_urlEncoder.Encode(email), | |
unformattedKey); | |
} |
This file contains hidden or 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
using Microsoft.AspNetCore.Identity; | |
using System.Security.Claims; | |
public static class UserManagerExtensions | |
{ | |
public static async Task<bool> SaveClaimsAsync(this UserManager<IdentityUser> userManager, Dictionary<string, string> claimsToSync, ExternalLoginInfo info, IdentityUser user) | |
{ | |
bool refreshSignIn = false; | |
var userClaims = await userManager.GetClaimsAsync(user); | |
foreach (var addedClaim in claimsToSync) | |
{ | |
var userClaim = userClaims | |
.FirstOrDefault(c => c.Type == addedClaim.Key); | |
var externalClaim = info.Principal.FindFirst(addedClaim.Key); | |
if (userClaim == null) | |
{ | |
await userManager.AddClaimAsync(user, | |
new Claim(addedClaim.Key, externalClaim.Value)); | |
refreshSignIn = true; | |
} | |
else if (userClaim.Value != externalClaim.Value) | |
{ | |
await userManager | |
.ReplaceClaimAsync(user, userClaim, externalClaim); | |
refreshSignIn = true; | |
} | |
} | |
return refreshSignIn; | |
} | |
} |
Comments are disabled for this gist.