Updating claims using Federated Authentication in asp.net mvc - asp.net-mvc

I'm using the FederatedAuthentication class in my MVC project.
To create the cookie I use FederatedAuthentication.SessionAuthenticationModule.CreateSessionCookie(...);, but I can't seem to find a way to update them if the user wants to change for example their first name.
How can I access and update the claims without logging out?

The process is rather quite easy
Get the list of current claims of the ClaimsPrincipal
Remove the claims you want to update
Add new claims
Authenticate like normal, passing the list of claims.
var claims = FederatedAuthentication.SessionAuthenticationModule
.ContextSessionSecurityToken.ClaimsPrincipal.Claims.ToList();
var givenNameClaim = claims.Single(x => x.Type == "given_name");
var familyNameClaim = claims.Single(x => x.Type == "family_name");
var timeZoneClaim = claims.Single(x => x.Type == "time_zone_string");
var newName = new Claim(givenNameClaim.Type.ToString(), model.FirstName, givenNameClaim.ValueType, givenNameClaim.Issuer);
var familyName = new Claim(familyNameClaim.Type.ToString(), model.LastName, familyNameClaim.ValueType, familyNameClaim.Issuer);
var timeZone = new Claim(timeZoneClaim.Type.ToString(), model.SelectedTimeZone, timeZoneClaim.ValueType, timeZoneClaim.Issuer);
UpdateClaims(newName, familyName, timeZone);
public void UpdateClaims(params Claim[] updatedClaims)
{
var currentClaims = FederatedAuthentication.SessionAuthenticationModule.ContextSessionSecurityToken.ClaimsPrincipal.Claims.ToList();
foreach (var claim in updatedClaims)
{
currentClaims.Remove(currentClaims.Single(x => x.Type == claim.Type));
currentClaims.Add(claim);
}
var principal = new ClaimsPrincipal(new ClaimsIdentity[] { new ClaimsIdentity(currentClaims, "Auth0") });
var session = FederatedAuthentication.SessionAuthenticationModule.CreateSessionSecurityToken(
principal,
null,
DateTime.UtcNow,
DateTime.MaxValue.ToUniversalTime(),
true);
FederatedAuthentication.SessionAuthenticationModule.AuthenticateSessionSecurityToken(session, true);
}

Related

where to add an additional claim to the current principal after log in?

