OWIN with LDAP Authentication - asp.net-mvc-5

Here is my scenario. I have an MVC 5 application that uses Owin as an authentication mechanism. The default template calls the SignInManager.PasswordSignInAsync in the Login action which I would like to overwrite to use LDAP to validate the user instead of looking into the database.
I am able to do the validation via:
PrincipalContext dc = new PrincipalContext(ContextType.Domain, "domain.com", "DC=domain,DC=com", "user_name", "password");
bool authenticated = dc.ValidateCredentials(userName, password);
Then I can retrieve the UserPrincipal using:
UserPrincipal user = UserPrincipal.FindByIdentity(dc, IdentityType.SamAccountName, userName);
However, I am stuck here and I am not sure how to continue with signing in the user. The goal is that after I sign in the user, I would have access to User.Identity including all the roles the user is in. Essentially, the app should behave as if it uses Windows Authentication, but the credentials are provided by the user on the Login page.
You would probably ask why not user Windows Authentication directly. The app will be accessed from the outside of the network, but the requirements are to use AD authentication and authorization. Hence my predicament.
Any suggestions are highly appreciated.
Thank you.

After many hours of research and trial and error, here is what I ended up doing:
AccountController.cs - Create the application user and sign in
ApplicationUser usr = new ApplicationUser() { UserName = model.Email };
bool auth = await UserManager.CheckPasswordAsync(usr, model.Password);
if (auth)
{
List claims = new List();
foreach (var group in Request.LogonUserIdentity.Groups)
{
string role = new SecurityIdentifier(group.Value).Translate(typeof(NTAccount)).Value;
string clean = role.Substring(role.IndexOf("\\") + 1, role.Length - (role.IndexOf("\\") + 1));
claims.Add(new Claim(ClaimTypes.Role, clean));
}
claims.Add(new Claim(ClaimTypes.NameIdentifier, model.Email));
claims.Add(new Claim(ClaimTypes.Name, model.Email));
ClaimsIdentity ci = new ClaimsIdentity(claims, DefaultAuthenticationTypes.ApplicationCookie);
AuthenticationManager.SignIn(new AuthenticationProperties()
{
AllowRefresh = true,
IsPersistent = false,
ExpiresUtc = DateTime.UtcNow.AddDays(7),
}, ci);
return RedirectToLocal(returnUrl);
}
else
{
ModelState.AddModelError("", "Invalid login credentials.");
return View(model);
}
IdentityConfig.cs (CheckPasswordAsync) - Authenticate against LDAP
public override async Task CheckPasswordAsync(ApplicationUser user, string password)
{
PrincipalContext dc = new PrincipalContext(ContextType.Domain, "domain", "DC=domain,DC=com", [user_name], [password]);
bool authenticated = dc.ValidateCredentials(user.UserName, password);
return authenticated;
}
Global.asax - if you are using the Anti Forgery Token in your login form
AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier;
At this point, you will are logged in and can access the User.Identity object. You can also mark controllers and actions with [Authorize(Roles = "some_role"]
It turned out that it was easier than I thought, it is just that not much is really written on the topic (at least I could not find anything).
Also, this code presumes that you are running the app from a server which has access to the Domain Controller on your network. If you are on a DMZ server, you need to discuss this strategy with your network admin for other options.
I hope this saves you some time. I am also eager to hear what the community thinks of this. Maybe there is a better way of handling this situation. If so, please share it here.
Thanks.
Daniel D.

Related

How to query MS Graph API in User Context?

I'm trying to change a user's password using MS Graph API. I was checking earlier questions like this and this where the answer were always similar: register an AAD application, because changing the password requires Delegated
UserAuthenticationMethod.ReadWrite.All permissions, and you cannot set that in a B2C application as a B2C app supports only offline_access and openid for Delegated.
So the answers were always suggesting creating an AAD app, and using this app I could query the Graph API on behalf of the user. The question is, how to achieve this? If I check the documentation from Microsoft: Get access on behalf of a user, it is saying that first you need to get authorization, only then you can proceed to get your access token.
But as part of the authorization process, there is a user consent screen. If I'm calling my ASP.NET Core Web API endpoint to change my password on behalf of my user, how will it work on the server? The client won't be able to consent, if I'm doing these calls on the server, right?
Also, I'm using Microsoft.Graph and Microsoft.Graph.Auth Nuget packages and it's not clear how to perform these calls on behalf of the user. I was trying to do this:
var client = new GraphServiceClient(new SimpleAuthProvider(authToken));
await client.Users[myUserId]
.ChangePassword(currentPassword, newPassword)
.Request()
.PostAsync();
Where SimpleAuthProvider is just a dummy IAuthProvider implementation.
Any ideas how to make this work?
OK, got it:
static void ChangePasswordOfAUser()
{
var myAzureId = "65e328e8-5017-4966-93f0-b651d5261e2c"; // id of B2C user
var currentPassword = "my_old_pwd";
var newPassword = "newPassword!";
using (var client = new HttpClient())
{
var passwordTokenRequest = new PasswordTokenRequest
{
Address = $"https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token",
ClientId = clientId, // client ID of AAD app - not the B2C app!
ClientSecret = clientSecret,
UserName = $"{myAzureId}#contoso.onmicrosoft.com",
Password = currentPassword,
Scope = "https://graph.microsoft.com/.default" // you need to have delegate access
};
var response = client.RequestPasswordTokenAsync(passwordTokenRequest).Result;
var userAccessToken = response.AccessToken;
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {userAccessToken}");
var json = System.Text.Json.JsonSerializer.Serialize(new
{
currentPassword = currentPassword,
newPassword = newPassword
});
var changePasswordResponse = client.PostAsync(
$"https://graph.microsoft.com/v1.0/users/{myAzureId}/changePassword",
new StringContent(json, Encoding.UTF8, "application/json"))
.Result;
changePasswordResponse.EnsureSuccessStatusCode();
}
}

How Owin Works regarding AuthenticationTicket to a authenticated UserPrincipal

So, ive made a simple basic auth middleware for owin. The middleware returns an AuthenticationTicket with the user principal claims, authentication type ("Basic") yada yada.
protected override Task<AuthenticationTicket> AuthenticateCoreAsync()
{
....
var claimsIdentity = TryGetPrincipalFromBasicCredentials(token, Options.CredentialValidationFunction);
....
var ticket = new AuthenticationTicket(claimsIdentity, new AuthenticationProperties(){userName = claimsIdentity.name});
return Task.FromResult(ticket);
}
To get the claims, im using a hardcoded value for testing:
var claims = new List<Claim>();
claims.Add(new Claim(KneatClaimTypes.Permission, Permission.UserAdministrationView.ToString()));
var claimsIdentity = new ClaimsIdentity(
new GenericIdentity("Administrator", "Basic"),
claims
);
return claimsIdentity;
I`m hooking into owin using a simple extension using the same architecture IdentityModel has
https://github.com/IdentityModel/IdentityModel.Owin.BasicAuthentication
app.UseBasicAuthentication(new BasicAuthOptions("Realm",
(username, password, context) => BasicAuthCredentialCheck.Authenticate(username, password, context))
{
AuthenticationMode = AuthenticationMode.Active,
AuthenticationType = "Basic"
});
Im creating the correct principal for the ticket, but after the owin resolves the authentication the Thread.currentPrincipal is not populated. If i add a simple cookie auth , whenever i call Owin.Authentication.SignIn(...) it creates the Principal for me. If i set manually the Thread.currentPrincipal, something is UNSETTING that.
Im not very deep on how owin works under the hood, and i was tryng to get the source from it and begin reading it maybe ill figure it out. But in case i dont, any ideas on who could be resetting this Thread.currentPrincipal ? (Searched my own app code, no one does that, only reads no setting)
Best Regards.

IdentityServer4 - is it possible to use local login form with external provider and no round trip?

I'm trying to use a local login form to authenticate a user credentials against its external provider (Azure Active Directory).
I understand that, per client, you can enable local login. That helps, as when set to true, I'll get the local login form but but I'm still unclear as to how to fire off the middle ware for that external provider. Is there a way to send client credentials to the external provider to receive an ID token? My current code redirects to the Microsoft login; and then back to my identity server, and then the client application. I want the user to login in through identity server but not have them know it's really authenticating against Azure.
Here's my start up:
var schemeName = "Azure-AD";
var dataProtectionProvibder = app.ApplicationServices.GetRequiredService<IDataProtectionProvider>();
var distributedCache = app.ApplicationServices.GetRequiredService<IDistributedCache>();
var dataProtector = dataProtectionProvider.CreateProtector(
typeof(OpenIdConnectMiddleware).FullName,
typeof(string).FullName, schemeName,
"v1");
var dataFormat = new CachedPropertiesDataFormat(distributedCache, dataProtector);
///
/// Azure AD Configuration
///
var clientId = Configuration["AzureActiveDirectory:ClientId"];
var tenantId = Configuration["AzureActiveDirectory:TenantId"];
Redirect = Configuration["AzureActiveDirectory:TenantId"];
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
AuthenticationScheme = schemeName,
DisplayName = "Azure-AD",
SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme,
ClientId = clientId,
Authority = $"https://login.microsoftonline.com/{tenantId}",
ResponseType = OpenIdConnectResponseType.IdToken,
StateDataFormat = dataFormat,
});
app.UseIdentity();
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
This is the login.
[HttpGet]
public async Task<IActionResult> ExternalLogin(string provider, string returnUrl)
{
var context = this.HttpContext.Authentication;
List<AuthenticationDescription> schemes = context.GetAuthenticationSchemes().ToList();
returnUrl = Url.Action("ExternalLoginCallback", new { returnUrl = returnUrl });
// start challenge and roundtrip the return URL
var props = new AuthenticationProperties
{
RedirectUri = returnUrl,
Items = { { "scheme", provider } }
};
//await HttpContext.Authentication.ChallengeAsync(provider, props);
return new ChallengeResult(provider, props);
}
In my opinion ,we shouldn't directly pass the username/password directly from other Idp to azure AD for authentication as a security implementation .And even Azure AD supports the Resource Owner Password Credentials Grant ,it's only available in native client. I suggest you keep the normal way and don't mix them .

How to change authentication cookies after changing UserName of current user with asp.net identity

Using asp.net identity version 1.0.0-rc1 with Entity Framework 6.0.0-rc1 (the ones that come with Visual Studio 2013 RC).
Trying to give users an opportunity to change their UserName.
There seems to be no function for that under AuthenticationIdentityManager, so I change the data using EF (get User object for current user, change UserName and save changes).
The problem is that authentication cookies remain unchanged, and the next request fails as there is no such user.
With forms authentication in the past I used the following code to solve this.
var formsAuthCookie = Request.Cookies[FormsAuthentication.FormsCookieName];
var isPersistent = FormsAuthentication.Decrypt(formsAuthCookie.Value).IsPersistent;
FormsAuthentication.SetAuthCookie(newUserName, isPersistent);
What should I do with asp.net identity to update the cookies?
UPDATE
The following code seems to update the authentication cookie.
var identity = new ClaimsIdentity(User.Identity);
identity.RemoveClaim(identity.FindFirst(identity.NameClaimType));
identity.AddClaim(new Claim(identity.NameClaimType, newUserName));
AuthenticationManager.AuthenticationResponseGrant = new AuthenticationResponseGrant
(new ClaimsPrincipal(identity), new AuthenticationProperties {IsPersistent = false});
The remaining problem is: how to extract IsPersistent value from current authentication cookie?
How do you login/authenticate a user with Asp.Net MVC5 RTM bits using AspNet.Identity?
private async Task SignInAsync(ApplicationUser user, bool isPersistent)
{
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
}
For the RC1, You can use the similar code.
AuthenticationManager.SignOut();
IdentityManager.Authentication.SignIn(AuthenticationManager, user.UserId, isPersistent:false);
For persistent value, you need to access the authentication cookie and retrieve the status.
Updated:
Use appropriate AuthenticationType used in place of "Bearer". Also make sure while issuing the signin ticket, you are setting the AuthenticationProperties.IsPersistent.
bool isPersistent=false;
var authContext = await Authentication.AuthenticateAsync("Bearer");
if (authContext != null)
{
var aProperties = authContext.Properties;
isPersistent = aProperties.IsPersistent;
}

Adding features to ServiceStack auth provider

I am evaluating ServiceStack using OrmLite. The built in Auth service, along with Session and Cache are so much better than ASP.NET membership provider.
However, out of the box the Auth Service does not provide some of the features required for apps we want to build like:
Change password
Locking of account after 3 unsuccessful logon attempts
Disabling user accounts
Password reminder question and answer
Audit log of log on attempts
Do I need to build custom auth provider or is there something out there which already does provides this functionality?
Many thanks!
I'm just starting to implement a password reset and can see two ways of achieving it (I've not tested - or even tried - either yet):
1.Create a class that inherits from Registration and handles PUT. It should then be possible to call the UpdateUserAuth method of the registration class which would change the password. The problem - for me - here is that the put validation requires username and password to be specified, not just one (We only use email as an identifier). This could be worked around by turning the validation feature off.
2.Create a password reset service that does what UpdateUserAuth does.
var session = this.GetSession();
var existingUser = UserAuthRepo.GetUserAuth(session, null);
if (existingUser == null)
{
throw HttpError.NotFound("User does not exist");
}
var newUserAuth = ToUserAuth(request);
UserAuthRepo.UpdateUserAuth(newUserAuth, existingUser, request.Password);
Obviously need to add some appropriate validation in.
UPDATED
I've put my change password reminder/reset service up as a gist (My first gist!)
here's what I did, works well. - I realise the "new" is a code-smell, just inject it :)
private int LoginAttempts = 0;
public override bool TryAuthenticate(IServiceBase authService, string userName, string password)
{
var authRepo = authService.TryResolve<IUserAuthRepository>();
if (authRepo == null)
{
Log.WarnFormat("Tried to authenticate without a registered IUserAuthRepository");
return false;
}
var session = authService.GetSession();
UserAuth userAuth = null;
if (authRepo.TryAuthenticate(userName, password, out userAuth))
{
session.PopulateWith(userAuth);
session.IsAuthenticated = true;
session.UserAuthId = userAuth.Id.ToString(CultureInfo.InvariantCulture);
session.ProviderOAuthAccess = authRepo.GetUserOAuthProviders(session.UserAuthId)
.ConvertAll(x => (IOAuthTokens)x);
return true;
}
else
{
LoginAttempts++;
if (LoginAttempts >= 3)
{
ServiceStack.ServiceInterface.Service s = new Service();
s.Db.ExecuteSql("update [User] set AccountLocked = 'true' where Email='" + userName + "'");
}
authService.RemoveSession();
return false;
}
}
and I hope the mod_from_hell manages to leave this alone!!!

Resources