Skip to content

Instantly share code, notes, and snippets.

@rbrayb
Last active November 22, 2022 09:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rbrayb/945b985a5559288d3663556e8321502d to your computer and use it in GitHub Desktop.
Save rbrayb/945b985a5559288d3663556e8321502d to your computer and use it in GitHub Desktop.
Using Active Directory (AD) as the repository for authentication with identityserver4
// ...
/// <summary>
/// Handle postback from username/password login
/// </summary>
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginInputModel model, string button)
{
// check if we are in the context of an authorization request
var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);
// the user clicked the "cancel" button
if (button != "login")
{
if (context != null)
{
// if the user cancels, send a result back into IdentityServer as if they
// denied the consent (even if this client does not require consent).
// this will send back an access denied OIDC error response to the client.
await _interaction.GrantConsentAsync(context, ConsentResponse.Denied);
// we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
if (await _clientStore.IsPkceClientAsync(context.ClientId))
{
// if the client is PKCE then we assume it's native, so this change in how to
// return the response is for better UX for the end user.
return View("Redirect", new RedirectViewModel { RedirectUrl = model.ReturnUrl });
}
return Redirect(model.ReturnUrl);
}
else
{
// since we don't have a valid context, then we just go back to the home page
return Redirect("~/");
}
}
if (ModelState.IsValid)
{
// A useful overload is using (var principalContext = new PrincipalContext(ContextType.Domain, "YOUR_AD_DOMAIN", "CN=Users,DC=AwesomeDepartment,DC=com"))
//var adContext = new PrincipalContext(ContextType.Domain, "my-adfs", "OU=aaa,DC=bbb,DC=ccc,DC=com", ContextOptions.Negotiate);
// Access local DC
var adContext = new PrincipalContext(ContextType.Domain);
// validate username/password against in-memory store
// if (_users.ValidateCredentials(model.Username, model.Password))
if (adContext.ValidateCredentials(model.Username, model.Password))
{
// https://xcentium.com/blog/2019/05/07/active-directory-integration-with-a-net-application
// https://stackoverflow.com/questions/44392817/customizing-asp-net-identity
// Equivalent of var user = _users.FindByUsername(model.Username)
UserPrincipal uPrincipal = null;
uPrincipal = UserPrincipal.FindByIdentity(adContext, IdentityType.SamAccountName, model.Username);
// Use "TestUser" model for convenience so following code works
TestUser tUser = new TestUser();
// A better "SubjectId" would be userGUID
tUser.SubjectId = model.Username;
tUser.Username = model.Username;
var user = tUser;
await _events.RaiseAsync(new UserLoginSuccessEvent(user.Username, user.SubjectId, user.Username, clientId: context?.ClientId));
// only set explicit expiration here if user chooses "remember me".
// otherwise we rely upon expiration configured in cookie middleware.
AuthenticationProperties props = null;
if (AccountOptions.AllowRememberLogin && model.RememberLogin)
{
props = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)
};
};
// issue authentication cookie with subject ID and username
await HttpContext.SignInAsync(user.SubjectId, user.Username, props);
if (context != null)
{
if (await _clientStore.IsPkceClientAsync(context.ClientId))
{
// if the client is PKCE then we assume it's native, so this change in how to
// return the response is for better UX for the end user.
return View("Redirect", new RedirectViewModel { RedirectUrl = model.ReturnUrl });
}
// we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
return Redirect(model.ReturnUrl);
}
// request for a local page
if (Url.IsLocalUrl(model.ReturnUrl))
{
return Redirect(model.ReturnUrl);
}
else if (string.IsNullOrEmpty(model.ReturnUrl))
{
return Redirect("~/");
}
else
{
// user might have clicked on a malicious link - should be logged
throw new Exception("invalid return URL");
}
}
await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials", clientId:context?.ClientId));
ModelState.AddModelError(string.Empty, AccountOptions.InvalidCredentialsErrorMessage);
}
// something went wrong, show form with error
var vm = await BuildLoginViewModelAsync(model);
return View(vm);
}
// ...
using IdentityModel;
using IdentityServer4.Extensions;
using IdentityServer4.Models;
using IdentityServer4.Services;
using System.Collections.Generic;
using System.DirectoryServices;
using System.DirectoryServices.AccountManagement;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
namespace is4inmem
{
public class ADProfileService : IProfileService
{
UserPrincipal uPrincipal = null;
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var adContext = new PrincipalContext(ContextType.Domain);
//var user = Users.FindBySubjectId(context.Subject.GetName);
var user = context.Subject.GetDisplayName();
uPrincipal = UserPrincipal.FindByIdentity(adContext, IdentityType.SamAccountName, user);
var claims = new Claim[]
{
new Claim(JwtClaimTypes.Name, uPrincipal.Name),
new Claim(JwtClaimTypes.GivenName, uPrincipal.GivenName),
new Claim(JwtClaimTypes.FamilyName, uPrincipal.DisplayName),
new Claim(JwtClaimTypes.Email, uPrincipal.EmailAddress),
new Claim(JwtClaimTypes.Address, "123 Main Street"),
new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean)
};
//context.AddRequestedClaims(claims);
// To get another AD attribute not in "UserPrincipal" e.g. "Department"
string department = "";
if (uPrincipal.GetUnderlyingObjectType() == typeof(DirectoryEntry))
{
// Transition to directory entry to get other properties
using (var entry = (DirectoryEntry)uPrincipal.GetUnderlyingObject())
{
if (entry.Properties["department"] != null)
department = entry.Properties["department"].Value.ToString();
}
}
List<Claim> cl = new List<Claim>();
cl = claims.ToList();
// Add custom claims in token here based on user properties or any other source
cl.Add(new Claim ("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/department", department));
cl.Add(new Claim ("upn_custom", uPrincipal.UserPrincipalName));
context.IssuedClaims = cl;
return Task.CompletedTask;
}
public Task IsActiveAsync(IsActiveContext context)
{
var adContext = new PrincipalContext(ContextType.Domain);
var user = context.Subject;
Claim userClaim = user.Claims.FirstOrDefault(claimRecord => claimRecord.Type == "sub");
uPrincipal = UserPrincipal.FindByIdentity(adContext, IdentityType.SamAccountName, userClaim.Value);
// To be active, user must be enabled and not locked out
var isLocked = uPrincipal.IsAccountLockedOut();
context.IsActive = (bool)(uPrincipal.Enabled & !isLocked);
return Task.CompletedTask;
}
}
}
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
using IdentityModel;
using IdentityServer4.Models;
using System.Collections.Generic;
namespace is4inmem
{
public static class Config
{
public static IEnumerable<IdentityResource> Ids =>
new IdentityResource[]
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
new IdentityResources.Address(),
};
public static IEnumerable<ApiResource> Apis =>
new ApiResource[]
{
// new ApiResource("api1", "My API #1")
new ApiResource("api1", "My API", new[] { JwtClaimTypes.Subject, JwtClaimTypes.Email, JwtClaimTypes.Address, "upn_custom"})
};
public static IEnumerable<Client> Clients =>
new Client[]
{
// client credentials flow client
new Client
{
ClientId = "client",
ClientName = "Client Credentials Client",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets = { new Secret("511536EF-F270-4058-80CA-1C89C192F69A".Sha256()) },
AllowedScopes = { "api1" }
},
// MVC client using code flow + pkce
new Client
{
//ClientId = "mvc",
ClientId = "mvc.code",
ClientName = "MVC Client",
// Note
AlwaysIncludeUserClaimsInIdToken = true,
AllowedGrantTypes = GrantTypes.CodeAndClientCredentials,
//RequirePkce = true,
RequirePkce = false,
//ClientSecrets = { new Secret("49C1A7E1-0C79-4A89-A3D6-A37998FB86B0".Sha256()) },
ClientSecrets = { new Secret("secret".Sha256()) },
//RedirectUris = { "http://localhost:5003/signin-oidc" },
RedirectUris = { "https://localhost:44392/signin-oidc" },
FrontChannelLogoutUri = "http://localhost:5003/signout-oidc",
PostLogoutRedirectUris = { "http://localhost:5003/signout-callback-oidc" },
AllowOfflineAccess = true,
AllowedScopes = { "openid", "profile", "email", "address", "api1", "upn_custom" }
},
// SPA client using code flow + pkce
new Client
{
ClientId = "spa",
ClientName = "SPA Client",
ClientUri = "http://identityserver.io",
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true,
RequireClientSecret = false,
RedirectUris =
{
"http://localhost:5002/index.html",
"http://localhost:5002/callback.html",
"http://localhost:5002/silent.html",
"http://localhost:5002/popup.html",
},
PostLogoutRedirectUris = { "http://localhost:5002/index.html" },
AllowedCorsOrigins = { "http://localhost:5002" },
AllowedScopes = { "openid", "profile", "api1" }
}
};
}
}
// Update to add groups to JWT
// GetProfileDataAsync(ProfileDataRequestContext context) is now:
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var adContext = new PrincipalContext(ContextType.Domain);
//var user = Users.FindBySubjectId(context.Subject.GetName);
var user = context.Subject.GetDisplayName();
uPrincipal = UserPrincipal.FindByIdentity(adContext, IdentityType.SamAccountName, user);
var claims = new Claim[]
{
new Claim(JwtClaimTypes.Name, uPrincipal.Name),
new Claim(JwtClaimTypes.GivenName, uPrincipal.GivenName),
new Claim(JwtClaimTypes.FamilyName, uPrincipal.DisplayName),
new Claim(JwtClaimTypes.Email, uPrincipal.EmailAddress),
new Claim(JwtClaimTypes.Address, "123 Main Street"),
new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean)
};
//context.AddRequestedClaims(claims);
List<Claim> cl = new List<Claim>();
cl = claims.ToList();
// Get all groups user is a "memberOf"
PrincipalSearchResult<System.DirectoryServices.AccountManagement.Principal> oPrincipalSearchResult = null;
oPrincipalSearchResult = uPrincipal.GetGroups();
// Add groups as a claim type of "role"
foreach (System.DirectoryServices.AccountManagement.Principal oResult in oPrincipalSearchResult)
{
// Getting all groups causes JWT to be far too big so just using one as an example.
// To see if a user is a "memberOf" a group, use "uPrincipal.IsMemberOf"
if (oResult.Name == "Domain Users")
cl.Add(new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role", oResult.Name));
}
// To get another AD attribute not in "UserPrincipal" e.g. "Department"
string department = "";
if (uPrincipal.GetUnderlyingObjectType() == typeof(DirectoryEntry))
{
// Transition to directory entry to get other properties
using (var entry = (DirectoryEntry)uPrincipal.GetUnderlyingObject())
{
if (entry.Properties["department"] != null)
department = entry.Properties["department"].Value.ToString();
}
}
// Add custom claims in token here based on user properties or any other source
cl.Add(new Claim ("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/department", department));
cl.Add(new Claim ("upn_custom", uPrincipal.UserPrincipalName));
context.IssuedClaims = cl;
return Task.CompletedTask;
}
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
using IdentityServer4;
using IdentityServer4.Quickstart.UI;
using IdentityServer4.Services;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace is4inmem
{
public class Startup
{
public IWebHostEnvironment Environment { get; }
public IConfiguration Configuration { get; }
public Startup(IWebHostEnvironment environment, IConfiguration configuration)
{
Environment = environment;
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
// configures IIS out-of-proc settings (see https://github.com/aspnet/AspNetCore/issues/14882)
services.Configure<IISOptions>(iis =>
{
iis.AuthenticationDisplayName = "Windows";
iis.AutomaticAuthentication = false;
});
// configures IIS in-proc settings
services.Configure<IISServerOptions>(iis =>
{
iis.AuthenticationDisplayName = "Windows";
iis.AutomaticAuthentication = false;
});
var builder = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
});
//.AddTestUsers(TestUsers.Users);
// in-memory, code config
builder.AddInMemoryIdentityResources(Config.Ids);
builder.AddInMemoryApiResources(Config.Apis);
builder.AddInMemoryClients(Config.Clients);
services.AddScoped<IProfileService, ADProfileService>();
// or in-memory, json config
//builder.AddInMemoryIdentityResources(Configuration.GetSection("IdentityResources"));
//builder.AddInMemoryApiResources(Configuration.GetSection("ApiResources"));
//builder.AddInMemoryClients(Configuration.GetSection("clients"));
// not recommended for production - you need to store your key material somewhere secure
builder.AddDeveloperSigningCredential();
services.AddAuthentication()
.AddGoogle(options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
// register your IdentityServer with Google at https://console.developers.google.com
// enable the Google+ API
// set the redirect URI to http://localhost:5000/signin-google
options.ClientId = "copy client ID from Google here";
options.ClientSecret = "copy client secret from Google here";
});
}
public void Configure(IApplicationBuilder app)
{
if (Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseRouting();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
});
}
}
}
@flawesomesoft
Copy link

flawesomesoft commented Nov 22, 2022

var claims = new Claim[]
{
    //...
}

List<Claim> cl = new List<Claim>();
cl = claims.ToList();

why not just creating a list in first place?

var claims = new List<Claim>()
{
    // ...
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment