I am using Azure AD to authenticate the users. I want to add few user claims specific to my application. Should I do it in Application_PostAuthenticateRequest` in global.asax ?. Is there a way I can cache my claims too ?
If you are using the ASP.NET OWIN middleware, there are specific notifications you can use for that purpose. Claims added in that way will end up in your session cookie, so that you won't have to repeat the claims augmentation logic in subsequent calls. See http://www.cloudidentity.com/blog/2015/08/26/augmenting-the-set-of-incoming-claims-with-the-openid-connect-and-oauth2-middleware-in-katana-3-x/ for details.
BTW you can add your custom cliams but you cannot override the existing claims added by the Azure AD (what i have seen so far might be i am wrong). what you can do is to add the new cliams like this
AuthorizationCodeReceived = context =>
{
List<System.Security.Claims.Claim> allcustomClaims = new List<System.Security.Claims.Claim>();
allcustomClaims.Add(new System.Security.Claims.Claim("customClaim", "YourDefindedValue"));
context.AuthenticationTicket.Identity.AddClaims(allcustomClaims);
return Task.FromResult(0);
}`
and then you can get the claim anywhere in controller like
#{
var claimsIdentity = User.Identity as System.Security.Claims.ClaimsIdentity;
if (claimsIdentity != null)
{
var c = claimsIdentity.FindFirst("customClaim").Value;
}
}
You can augment the claims programmatically like this:
public async Task<ActionResult> AuthenticateAsync()
{
ClaimsPrincipal incomingPrincipal = System.Threading.Thread.CurrentPrincipal as ClaimsPrincipal;
if (incomingPrincipal != null && incomingPrincipal.Identity.IsAuthenticated == true)
{
ClaimsIdentity claimsIdentity = incomingPrincipal.Identity as ClaimsIdentity;
if (!claimsIdentity.HasClaim(ClaimTypes.Role, "Admin"))
{
claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, "Admin", ClaimValueTypes.String, "AADGuide"));
var ctx = Request.GetOwinContext();
var authenticationManager = ctx.Authentication;
AuthenticateResult authResult = await authenticationManager.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationType);
authenticationManager.SignIn(authResult.Properties,claimsIdentity);
}
}
return RedirectToAction("Index", "Start");
}
This solution relies on AuthenticationAsync method of AuthenticationManager to retrieve the original AuthenticationProperties. After retrieving the properties, call the SignIn method to persist the new ClaimsIdentity in the auth cookie.
If you're making use of:
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
...
This is how I managed to add additional custom claims using new OAuthBearerAuthenticationProvider:
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
// The id of the client application that must be registered in Azure AD.
TokenValidationParameters = new TokenValidationParameters { ValidAudience = clientId },
// Our Azure AD tenant (e.g.: contoso.onmicrosoft.com).
Tenant = tenant,
Provider = new OAuthBearerAuthenticationProvider
{
// In this handler we can perform additional coding tasks...
OnValidateIdentity = async context =>
{
try
{
// Retrieve user JWT token from request.
var authorizationHeader = context.Request.Headers["Authorization"].First();
var userJwtToken = authorizationHeader.Substring("Bearer ".Length).Trim();
// Get current user identity from authentication ticket.
var authenticationTicket = context.Ticket;
var identity = authenticationTicket.Identity;
// Credential representing the current user. We need this to request a token
// that allows our application access to the Azure Graph API.
var userUpnClaim = identity.FindFirst(ClaimTypes.Upn);
var userName = userUpnClaim == null
? identity.FindFirst(ClaimTypes.Email).Value
: userUpnClaim.Value;
var userAssertion = new UserAssertion(
userJwtToken, "urn:ietf:params:oauth:grant-type:jwt-bearer", userName);
identity.AddClaim(new Claim(identity.RoleClaimType, "myRole"));
}
catch (Exception e)
{
throw;
}
}
}
});
For a full sample, check this blog post.
Related
I am running a ADB2C tenant and would like to know how to configure and retrieve user groups within a auth token in the ADB2C auth flow.
I am able to configure and receive the custom attribute in my token but I am unable to configure groups claim that can potentially list user membership to a certain group(s).
expected is a groups:{}
In the B2C, there is no group claim that will returns in the token, so you cannot follow the same way in the Azure AD.
You could try to let the application manually retrieve these claims the group claims and inject them into the token.
For this way, you could refer to the method in Azure AD:
var authority = $"https://login.microsoftonline.com/{Tenant}";
var graphCca = new ConfidentialClientApplication(GraphClientId, authority, GraphRedirectUri, new ClientCredential(GraphClientSecret), userTokenCache, null);
string[] scopes = new string[] { "https://graph.microsoft.com/.default" };
try
{
AuthenticationResult authenticationResult = await graphCca.AcquireTokenForClientAsync(scopes);
string token = authenticationResult.AccessToken;
using (var client = new HttpClient())
{
string requestUrl = $"https://graph.microsoft.com/v1.0/users/{signedInUserID}/memberOf?$select=displayName";
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUrl);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
HttpResponseMessage response = await client.SendAsync(request);
var responseString = await response.Content.ReadAsStringAsync();
var json = JObject.Parse(responseString);
foreach (var group in json["value"])
notification.AuthenticationTicket.Identity.AddClaim(new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Role, group["displayName"].ToString(), System.Security.Claims.ClaimValueTypes.String, "Graph"));
//TODO: Handle paging.
// https://developer.microsoft.com/en-us/graph/docs/concepts/paging
// If the user is a member of more than 100 groups,
// you'll need to retrieve the next page of results.
}
} catch (Exception ex)
{
//TODO: Handle
throw;
}
I want to set an expiry time for my web app so that after 1 hour the user will automatically be logged out. It seems that azure now have a new feature called 'conditional access'. Firstly I have to have a premium account (so yet more money) and secondly I cant find anything in the documentation that shows how to use it to log someone out after a specified time. Has anyone used this feature yet ? how can this be done ?
can anyone help ?
Assuming that you are using OpenID Connect and Cookie authentication middleware to protect your web app, for your requirement, I assumed that you could add a custom claim named loggedTicks and check the time interval under the OnValidateIdentity of CookieAuthenticationProvider, then explicitly invoke the sign out operation against your web application and AAD. Here is the code snippet, you could refer to it:
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions() {
ExpireTimeSpan=TimeSpan.MaxValue,
Provider = new CookieAuthenticationProvider()
{
OnValidateIdentity = ctx => {
var loggedClaim=ctx.Identity.FindFirst("loggedTicks")?.Value;
if (loggedClaim != null)
{
var loggedDateTime = new DateTime(long.Parse(loggedClaim), DateTimeKind.Utc);
if (loggedDateTime.AddHours(1) < DateTime.UtcNow)
{
ctx.RejectIdentity();
ctx.OwinContext.Authentication.SignOut(OpenIdConnectAuthenticationDefaults.AuthenticationType, CookieAuthenticationDefaults.AuthenticationType);
}
}
return Task.FromResult(0);
}
}
});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
PostLogoutRedirectUri = postLogoutRedirectUri,
RedirectUri = postLogoutRedirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = context =>
{
context.HandleResponse();
context.Response.Redirect("/Error?message=" + context.Exception.Message);
return Task.FromResult(0);
},
SecurityTokenValidated = async (x) =>
{
var identity = x.AuthenticationTicket.Identity;
//add a additional claim which represents the current user logged UTC time ticks
identity.AddClaim(new System.Security.Claims.Claim("loggedTicks", DateTime.UtcNow.Ticks.ToString()));
await Task.FromResult(0);
}
}
});
}
I have a set of claims. I'm using it to create a ClaimsIdentity. I also use OWIN to signin the identity. In addition, I'm adding it to the ClaimsPrincipal.Current.Identities. Here is my code...
[HttpPost]
[AllowAnonymous]
public async Task<ActionResult> LogonCallBack()
{
var token = Request.Params["id_token"];
var validatedToken = TokenService.ValidateIdToken(token);
var identity = new ClaimsIdentity(validatedToken.Claims);
HttpContext.GetOwinContext().Authentication.SignIn(new AuthenticationProperties { IsPersistent = false }, identity);
ClaimsPrincipal.Current.AddIdentity(identity);
return RedirectToAction("Display");
//return RedirectToAction("Index", "Error", new { area = "Token Validate Failed." });
}
When debugging, I see that the set of claims that i am retrieving are coming across fine. And I can create the ClaimsIdentity. However, after this, when I am redirected to the Display page, the User.Identity.IsAuthenticated is still false. ClaimsPrincipal.Current does not have the added identity in its list.
How am I able to get the user to be authenticated?
You should use SecurityHelper to Add an Identity to Current User.
var owinContext = HttpContext.GetOwinContext();
var securityHelper = new Microsoft.Owin.Security.Infrastructure.SecurityHelper(owinContext);
securityHelper.AddUserIdentity(identity);
I added the cookietype string to the identity declaration:
var identity = new ClaimsIdentity(validatedToken.Claims, DefaultAuthenticationTypes.ApplicationCookie);
And I also added the same string to the middleware pipeline as follows:
app.SetDefaultSignInAsAuthenticationType(DefaultAuthenticationTypes.ApplicationCookie);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/OIDC/Authenticate")
});
I have got a basic Identity Server setup as per the UI sample project instructions on GitHub. I have it set it up to use Windows authentication with our on site AD. This is working beautifully.
My issue is with adding the users AD groups to the claims. As per the sample project I have enabled the IncludeWindowsGroups option. Which seems to be adding the claims to the ClaimsIdentity. However, on my MVC client, when I print out the claims I only ever get the same 4. They are sid, sub, idp and name. I have tried adding other claims but I can never get any others to show up.
I have the following as my Client Setup:
return new List<Client>
{
// other clients omitted...
// OpenID Connect implicit flow client (MVC)
new Client
{
ClientId = "mvc",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.Implicit,
// where to redirect to after login
RedirectUris = { "http://localhost:5002/signin-oidc" },
// where to redirect to after logout
PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile
},
RequireConsent = false
}
};
Hopefully I am just missing something simple but I am struggling for ideas now, so any pointers would be much appreciated.
I managed to get this working with a few changes, beyond setting IncludeWindowsGroups = true in the IdentityServer4 project. Note that I downloaded the IdentityServer4 UI quickstart as of the 2.2.0 tag
Per this comment in GitHub, I modified ExternalController.cs in the quickstart UI:
// this allows us to collect any additonal claims or properties
// for the specific prtotocols used and store them in the local auth cookie.
// this is typically used to store data needed for signout from those protocols.
var additionalLocalClaims = new List<Claim>();
var roleClaims = claims.Where(c => c.Type == JwtClaimTypes.Role).ToList();
if (roleClaims.Count > 0)
{
additionalLocalClaims.AddRange(roleClaims);
}
I then created a profile service to copy the claims from Windows Auth into the token being sent back:
public class ProfileService : IProfileService
{
private readonly string[] _claimTypesToMap = {"name", "role"};
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
foreach (var claimType in _claimTypesToMap)
{
var claims = context.Subject.Claims.Where(c => c.Type == claimType);
context.IssuedClaims.AddRange(claims);
}
return Task.CompletedTask;
}
public Task IsActiveAsync(IsActiveContext context)
{
context.IsActive = true; //use some sort of actual validation here!
return Task.CompletedTask;
}
}
and registered with IdentityServer4 in Startup.cs
services
.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryIdentityResources(StaticConfig.GetIdentityResources())
.AddInMemoryApiResources(StaticConfig.GetApiResources())
.AddInMemoryClients(StaticConfig.GetClients())
.AddTestUsers(StaticConfig.GetUsers())
.AddProfileService<ProfileService>();
In my client config in IdentityServer4, I set user claims to be included in the Id token. I found that if I tried to map the claims in the callback to UserInfo, that context was lost in IdentityServer4, so the claims wouldn't map.
public static class StaticConfig
{
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
new Client
{
...
AlwaysIncludeUserClaimsInIdToken = true,
...
}
}
}
}
Finally, in Startup.cs for the client website, I did not setup the UserInfo callback; I just made sure that my name and role claims were mapped. Note that if your profile service returns any other claim types, you need to manually map them with a call to a helper method on options.ClaimActions.
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ClientId = "mvc";
options.ClientSecret = "secret";
options.SaveTokens = true;
options.ResponseType = "code id_token";
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
//map any other app-specific claims we're getting from IdentityServer
options.ClaimActions.MapUniqueJsonKey("someotherclaimname", "someotherclaimname");
};
Hi I started with the sample here
https://github.com/Azure-Samples/active-directory-b2c-dotnet-webapp-and-webapi
And manqaged to get the TaskWebApp talking to the TaskService.
I now want to extend the solution to have a 3rd service
Im not sure if the problem is within the ConfidentialClientApplication as when it fails to get my tokens i get no exception.
I think the issue is the COnfigureApp method in my TaskWebApp is only expecting to manage tokens from the single TokenService.
Here is my web App Starttup.Auth ConfigureAuth method, this is unchanged from the sample app
public void ConfigureAuth(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
// Generate the metadata address using the tenant and policy information
MetadataAddress = String.Format(AadInstance, Tenant, DefaultPolicy),
// These are standard OpenID Connect parameters, with values pulled from web.config
ClientId = ClientId,
RedirectUri = RedirectUri,
PostLogoutRedirectUri = RedirectUri,
// Specify the callbacks for each type of notifications
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = OnRedirectToIdentityProvider,
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
AuthenticationFailed = OnAuthenticationFailed,
},
// Specify the claims to validate
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name"
},
// Specify the scope by appending all of the scopes requested into one string (seperated by a blank space)
Scope = $"{OpenIdConnectScopes.OpenId} {ReadTasksScope} {WriteTasksScope}"
}
);
}
Here is my OnAuthorizationCodeReceived method, again unchanged
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification notification)
{
// Extract the code from the response notification
var code = notification.Code;
var userObjectId = notification.AuthenticationTicket.Identity.FindFirst(ObjectIdElement).Value;
var authority = String.Format(
AadInstance,
Tenant,
DefaultPolicy);
var httpContext = notification.OwinContext.Environment["System.Web.HttpContextBase"] as HttpContextBase;
// Exchange the code for a token. Make sure to specify the necessary scopes
ClientCredential cred = new ClientCredential(ClientSecret);
ConfidentialClientApplication app = new ConfidentialClientApplication(
authority,
Startup.ClientId,
RedirectUri,
cred,
new NaiveSessionCache(userObjectId, httpContext));
var authResult = await app.AcquireTokenByAuthorizationCodeAsync(new string[]
{
ReadTasksScope,
WriteTasksScope
},
code,
DefaultPolicy);
}
The problem seems to be that my Readand write scope are referencing the TaskService directly and when i try to ask for appropriate scopes from other apps it complains.
in my controller
private async Task<String> acquireToken(String[] scope)
{
try
{
string userObjectID = ClaimsPrincipal.Current.FindFirst(Startup.ObjectIdElement).Value;
string authority = String.Format(Startup.AadInstance, Startup.Tenant, Startup.DefaultPolicy);
ClientCredential credential = new ClientCredential(Startup.ClientSecret);
// Retrieve the token using the provided scopes
ConfidentialClientApplication app = new ConfidentialClientApplication(authority, Startup.ClientId,
Startup.RedirectUri, credential,
new NaiveSessionCache(userObjectID, this.HttpContext));
AuthenticationResult result = await app.AcquireTokenSilentAsync(scope);
return result.Token;
}
catch (Exception e)
{
Debug.WriteLine(e.Message);
throw e;
}
This method is used but My AuthorisationResult fails with the message couldnt get token silently with no inner exception.
So far I have tried removing the NaiveSessionCache as its optional and it still fails
To get past this I have registered a 4th service and then made my webapis use that single client id and secret to control access to their resources but this feels like it is not the correct way forward.
Any help would be great thanks