My web application uses Active Directory Authentication when a user logs in. I use the following code for audit columns. It works perfectly fine for the CreatedAt and ModifiedAt dates but the currentUsername is hardcoded.
public override int SaveChanges()
{
var entities = ChangeTracker.Entries().Where(x => x.Entity is BaseClass && (x.State == EntityState.Added || x.State == EntityState.Modified));
var currentUsername = "T";
foreach (var entity in entities)
{
if (entity.State == EntityState.Added)
{
((BaseClass)entity.Entity).CreatedAt = DateTime.Now;
((BaseClass)entity.Entity).CreatedBy = currentUsername;
}
((BaseClass)entity.Entity).ModifiedAt = DateTime.Now;
((BaseClass)entity.Entity).ModifiedBy = currentUsername;
}
return base.SaveChanges();
}
How can I get the current username logged in Active Directory?
If you're on .NET 4.5 or higher, just use the System.DirectoryServices.AccountManagement namespace and the UserPrincipal class in that context:
// you'll need to add a reference to this .NET assembly in your project
// so that you can use this namespace
using System.DirectoryServices.AccountManagement;
public string GetLoggedInUser()
{
// establish the PrincipalContext - this will grab the default domain, default containers
using (PrincipalContext ctx = new PrincipalContext(ContextType.Domain))
{
// get the currently active user
UserPrincipal currentUser = UserPrincipal.Current;
if (currentUser != null)
{
// this will return "first name last name" separated by a space,
// e.g. "John Doe" or "Jane Tarzan"
return $"{currentUser.GivenName} {currentUser.Surname}";
}
}
return string.Empty;
}
I have class UserFactory where I want to check if User already exist in database, if not it will be automatically created. But having difficulties with accessing DbContext from outside the Controller.
public UserFactory(DbContextOptionsBuilder builder)
{
_dbContext = new PMSContext(builder.Options) ;
}
public User Create(WindowsIdentity currentWindowsUser)
{
User user = new User();
string name = currentWindowsUser.Name.Replace("DOMAIN\\", "");
PrincipalContext ctx = new PrincipalContext(ContextType.Domain);
UserPrincipal ADuser = UserPrincipal.FindByIdentity(ctx, name);
if(ADuser != null)
{
User userInDatabase = _dbContext.Users.Where(u => u.SamAccountName == name).FirstOrDefault();
}
}
And than in the Startup.cs I habe :
public Startup(IHostingEnvironment env)
{
var builder = new DbContextOptionsBuilder<PMSContext>()
.UseSqlServer(Configuration["Data:DefaultConnection:ConnectionString"]);
User user = new UserFactory(builder).Create(WindowsIdentity.GetCurrent());
}
But I'm getting error:
Severity Code Description Project File Line
Error CS1503 Argument 1: cannot convert from 'Microsoft.Data.Entity.Infrastructure.SqlServerDbContextOptionsBuilder' to 'Microsoft.Data.Entity.DbContextOptionsBuilder'
Is this the right way to access DbContext from the outside of the Controller?
The answer was quite simple:
var optionsBuilder = new DbContextOptionsBuilder();
optionsBuilder.UseSqlServer(Configuration["Data:DefaultConnection:ConnectionString"]);
var context = new PMSContext(optionsBuilder.Options);
User user = new UserFactory(context).Create(WindowsIdentity.GetCurrent());
Context:
I am using ASP.NET MVC with OWIN self host. Below are the rest of the configs/setup.
In my Clients in identity server (notice the AllowedScopes set):
public static class InMemoryClientSource
{
public static List<Client> GetClientList()
{
return new List<Client>()
{
new Client()
{
ClientName = "Admin website",
ClientId = "admin",
Enabled = true,
Flow = Flows.Hybrid,
ClientSecrets = new List<Secret>()
{
new Secret("admin".Sha256())
},
RedirectUris = new List<string>()
{
"https://admin.localhost.com/"
},
PostLogoutRedirectUris = new List<string>()
{
"https://admin.localhost.com/"
},
AllowedScopes = new List<string> {
Constants.StandardScopes.OpenId,
Constants.StandardScopes.Profile,
Constants.StandardScopes.Email,
Constants.StandardScopes.Roles
}
}
};
}
}
Here are the Scopes:
public static class InMemoryScopeSource
{
public static List<Scope> GetScopeList()
{
var scopes = new List<Scope>();
scopes.Add(StandardScopes.OpenId);
scopes.Add(StandardScopes.Profile);
scopes.Add(StandardScopes.Email);
scopes.Add(StandardScopes.Roles);
return scopes.ToList();
}
}
In the Identity Server, here's how the server is configured. (Notice the Clients and Scopes are the ones provided above) :
var userService = new UsersService( .... repository passed here .... );
var factory = new IdentityServerServiceFactory()
.UseInMemoryClients(InMemoryClientSource.GetClientList())
.UseInMemoryScopes(InMemoryScopeSource.GetScopeList());
factory.UserService = new Registration<IUserService>(resolver => userService);
var options = new IdentityServerOptions()
{
Factory = factory,
SigningCertificate = Certificates.Load(), // certificates blah blah
SiteName = "Identity"
};
app.UseIdentityServer(options);
Finally, on the client web application side, this is how auth is set up:
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationType = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions()
{
Authority = "https://id.localhost.com",
ClientId = "admin",
RedirectUri = "https://admin.localhost.com/",
PostLogoutRedirectUri = "https://admin.localhost.com/",
ResponseType = "code id_token token",
Scope = "openid profile email roles",
ClientSecret = "admin",
SignInAsAuthenticationType = "Cookies"
});
I have implemented a custom class for IUserService:
public class UsersService : UserServiceBase
{
public UsersService( .... repository passed here .... )
{
//.... ctor stuff
}
public override Task AuthenticateLocalAsync(LocalAuthenticationContext context)
{
// var user = .... retrieved from database .....
// ... auth logic ...
if (isAuthenticated)
{
var claims = new List<Claim>();
claims.Add(new Claim(Constants.ClaimTypes.GivenName, user.FirstName));
claims.Add(new Claim(Constants.ClaimTypes.FamilyName, user.LastName));
claims.Add(new Claim(Constants.ClaimTypes.Email, user.EmailAddress));
context.AuthenticateResult = new AuthenticateResult(user.Id.ToString(), user.EmailAddress, claims);
}
return Task.FromResult(0);
}
}
As you see, the claims are passed in this line:
context.AuthenticateResult = new AuthenticateResult(user.Id.ToString(), user.EmailAddress, claims);
When I try logging in to IdentityServer3, I can log in successfully to the client web application. HOWEVER, when I get the user claims, I don't see any identity claims. No given_name, family_name, and email claims. Screenshot below:
Anything I might have missed? Thanks in advance!
My solution was to add a list of claims to my scope configuration in order to return those claims. The wiki's documentation here described it.
For an in-memory client all I did was something like this:
public class Scopes
{
public static IEnumerable<Scope> Get()
{
return new Scope[]
{
StandardScopes.OpenId,
StandardScopes.Profile,
StandardScopes.Email,
StandardScopes.Roles,
StandardScopes.OfflineAccess,
new Scope
{
Name = "yourScopeNameHere",
DisplayName = "A Nice Display Name",
Type = ScopeType.Identity,
Emphasize = false,
Claims = new List<ScopeClaim>
{
new ScopeClaim("yourClaimNameHere", true),
new ScopeClaim("anotherClaimNameHere", true)
}
}
};
}
}
Finally found the solution for this problem.
First, I moved the creation of claims to the overridden GetProfileDataAsync (in my UserService class). Here's my implementation of it:
public override Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var identity = new ClaimsIdentity();
UserInfo user = null;
if (!string.IsNullOrEmpty(context.Subject.Identity.Name))
user = _facade.Get(context.Subject.Identity.Name);
else
{
// get the sub claim
var claim = context.Subject.FindFirst(item => item.Type == "sub");
if (claim != null)
{
Guid userId = new Guid(claim.Value);
user = _facade.Get(userId);
}
}
if (user != null)
{
identity.AddClaims(new[]
{
new Claim(Constants.ClaimTypes.PreferredUserName, user.Username),
new Claim(Constants.ClaimTypes.Email, user.EmailAddress)
// .. other claims
});
}
context.IssuedClaims = identity.Claims; //<- MAKE SURE you add the claims here
return Task.FromResult(identity.Claims);
}
Make sure that we pass the claims to the "context.IssueClaims" inside the GetProfileDataAsync() before returning the task.
And for those interested on how my AuthenticateLocalAsync() looks like:
var user = _facade.Get(context.UserName);
if (user == null)
return Task.FromResult(0);
var isPasswordCorrect = BCrypt.Net.BCrypt.Verify(context.Password, user.Password);
if (isPasswordCorrect)
{
context.AuthenticateResult = new AuthenticateResult(user.Id.ToString(), user.Username);
}
return Task.FromResult(0);
I raised a similar issue in IdentityServer3 GitHub project page that contains the explanation on why I encountered my issue. Here's the link:
https://github.com/IdentityServer/IdentityServer3/issues/1938
I am not using the identity server, however I am using the Windows Identity Foundation, which I believe is what IdentityServer uses. In order to access the claims I use:
((ClaimsIdentity)User.Identity).Claims
In asp.net webform application, trying to save user to AspNetUsers after UPDATE-DATABASE command. the following code doesnt do that. solution ?
public Configuration()
{
AutomaticMigrationsEnabled = true;
}
protected override void Seed(MyApp.Models.ApplicationDbContext context)
{
if (!context.Users.Any(u => u.Email == "some#mail"))
{
var store = new UserStore<ApplicationUser>(context);
var manager = new UserManager<ApplicationUser>(store);
var user = new ApplicationUser { Email = "some#mail" };
manager.Create(user, "password");
}
}
In order to add ApplicationUser you must have the property Username initialized.
I have a Web Api project.
I have implemented a custom Authentication Attribute like so:
public class TokenAuthenticationAttribute : System.Web.Http.Filters.ActionFilterAttribute
{
public override void OnActionExecuting(System.Web.Http.Controllers.HttpActionContext actionContext)
{
// In auth web method you should implement functionality of authentication
// so that client app could be able to get token
if (actionContext.Request.RequestUri.AbsolutePath.Contains("api/auth/login"))
{
return;
}
// Receive token from the client. Here is the example when token is in header:
var token = HttpContext.Current.Request.Headers["Token"];
// Put your secret key into the configuration
var secretKey = ConfigurationManager.AppSettings["JWTSecurityKey"];
try
{
string jsonPayload = JWT.JsonWebToken.Decode(token, secretKey);
int separatorIndex = jsonPayload.IndexOf(';');
string userId = "";
DateTime timeIssued = DateTime.MinValue;
if (separatorIndex >= 0)
{
//userId = UTF8Encoding.UTF8.GetString(Convert.FromBase64String(jsonPayload.Substring(0, separatorIndex)));
userId = jsonPayload.Substring(0, separatorIndex);
timeIssued = DateTime.Parse(jsonPayload.Substring(separatorIndex + 1));
}
short TokenTTL = 10;
//try{
//Int16.TryParse(ConfigurationManager.AppSettings["TokenTTL"],TokenTTL);
//}catch(Exception e){ //}
if ((DateTime.Now.Subtract(timeIssued).TotalMinutes >= TokenTTL))
{
throw new HttpResponseException(HttpStatusCode.Forbidden);
}
//Save user in context
var claims = new List<Claim>()
{
new Claim(ClaimTypes.Name, userId)
};
var id = new ClaimsIdentity(claims, "Basic");
var principal = new ClaimsPrincipal(new[] { id });
actionContext.Request.GetRequestContext().Principal = principal;
}
catch (JWT.SignatureVerificationException)
{
throw new HttpResponseException(HttpStatusCode.Unauthorized);
}
}
}
Now how do I get hold of that user in my actionmethod?
[BasicHttpAuthorizeAttribute]
[httpGet]
public void Login()
{
// how do i get user here
}
/////// Save the string username to the context so that I can acess
it in the controler.
var claims = new List<Claim>()
{
new Claim(ClaimTypes.Name, "john")
};
var id = new ClaimsIdentity(claims, "Basic");
var principal = new ClaimsPrincipal(new[] { id });
actionContext.Request.GetRequestContext().Principal = principal;
// how do i get user here
var name = User.Identity.Name;
BTW, use an authentication filter instead of an authorization filter to perform authentication. See my blog post - http://lbadri.wordpress.com/2014/02/13/basic-authentication-with-asp-net-web-api-using-authentication-filter/.