Skip to content

Instantly share code, notes, and snippets.

@sadukie
Last active April 1, 2025 22:51
Show Gist options
  • Save sadukie/d492626c0dd15f21e2bc8c1f950b0b92 to your computer and use it in GitHub Desktop.
Save sadukie/d492626c0dd15f21e2bc8c1f950b0b92 to your computer and use it in GitHub Desktop.
ASP-NET-Core-Identity-in-Action-Implementing-Individual-Accounts
<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>
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
};
});
}
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
};
});
services.AddAuthorization(options =>
{
options.AddPolicy("ListEditor", policy =>
policy.RequireClaim("CanEditWishlists", "true"));
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("GitHubUser", policy =>
{
policy.RequireClaim("urn:github:name");
});
});
public static string GetProductManagerUserToken()
{
string userName = "[email protected]";
string[] roles = { "Product Managers" };
return CreateToken(userName, roles);
}
builder.Services.AddRazorPages(options =>
{
options.Conventions.AllowAnonymousToPage("/Error");
options.Conventions.AuthorizeAreaFolder("Identity", "/Manage","DevCheck");
});
[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);
}
@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>
}
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);
}
namespace BlazorShared.Authorization;
public static class Constants
{
public static class Roles
{
public const string ADMINISTRATORS = "Administrators";
public const string PRODUCT_MANAGERS = "Product Managers";
}
public static class RoleCombinations
{
public const string ADMIN_PORTAL_ROLES = $"{Roles.ADMINISTRATORS},{Roles.PRODUCT_MANAGERS}";
}
}
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));
}
}
}
@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>
}
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;
}
}
window.addEventListener("load", () => {
const uri = document.getElementById("qrCodeData").getAttribute('data-url');
new QRCode(document.getElementById("qrCode"),
{
text: uri,
width: 150,
height: 150
});
});
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));
}
}
}
}
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;
}
}
[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));
}
// 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")
);
});
});
options.AddPolicy("DevCheck", policy =>
{
policy.RequireAssertion(context => context.User.HasClaim(claim => claim.Type == "urn:github:name")
|| context.User.IsInRole("Developer")
);
});
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);
}
}
}
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>();
private string GenerateQrCodeUri(string email, string unformattedKey)
{
return string.Format(
CultureInfo.InvariantCulture,
AuthenticatorUriFormat,
_urlEncoder.Encode("ASP.NET Identity 2FA Demo"),
_urlEncoder.Encode(email),
unformattedKey);
}
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.