I have an asp.net mvc app, and am utilizing the code from the following sample: https://github.com/Azure-Samples/active-directory-b2c-dotnet-webapp-and-webapi
After the user is logged in, I would like to add a system admin role claim to the principal claims, and the best place I can figure is in the Startup.Auth.cs :
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification notification)
{
try
{
var userEmail = notification.AuthenticationTicket.Identity.
Claims.SingleOrDefault(x => x.Type == ClaimTypes.Email)?.Value;
if (userEmail != null)
{
using (var ctx = new DbContext())
{
var currentUser = ctx.Users.SingleOrDefault(u => u.Email == userEmail);
if (currentUser != null && currentUser.IsAdmin)
{
notification.AuthenticationTicket.Identity.AddClaim(new Claim(ClaimTypes.Role, Common.Configuration.RoleAdministratorName));
}
}
}
This seems to work, but somehow doesn't feel right to instantiate a new db context just for this in startup.auth. Is this normal practice to do it here?

MVC Razor view to detect if authenticated user has social login provider?

Is there any way in an MVC Razor view to do something like:
if (user has associated/logged in with a Facebook account)
I think in code behind I can retrieve it like so:
var logins = await UserManager.GetLoginsAsync(loggedInUserId);
string loginProvider = "Facebook"
string providerKey = logins.Where(c => c.LoginProvider == loginProvider)
.Select(c => c.ProviderKey)
.FirstOrDefault();
However a helper class doesn't have the context to get 'UserManager' (I don't think). I'm not sure if I'm reinventing the wheel here or if there's a simple way to do this....
Thanks.
UPDATE
Sorry....I probably should have mentioned I want to add this logic to a shared partial view (my main menu) that I want to use across all pages. Hence i think a ViewModel is probably out?
I think I have a working solution. In a helper class I've created:
public static string GetLoginType(this IPrincipal user)
{
if (!(user.Identity is ClaimsIdentity)) return "";
string loginProvider = ((ClaimsIdentity)user.Identity).Claims
.Where(c => c.Type.Equals("ExternalLoginProvider"))
.Select(c => c.Value)
.FirstOrDefault();
return loginProvider;
}
and in my razor view I have:
#using Namespace.Helpers
if (User.GetLoginType() == "Facebook")
{
//do something
}
and in my external login sign-in method i currently have:
private async Task SignInAsync(ApplicationUser user, bool isPersistent, string loginProvider)
{
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
//create new claim called ExternalLoginProvider with a value of for example, "Facebook"
Claim clm = new Claim("ExternalLoginProvider", loginProvider);
identity.AddClaim(clm);
AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
}

Seed Roles (RoleManager vs RoleStore)

Through looking at the posts here, I've seen two different ways of creating ASP.NET Identity roles through Entity Framework seeding. One way uses RoleManager and the other uses RoleStore. I was wondering if there is a difference between the two. As using the latter will avoid one less initialization
string[] roles = { "Admin", "Moderator", "User" };
// Create Role through RoleManager
var roleStore = new RoleStore<IdentityRole>(context);
var manager = new RoleManager<IdentityRole>(roleStore);
foreach (string role in roles)
{
if (!context.Roles.Any(r => r.Name == role))
{
manager.Create(new IdentityRole(role));
}
// Create Role through RoleStore
var roleStore = new RoleStore<IdentityRole>(context);
foreach (string role in roles)
{
if (!context.Roles.Any(r => r.Name == role))
{
roleStore.CreateAsync(new IdentityRole(role));
}
}
In your specific case, using both methods, you achieve the same results.
But, the correct usage would be:
var context = new ApplicationIdentityDbContext();
var roleStore = new RoleStore<IdentityRole>(context);
var roleManager = new RoleManager<IdentityRole>(roleStore);
string[] roles = { "Admin", "Moderator", "User" };
foreach (string role in roles)
{
if (!roleManager.RoleExists(role))
{
roleManager.Create(new IdentityRole(role));
}
}
The RoleManager is a wrapper over a RoleStore, so when you are adding roles to the manager, you are actually inserting them in the store, but the difference here is that the RoleManager can implement a custom IIdentityValidator<TRole> role validator.
So, implementing the validator, each time you add a role through the manager, it will first be validated before being added to the store.

ASP.NET MVC 5 Dynamic Azure AD connectivity

I am currently building a SaaS - application with ASP.NET MVC 5 as the core platform. One of the requirements is to allow tenants to login with their Azure AD Accounts.
Here is the background story:
It's a multi tenant application where each customer/tenant can have multiple users. Tenants share the same instance of the application, so they will ultimately all navigate to the same URL (myapplication.com). We allow users to login with their organizational accounts (Azure AD, Office 365). Since all tenants go to the same URL, we cannot determine upfront which client ID/secret to use as this will only work for just one client. By using subdomains, we can identify the client (e.g. client1.ourapplication.com), so we can retrieve the Azure AD data from the database.The next step would be to use pass this data to the OpenID Authentication Options for each request that comes in, instead of 'hard coding' it in compile time during the startup of the application.
The issue that I'm currently facing is how I can get rid of the hard coded ClientId and AppKeys in the Web.Config (or database, as is in this case) and dynamically change these values according to the user that is logging in. In other words, I need to create the Azure AD login provider on the fly for every login request.
In the full code sample below, I somehow need to be able to set this line app.UseOpenIdConnectAuthentication(...) during runtime and not during the startup of the application with the updated clientID and appKey of the tenant in question.
public void ConfigureAuth(IAppBuilder app)
{
string clientId = string.Empty;
string appKey = string.Empty;
ConfigurationWorker worker = new ConfigurationWorker(DependencyResolver.Current.GetService<IUnitOfWork>());
IEnumerable<Config> azureAdConfiguration = worker.GetConfiguration("AzureId", "AzurePassword").Result;
if (azureAdConfiguration != null && azureAdConfiguration.Count() == 2)
{
clientId = azureAdConfiguration.First(x => x.Key == "AzureId").Value;
appKey = azureAdConfiguration.First(x => x.Key == "AzurePassword").Value;
}
string graphResourceID = "https://graph.windows.net";
string Authority = "https://login.microsoftonline.com/common/";
app.UseOpenIdConnectAuthentication(this.GetAzureADLoginProvider(clientId, appKey, Authority, graphResourceID));
}
private OpenIdConnectAuthenticationOptions GetAzureADLoginProvider(string clientId, string appKey, string Authority, string graphResourceID)
{
return new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = Authority,
AuthenticationMode = AuthenticationMode.Passive,
TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuer = false,
},
Notifications = new OpenIdConnectAuthenticationNotifications()
{
AuthorizationCodeReceived = (context) =>
{
var code = context.Code;
ClientCredential credential = new ClientCredential(clientId, appKey);
string tenantID = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
string signedInUserID = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;
AuthenticationContext authContext = new AuthenticationContext(string.Format("https://login.microsoftonline.com/{0}", tenantID));
AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(
code,
new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)),
credential,
graphResourceID);
return Task.FromResult(0);
},
RedirectToIdentityProvider = (context) =>
{
// This ensures that the address used for sign in and sign out is picked up dynamically from the request
// this allows you to deploy your app (to Azure Web Sites, for example)without having to change settings
// Remember that the base URL of the address used here must be provisioned in Azure AD beforehand.
string appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase;
context.ProtocolMessage.RedirectUri = appBaseUrl + "/";
context.ProtocolMessage.PostLogoutRedirectUri = appBaseUrl;
return Task.FromResult(0);
},
// we use this notification for injecting our custom logic
SecurityTokenValidated = (context) =>
{
// Once user has logged in, create and replace existing claims identity
ClaimsIdentity newClaimsIdentity = ClaimsHelper.Create(context.AuthenticationTicket.Identity, (x) => Singleton.Instance().Logger.LogInformation(x));
// Ultimately add tenant ID from azure to claims identity
Claim tenantIdClaim = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid");
newClaimsIdentity.AddClaim(tenantIdClaim);
// Set context's user to the new claim identity
context.AuthenticationTicket = new AuthenticationTicket(newClaimsIdentity, context.AuthenticationTicket.Properties);
return Task.FromResult(0);
},
AuthenticationFailed = (context) =>
{
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
return Task.FromResult(0);
},
MessageReceived = (context) =>
{
return Task.FromResult(0);
},
SecurityTokenReceived = (context) =>
{
return Task.FromResult(0);
}
}
});
}
This is the AccountController's action that will be executed if the user selects to login with Azure AD:
[AllowAnonymous]
public void SignInAzure()
{
// Send an OpenID Connect sign-in request.
if (!Request.IsAuthenticated)
{
this.AuthenticationManager.Challenge(
new AuthenticationProperties { RedirectUri = Url.Action("AzureADCallback", new { returnUrl = "~/" }) },
OpenIdConnectAuthenticationDefaults.AuthenticationType
);
}
}
Can I somehow set the clientID and AppKey in this controller? Because at this point I'm able to determine the tenant (using the subdomain), which allows me to query the correct values (in the database).
Something like this pseudocode would be awesome to have:
this.AuthenticationManager.OpenIdConnectAuthenticationOptions.ClientID= NEWID;
this.AuthenticationManager.OpenIdConnectAuthenticationOptions.AppKey= NEWAPPKEY;
Or equally as well create the OpenIdConnectAuthenticationOptions object on the fly per request.
Is there any possibility to achieve this?
Update:
As a workaround for this issue, I currently use this approach, as suggested by this solution:
[AllowAnonymous]
public async Task<ActionResult> SignInAzure()
{
// Resolve tenant
string currentTenant = this.HttpContext.Request?.RequestContext?.RouteData?.Values["tenant"]?.ToString();
IEnumerable<Config> configKeys = await this.ConfigurationWorker.GetConfiguration(currentTenant, ConfigurationKeys.AzureId, ConfigurationKeys.AzurePassword);
string clientId = string.Empty;
string appKey = string.Empty;
if (configKeys != null && configKeys.Count() == 2)
{
clientId = configKeys.First(x => x.Key == ConfigurationKeys.AzureId).Value;
appKey = configKeys.First(x => x.Key == ConfigurationKeys.AzurePassword).Value;
}
string stateMarker = Guid.NewGuid().ToString();
string returnUrl = this.Request.Url.GetLeftPart(UriPartial.Authority).ToString() + "/Account/AzureADCallback";
string authorizationRequest = String.Format(
"https://login.microsoftonline.com/common/oauth2/authorize?response_type=code&client_id={0}&resource={1}&redirect_uri={2}&state={3}",
Uri.EscapeDataString(clientId),
Uri.EscapeDataString("https://graph.windows.net"),
Uri.EscapeDataString(returnUrl),
Uri.EscapeDataString(stateMarker)
);
authorizationRequest += String.Format("&prompt={0}", Uri.EscapeDataString("admin_consent"));
return new RedirectResult(authorizationRequest);
}
[AllowAnonymous]
public async Task<ActionResult> AzureADCallback(string code, string error, string error_description, string resource, string state)
{
// Resolve tenant
string currentTenant = this.HttpContext.Request?.RequestContext?.RouteData?.Values["tenant"]?.ToString();
IEnumerable<Config> configKeys = await this.ConfigurationWorker.GetConfiguration(ConfigurationKeys.AzureId, ConfigurationKeys.AzurePassword);
string clientId = string.Empty;
string appKey = string.Empty;
if (configKeys != null && configKeys.Count() == 2)
{
clientId = configKeys.First(x => x.Key == ConfigurationKeys.AzureId).Value;
appKey = configKeys.First(x => x.Key == ConfigurationKeys.AzurePassword).Value;
}
ClientCredential credential = new ClientCredential(clientId, appKey);
AuthenticationContext authContext = new AuthenticationContext("https://login.windows.net/common/");
AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(code, new Uri(Request.Url.GetLeftPart(UriPartial.Path)), credential);
var user = this.UserManager.FindByEmail(result.UserInfo.DisplayableId);
await this.SignInAsync(user, false);
return RedirectToAction("Index", "Home");
}
In a nutshell, the tenant is resolved from the URL (subdomains) after which the database is called to get the Azure ID and Secret. This is used to generate the URL for the user to login. After the user has logged in to Azure AD, an AuthenticationResult is generated and the user is signed in to the application.

Azure ActiveDirectory Graph API GraphClient not returning AD Groups

I want to retrieve a User's Group information from Azure AD.
Using the following Graph API packages to achieve this
Microsoft.Azure.ActiveDirectory.GraphClient
Microsoft.IdentityModel.Clients.ActiveDirectory 2.13.112191810
I am able to successfully retrieve Users information from the Azure Graph API.
But when I run this method to retrieve a User's groups, Fiddler shows a successful HTTP 200 response with JSON fragment containing group information however the method itself does not return with the IEnumerable.
IEnumerable<string> groups = user.GetMemberGroupsAsync(false).Result.ToList();
The code doesn't seem to return from this async request.
The resulting experience is blank page while the authentication pipeline gets stuck.
Full code
public override ClaimsPrincipal Authenticate(string resourceName, ClaimsPrincipal incomingPrincipal)
{
if (!incomingPrincipal.Identity.IsAuthenticated == true &&
_authorizationService.IdentityRegistered(incomingPrincipal.Identity.Name))
{
return base.Authenticate(resourceName, incomingPrincipal);
}
_authorizationService.AddClaimsToIdentity(((ClaimsIdentity) incomingPrincipal.Identity));
Claim tenantClaim = incomingPrincipal.FindFirst(TenantIdClaim);
if (tenantClaim == null)
{
throw new NotSupportedException("Tenant claim not available, role authentication is not supported");
}
string tenantId = tenantClaim.Value;
string authority = String.Format(CultureInfo.InvariantCulture, _aadInstance, _tenant);
Uri servicePointUri = new Uri("https://graph.windows.net");
ClientCredential clientCredential = new ClientCredential(_clientId, _password);
AuthenticationContext authContext = new AuthenticationContext(authority, true);
AuthenticationResult result = authContext.AcquireToken(servicePointUri.ToString(), clientCredential);
Token = result.AccessToken;
ActiveDirectoryClient activeDirectoryClient =
new ActiveDirectoryClient(new Uri(servicePointUri, tenantId),
async () => await AcquireTokenAsync());
IUser user = activeDirectoryClient
.Users
.Where(x => x.UserPrincipalName.Equals(incomingPrincipal.Identity.Name))
.ExecuteAsync()
.Result
.CurrentPage
.ToList()
.FirstOrDefault();
if (user == null)
{
throw new NotSupportedException("Unknown User.");
}
IEnumerable<string> groups = user.GetMemberGroupsAsync(false).Result.ToList();
return incomingPrincipal;
}
I have the same problem. My code is working after changing it according to documentation
https://github.com/AzureADSamples/ConsoleApp-GraphAPI-DotNet
IUserFetcher retrievedUserFetcher = (User) user;
IPagedCollection<IDirectoryObject> pagedCollection = retrievedUserFetcher.MemberOf.ExecuteAsync().Result;
do {
List<IDirectoryObject> directoryObjects = pagedCollection.CurrentPage.ToList();
foreach (IDirectoryObject directoryObject in directoryObjects) {
if (directoryObject is Group) {
Group group = directoryObject as Group;
((ClaimsIdentity)incomingPrincipal.Identity).AddClaim(
new Claim(ClaimTypes.Role, group.DisplayName, ClaimValueTypes.String, "GRAPH"));
}
}
pagedCollection = pagedCollection.GetNextPageAsync().Result;
} while (pagedCollection != null && pagedCollection.MorePagesAvailable);
IEnumerable, string groups = user.GetMemberGroupsAsync(false).Result.ToList() doesn't work since the result is not of type IEnumerable, string.
IEnumerable<string> groups = await user.GetMemberGroupsAsync(false);
Above code would return the correct type.

Resources