Add custom data to ADFS authentication - c#

I have many applications and I'm switching the authentication to ADFS, and I need to add custom data, lets say an array of roles from a database after the successful login.
Scenario Explained:
Each application has its own roles in DB,
during a user authentication after an authorization request was sent, Application_AuthenticateRequest(object sender, EventArgs e) will be invoked, so I can add roles as claims like this
((ClaimsIdentity)((ClaimsPrincipal)currentUser).Identity)
.AddClaim(new Claim(ClaimTypes.Role, "role1FromDataBase"));
HttpContext.Current.User = currentUser;
But the Application_AuthenticateRequest() method will be invoked for each request and I don't want to request the roles from DB every time.
So, I need to add those roles somewhere to then be able to call them. Of course, Sessions and Cookies are not the best practice when I deal with API role based authorization.
Applications have Controllers and APIs and my ADFS on Windows server 2012
My OWIN Startup is
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseWsFederationAuthentication(
new WsFederationAuthenticationOptions
{
Wtrealm = realm,
MetadataAddress = adfsMetadata,
Notifications = new WsFederationAuthenticationNotifications()
{
RedirectToIdentityProvider = context =>
{
context.ProtocolMessage.Wreply = "https://localhost:44329/";
return Task.FromResult(0);
}
},
});
app.UseStageMarker(PipelineStage.Authenticate);
What can I do ?

after many hours i solved the problem
in Startup Class and public void Configuration(IAppBuilder app) method
we have to add claims with roles to WsFederationAuthenticationOptions
like this
app.UseWsFederationAuthentication(new WsFederationAuthenticationOptions
{
Wtrealm = realm,
MetadataAddress = adfsMetadata,
Notifications = new WsFederationAuthenticationNotifications()
{
// this method will be invoked after login succes
SecurityTokenValidated = notification =>
{
ClaimsIdentity identity = notification.AuthenticationTicket.Identity;
// here we can add claims and specify the type, in my case i want to add Role Claim
identity.AddClaim(new Claim(ClaimTypes.Role, "student"));
return Task.FromResult(0);
},
RedirectToIdentityProvider = context =>
{
context.ProtocolMessage.Wreply = "https://localhost:44329/";
return Task.FromResult(0);
}
},
});

Related

Azure AD Authentication with custom roles maintained in SQL Server in ASP.NET MVC 5.0

First up, I am NOT going to be maintaining roles (claims) from within Azure AD, as I have to maintain it within SQL Server. So, for Authentication, I am using Azure AD. Once authenticated, I query my claims tables (aspnetmembership) and add it to the identity.
Right now, the below code seems to be working fine. But I don't feel confident at all due to these questions I have, as I just don't know if I have coded it right.
Here's my code and here are my questions:
Like we do with Forms auth, once authenticated, am I supposed to set the Thread.Currentprincipal, as well as Context.User even with Azure AD authentication or does this line of code automatically do that for me (I sign in once azure ad authenticates fine)
HttpContext.Current.GetOwinContext().Authentication.SignIn(identity);
If yes to the above question, I am really confused as to how I must sequence the above Signin line of code with setting the Principal (as well as Context.User) with the Azure AD authenticated identity?
I never knew this but does the [Authorize] attribute in MVC 5.0 automatically do the call to check if the request is 'authenticated' as well?
How do I access the custom claims that I added in Startup, within my controllers?
Can you please explain how I need to be handling the cookies with AZure AD authentication?
Thanks in advance!
Here's my code:
public void ConfigureAuth(IAppBuilder app)
{
// Configure the db context, user manager and signin manager to use a single instance per request
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
// app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
//app.UseCookieAuthentication(new CookieAuthenticationOptions
//{
// CookieDomain = "localhost",
// SlidingExpiration = true,
// ExpireTimeSpan = TimeSpan.FromHours(2)
//});
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
// Sets the ClientId, authority, RedirectUri as obtained from web.config
ClientId = clientId,
Authority = authority,
RedirectUri = null,
// PostLogoutRedirectUri is the page that users will be redirected to after sign-out. In this case, it is using the home page
PostLogoutRedirectUri = redirectUri,
Scope = OpenIdConnectScope.OpenIdProfile,
// ResponseType is set to request the code id_token - which contains basic information about the signed-in user
ResponseType = OpenIdConnectResponseType.CodeIdToken,
// ValidateIssuer set to false to allow personal and work accounts from any organization to sign in to your application
// To only allow users from a single organizations, set ValidateIssuer to true and 'tenant' setting in web.config to the tenant name
// To allow users from only a list of specific organizations, set ValidateIssuer to true and use ValidIssuers parameter
TokenValidationParameters = new TokenValidationParameters()
{
//NameClaimType = "preferred_username",
ValidateIssuer = false // TODO: SET THIS TO TRUE EVENTUALLY.
},
// OpenIdConnectAuthenticationNotifications configures OWIN to send notification of failed authentications to OnAuthenticationFailed method
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailed,
SecurityTokenValidated = async (x) =>
{
var identity = x.AuthenticationTicket.Identity; //Check this.
await Task.FromResult(0);
var claims = identity.Claims;
var name = claims.First(claim => claim.Type == "name").Value;
var email = claims.First(claim => claim.Type == "preferred_username").Value;
var user = UserManager.FindByEmail(email);
var customClaims = UserManager.GetClaims(user.Id);
foreach (var claim in customClaims)
{
identity.AddClaim(new Claim(claim.Type, claim.Value));
}
HttpContext.Current.GetOwinContext().Authentication.SignIn(identity); //THis is the key here.
var principal = new ClaimsPrincipal(identity);
System.Threading.Thread.CurrentPrincipal = principal;
if (System.Web.HttpContext.Current != null)
System.Web.HttpContext.Current.User = principal;
}
}
}
);
}
And in my controller methods I am accessing my claims that I added above, using this method. Please confirm if this is correct or should I use the Thread.CurrentPrincipal somehow?
[Authorize]
public class HomeController : BaseController
{
private ApplicationUserManager _userManager;
public ActionResult Index()
{
//{
var identity = User.Identity as ClaimsIdentity;
var count = identity.Claims.Count(); //I get to see all the claims here that I set in startup
return View();
}

How to overwrite openid connect claims

I have an ASP.NET MVC framework web application. I want to use both .net identity and OpenId Connect for authentication (Microsoft accounts).
It works and redirects to another controller as I want. In this target controller I get information from the claims (which are returned from Azure AD).
What I want is to add more claims to this collection from Azure, or create a new set of claims as I want. I set claims as following but when debugger hits to another controller I see default claims returned by Azure AD only; my modifications are not reflected in the claims collection.
How can I add claims which is usable for both OpenId Connect (Microsoft) and .NET identity authentication?
This is how I set example claims in the controller:
var identity = new ClaimsIdentity("ApplicationCookie", ClaimsIdentity.DefaultNameClaimType, ClaimsIdentity.DefaultRoleClaimType);
identity.AddClaim(new Claim("test", "test"));
IAuthenticationManager authenticationManager = HttpContext.GetOwinContext().Authentication;
authenticationManager.SignOut("ApplicationCookie");
authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = true }, identity);
This is how I configured in Startup:
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
// Sets the ClientId, authority, RedirectUri as obtained from web.config
ClientId = clientId,
Authority = authority,
// PostLogoutRedirectUri is the page that users will be redirected to after sign-out. In this case, it is using the home page
PostLogoutRedirectUri = redirectUri,
Scope = OpenIdConnectScope.OpenIdProfile,
// ResponseType is set to request the code id_token - which contains basic information about the signed-in user
ResponseType = OpenIdConnectResponseType.CodeIdToken,
AuthenticationMode = AuthenticationMode.Passive,
// OpenIdConnectAuthenticationNotifications configures OWIN to send notification of failed authentications to OnAuthenticationFailed method
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailed
},
TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
RoleClaimType = System.Security.Claims.ClaimTypes.Role,
ValidateIssuer = false
}
});
You have two options. Either hook into the various events provided by AddCookie and AddOpenIDConnect. Or add a custom claims transformation, like:
public class BonusLevelClaimTransformation : IClaimsTransformation
{
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
if (!principal.HasClaim(c => c.Type == "bonuslevel"))
{
//Lookup bonus level.....
principal.Identities.First().AddClaim(new Claim("bonuslevel", "12345"));
}
return Task.FromResult(principal);
}
}
You also need to register it in Startup.cs like
services.AddTransient<IClaimsTransformation, BonusLevelClaimTransformation>();

ASP.NET MVC Identity - Using internal User and Azure AD Authentication together

We are already running an ASP.NET MVC web application which is using internal users via token authentication. This is implemented in the standard way the ASP.NET MVC template provides.
Now we have a requirement to extend this authentication model and allow external Azure AD users to sign into the web application for configured tenant. I have figured out everything on the Azure AD side. Thanks to Microsoft's Azure Samples example.
Now both individual account authentication and Azure AD are working well independently. But they're not working together. When I insert both middleware together its giving issue.
Here's my startup_auth.cs file:
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
}
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
string ClientId = ConfigurationManager.AppSettings["ida:ClientID"];
string Authority = "https://login.microsoftonline.com/common/";
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = ClientId,
Authority = Authority,
TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuer = false,
},
Notifications = new OpenIdConnectAuthenticationNotifications()
{
RedirectToIdentityProvider = (context) =>
{
string appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase;
context.ProtocolMessage.RedirectUri = appBaseUrl;
context.ProtocolMessage.PostLogoutRedirectUri = appBaseUrl;
return Task.FromResult(0);
},
SecurityTokenValidated = (context) =>
{
// retriever caller data from the incoming principal
string issuer = context.AuthenticationTicket.Identity.FindFirst("iss").Value;
string UPN = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.Name).Value;
string tenantID = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
if (
// the caller comes from an admin-consented, recorded issuer
(db.Tenants.FirstOrDefault(a => ((a.IssValue == issuer) && (a.AdminConsented))) == null)
// the caller is recorded in the db of users who went through the individual onboardoing
&& (db.Users.FirstOrDefault(b =>((b.UPN == UPN) && (b.TenantID == tenantID))) == null)
)
// the caller was neither from a trusted issuer or a registered user - throw to block the authentication flow
throw new SecurityTokenValidationException();
return Task.FromResult(0);
},
AuthenticationFailed = (context) =>
{
context.OwinContext.Response.Redirect("/Home/Error?message=" + context.Exception.Message);
context.HandleResponse(); // Suppress the exception
return Task.FromResult(0);
}
}
});
}
}
This configuration works well for local user accounts but doesn't work for AAD. To enable AAD authentication, I need to configure the UseCookieAuthentication part as shown below, which will break my local user account authentication.
app.UseCookieAuthentication(new CookieAuthenticationOptions { });
Basically I need to remove the local users middleware to get AAD work.
What I mean by AAD not working is, I am not able to go to any secured action which is protected by the [Authorize] attribute. It's calling the SecurityTokenValidated event, and I am able to get all AAD claims and able to validate against my custom tenant. But when I redirect to root of my app (which is a secured action) at the end, it throws me back to my custom login page. Seems it's not internally signing in the user and not creating the necessary authentication cookies.
I would appreciate any ideas on what I could be missing here.
Thanks
To support both individual accounts and other account from social data provider, only need to add them using OWIN component.
And to sign-out the users which login from Azure AD, we need to sign-out both the cookie issued from web app and Azure AD. First, I modified the ApplicationUser class to add the custom claim to detect whether the users login from Azure AD or individual accounts like below.
public class ApplicationUser : IdentityUser
{
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
{
// Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
// Add custom user claims here
if((this.Logins as System.Collections.Generic.List<IdentityUserLogin>).Count>0)
userIdentity.AddClaim(new Claim("idp", (this.Logins as System.Collections.Generic.List<IdentityUserLogin>)[0].LoginProvider));
return userIdentity;
}
}
Then we can change the LogOff method to support sign-out from Azure AD to clear the cookies from Azure AD:
// POST: /Account/LogOff
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult LogOff()
{
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
var idpClaim = ClaimsPrincipal.Current.Claims.FirstOrDefault(claim => { return claim.Type == "idp"; });
if (idpClaim!=null)
HttpContext.GetOwinContext().Authentication.SignOut(
OpenIdConnectAuthenticationDefaults.AuthenticationType, CookieAuthenticationDefaults.AuthenticationType);
return RedirectToAction("Index", "Home");
}
It sounds like your OpenID Connect auth is not connecting to your Cookie auth. It looks like you need to specify a SignInAsAuthenticationType in your OpenIdConnectAuthenticationOptions that matches the AuthenticationType in your CookieAuthenticationOptions or your ExternalCookie auth type.

How do you manually set a logged in Identity User?

I am using ASP.NET Identity with an ADFS server. For development purposes, I want to avoid using the ADFS server when I'm in a network environment where I can't reach the ADFS server. That's why I added a simple controller action in my HomeController that manually sets the currently logged in user:
#if DEBUG
[AllowAnonymous]
public ActionResult LogIn()
{
var claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.NameIdentifier, "tester"));
System.Web.HttpContext.Current.User = new ClaimsPrincipal(new ClaimsIdentity(claims));
System.Threading.Thread.CurrentPrincipal = System.Web.HttpContext.Current.User;
return Redirect("Home/Index");
}
#endif
And the Owin Configuration method:
public void Configuration(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions() { });
app.UseWsFederationAuthentication(
new WsFederationAuthenticationOptions
{
Wtrealm = realm,
MetadataAddress = adfsMetadata
});
}
Commenting out the part where I use WsFederation Authentication is no problem, that way there is no link to my current ADFS server.
The problem: When I'm redirected to the Home/Index action (that has the Authorize attribute), ASP.NET Identity doesn't recognize my ClaimsPrincipal as a valid login, so I'm redirected to the Home/Login action, which creates a loop between Home/Login and Home/Index constantly.
My question: how do I make ASP.NET accept the ClaimsPrincipal created above as a valid login?
Problem with you approach - the cookie is not set, so the user information is not preserved across the HTTP requests. Your approach works only within a single call (there are uses for that, but not for you)
You can still use IAuthenticationManager from OWIN to set the cookie:
#if DEBUG
[AllowAnonymous]
public ActionResult LogIn()
{
var identity = new ClaimsIdentity("ApplicationCookie", ClaimsIdentity.DefaultNameClaimType, ClaimsIdentity.DefaultRoleClaimType);
identity.AddClaim(new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", "Active Directory"));
identity.AddClaim(new Claim(ClaimTypes.Name, "Testy McTestface"));
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, "testUser"));
IAuthenticationManager authenticationManager = HttpContext.GetOwinContext().Authentication;
authenticationManager.SignOut("ApplicationCookie");
authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = false }, identity);
return Redirect("Home/Index");
}
#endif
You will need nuget packages Microsoft.Owin.Security.Cookies,
Microsoft.Owin.Host.SystemWeb. See more explanations in my blog-post about authentication with AD
You will also need to make sure CookieAuthenticationMiddleware is configured correclty:
public void Configuration(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "ApplicationCookie",
LoginPath = new PathString("/Home/Login"),
Provider = new CookieAuthenticationProvider(),
CookieName = "ApplicationCookie",
CookieHttpOnly = true,
ExpireTimeSpan = TimeSpan.FromHours(1),
});
app.UseWsFederationAuthentication(
new WsFederationAuthenticationOptions
{
Wtrealm = realm,
MetadataAddress = adfsMetadata
});
}
Especially pay authentication to AuthenticationType value - it must match the value in ClaimsIdentity constructor. Otherwise cookie will not be set, or you won't be able to log-out.

How to set Claims from ASP.Net OpenID Connect OWIN components?

I have questions upon using the new ASP.Net OpenID Connect framework while adding new Claims during the authentication pipeline as shown in the code below. I'm not sure just how much 'magic' is happening behind the scenes. I think most of my questions center around not knowing much about OWIN authentication middleware as opposed to OpenID Connect.
Q1. Should I be manually setting HttpContext.Current.User and Thread.CurrentPrincipal from OwinContext.Authentication.User?
Q2. I want the ability to add object types to claims like I used to with System.IdentityModel.Claims.Claim. The new System.Security.Claims.Claim class only accepts string values?
Q3. Do I need to use the new SessionSecurityToken wrapper for my ClaimsPrincipal in System.Security.Claims.CurrentPrincipal for serializing into a cookie - I am using app.UseCookieAuthentication(new CookieAuthenticationOptions()); but now sure what that does exactly in terms of maintaining any additional claims I added during SecurityTokenValidated event?
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
PostLogoutRedirectUri = postLogoutRedirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications()
{
SecurityTokenValidated = (context) =>
{
// retriever caller data from the incoming principal
var UPN = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.Name).Value;
var db = new SOSBIADPEntities();
var user = db.DomainUser.FirstOrDefault(b => (b.EntityName == UPN));
if (user == null)
{
// the caller was not a registered user - throw to block the authentication flow
throw new SecurityTokenValidationException();
}
var applicationUserIdentity = new ClaimsIdentity();
applicationUserIdentity.AddClaim(new Claim(ClaimTypes.Name, UPN, ""));
applicationUserIdentity.AddClaim(new Claim(ClaimTypes.Sid, user.ID.ToString(CultureInfo.InvariantCulture)));
var applications =
db.ApplicationUser
.Where(x => x.ApplicationChild != null && x.DomainUser.ID == user.ID)
.Select(x => x.ApplicationChild).OrderBy(x => x.SortOrder);
applications.ForEach(x =>
applicationUserIdentity.AddClaim(new Claim(ClaimTypes.System, x.ID.ToString(CultureInfo.InvariantCulture))));
context.OwinContext.Authentication.User.AddIdentity(applicationUserIdentity);
var hasOutlook = context.OwinContext.Authentication.User.HasClaim(ClaimTypes.System, "1");
hasOutlook = hasOutlook;
HttpContext.Current.User = context.OwinContext.Authentication.User;
Thread.CurrentPrincipal = context.OwinContext.Authentication.User;
var usr = HttpContext.Current.User;
var c = System.Security.Claims.ClaimsPrincipal.Current.Claims.Count();
return Task.FromResult(0);
},
}
}
);
}
Is there a specific reason for which you are adding a new ClaimsIdentity?
The simplest way of doing what you are aiming at is to retrieve the ClaimsIdentity that was generated by validating the incoming token, via ClaimsIdentity claimsId = context.AuthenticationTicket.Identity; once you have it, just add claims to it. The rest of the middleware will take care of serializing it in the session cookie along with everything else, place the result in the current ClaimsPrincipal, and all those other things you appear to be trying to do manually.
HTH
V.
When performing the token validation you can SignIn with new Identity:
private Task OnSecurityTokenValidated(SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> n)
{
var claimIdentity = new ClaimsIdentity(n.AuthenticationTicket.Identity);
// Custom code...
claimIdentity.Claims.Append(new Claim("TEST","1234"));
n.OwinContext.Authentication.SignIn(claimIdentity);
return Task.FromResult(0);
}
Another option make the assignment directly, not working for me:
private Task OnSecurityTokenValidated(SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> n)
{
var claimsPrincipal = new ClaimsPrincipal(n.AuthenticationTicket.Identity);
// Custom code...
// TEST:
n.OwinContext.Response.Context.Authentication.User = claimsPrincipal;
n.OwinContext.Request.User = claimsPrincipal;
n.OwinContext.Authentication.User = claimsPrincipal;
return Task.FromResult(0);
}

Categories

Resources