I am using ASP.NET Core 2.0, with Azure AD v2.0 endpoint.
I am getting claims like this:
var currentUser = User;
var displayName = currentUser.FindFirst("name").Value;
var claims = currentUser.Claims;
I am not used to using this User to get claims, but could not get the old way with System.Security.Claims to work. So my first question is, is this how I should be getting my claims? And my second question is, how do I add claims to this User?
is this how I should be getting my claims?
AFAIK, you could leverage ControllerBase.HttpContext.User or ControllerBase.User for retrieving the System.Security.Claims.ClaimsPrincipal for current user. Details you could follow the similar issue1 and issue2.
And my second question is, how do I add claims to this User?
As you said you are using ASP.NET Core 2.0, with Azure AD v2.0. I assumed that when using UseOpenIdConnectAuthentication, you could add the additional claims under OnTokenValidated as follows:
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
ClientId = Configuration["AzureAD:ClientId"],
Authority = string.Format(CultureInfo.InvariantCulture, Configuration["AzureAd:AadInstance"], "common", "/v2.0"),
ResponseType = OpenIdConnectResponseType.IdToken,
PostLogoutRedirectUri = Configuration["AzureAd:PostLogoutRedirectUri"],
Events = new OpenIdConnectEvents
{
OnRemoteFailure = RemoteFailure,
OnTokenValidated = TokenValidated
},
TokenValidationParameters = new TokenValidationParameters
{
// Instead of using the default validation (validating against
// a single issuer value, as we do in line of business apps),
// we inject our own multitenant validation logic
ValidateIssuer = false,
NameClaimType = "name"
}
});
private Task TokenValidated(TokenValidatedContext context)
{
/* ---------------------
// Replace this with your logic to validate the issuer/tenant
---------------------
// Retriever caller data from the incoming principal
string issuer = context.SecurityToken.Issuer;
string subject = context.SecurityToken.Subject;
string tenantID = context.Ticket.Principal.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
// Build a dictionary of approved tenants
IEnumerable<string> approvedTenantIds = new List<string>
{
"<Your tenantID>",
"9188040d-6c67-4c5b-b112-36a304b66dad" // MSA Tenant
};
o
if (!approvedTenantIds.Contains(tenantID))
throw new SecurityTokenValidationException();
--------------------- */
var claimsIdentity=(ClaimsIdentity)context.Ticket.Principal.Identity;
//add your custom claims here
claimsIdentity.AddClaim(new Claim("test", "helloworld!!!"));
return Task.FromResult(0);
}
Then, I used the following code to retrieve the user claims:
public IActionResult UserInfo()
{
return Json(User.Claims.Select(c=>new {type=c.Type,value=c.Value}).ToList());
}
Test:
Moreover, you could refer to this sample Integrating Azure AD (v2.0 endpoint) into an ASP.NET Core web app.
Related
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();
}
I have two Asp.Net Core 2.1 applications.
One site on firstdomain.com that should be able to create/register/manage accounts for users on that same domain, as well as create/register/manage user accounts on the site at seconddomain.com.
Creating/registering users from firstdomain.com to be users on firstdomain.com works flawlessly via -
Send email from controller on firstdomain.com including token -
string token = await userManager.GenerateEmailConfirmationTokenAsync(user);
The user receives an email with a button that links to a ConfirmEmail page on firstdomain.com where the token is validated via -
var user = await userManager.FindByIdAsync(userId);
var result = await userManager.ConfirmEmailAsync(user, HttpUtility.UrlDecode(token));
User email is confirmed, they set their password and are logged into the site. on firstdomain.com.
However...
Creating/registering users from firstdomain.com to be users on seconddomain.com is handled in the following way -
Send email from controller on firstdomain.com including token -
string token = await userManager.GenerateEmailConfirmationTokenAsync(user);
The user receives an email with a button that links to a ConfirmEmail page on seconddomain.com where the token is actually marked Invalid via -
var user = await userManager.FindByIdAsync(userId);
var result = await userManager.ConfirmEmailAsync(user, HttpUtility.UrlDecode(token));
Creating/registering users from firstdomain.com to be users on seconddomain.com does not work and the logs show - VerifyUserTokenAsync() failed with purpose: EmailConfirmation for user XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
Both applications are running on the same server.
Relevant Startup.cs code
Both sites use these same settings in their respective Startup.cs -
// aspNet identity setup
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddScoped<ApplicationSignInManager>();
// access User/Identity
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
// set the identity config options with our custom helper
services.Configure<IdentityOptions>
(ApplicationConfigHelper.SetIdentityOptions);
services.ConfigureApplicationCookie(ApplicationConfigHelper.SetCookieOptions);
ApplicationConfigHelper.cs
public static class ApplicationConfigHelper
{
public static object ActionRequest { get; private set; }
public static void SetIdentityOptions(IdentityOptions options)
{
// password setup
options.Password.RequiredLength = 8;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequireDigit = true;
options.Password.RequireLowercase = true;
options.Password.RequireUppercase = true;
options.Password.RequiredUniqueChars = 6;
// lock out settings
options.Lockout.AllowedForNewUsers = true;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
options.Lockout.MaxFailedAccessAttempts = 5;
// user settings
options.User.RequireUniqueEmail = true;
// sign in settings
options.SignIn.RequireConfirmedEmail = true;
options.SignIn.RequireConfirmedPhoneNumber = false;
}
public static void SetCookieOptions(CookieAuthenticationOptions options)
{
options.Cookie.HttpOnly = true;
options.LoginPath = "/Account/Login";
options.AccessDeniedPath = "/Account/AccessDenied";
options.ExpireTimeSpan = TimeSpan.FromDays(1.0);
options.SlidingExpiration = true;
options.Cookie.SameSite = SameSiteMode.Strict;
}
}
ApplicationUser.cs
public class ApplicationUser : IdentityUser
{
public string AccountNumber { get; set; }
public DateTime? Enrolled { get; set; }
/// <summary>
/// Intended to prevent users from spamming the resend invitation email
/// link.
/// </summary>
public DateTime? InvitationExpirationDate { get; set; }
}
Do you want your users to automatically have access to both sites?
The default implementation of token validation uses a per-user random value that will be stored in the database. Are both of your apps storing user information in the same database?
If you don't want to give both apps complete access to the same database, you could implement a replacement IUserSecurityStampStore service to keep track of these random user secrets for both sites. Or derive user "secrets" in a deterministic way by hashing some other secret value that both sites already know, however this has other security implications you will need to consider.
You could also replace the IUserTwoFactorTokenProvider service to have complete control over how tokens are produced to ensure both sites can generate and validate these tokens.
Or go the OAuth route, and generate some form of bearer tokens from firstdomain, that seconddomain can verify.
UPDATE
This ended up being the perfect use-case for JWT.
Here's how I solved this issue -
From a controller on firstdomain.com, when sending a registration link to the user on seconddomain.com, I include a JWT as follows -
// use a jwt token that can be verified on another domain
var token = jsonWebTokenService.GetEncryptedToken(new ClaimsIdentity(GetClaimsList(user.Id)), TimeSpan.FromMinutes(10));
private IList<Claim> GetClaimsList(string id)
{
return new List<Claim>() {
new Claim(ClaimTypes.Name, id),
new Claim(ClaimTypes.UserData, id),
new Claim(ClaimTypes.Role, "ROLESTRING")
};
}
From JsonWebTokenService.cs -
public string GetEncryptedToken(ClaimsIdentity subject, TimeSpan timeSpan)
{
// setup credentials
var signingCreds = new SigningCredentials(signatureKey, SecurityAlgorithms.HmacSha256);
var encryptionCreds = new EncryptingCredentials(encryptionKey,
SecurityAlgorithms.Aes128KW, SecurityAlgorithms.Aes128CbcHmacSha256);
// claims identity for jwt creation
var handler = new JwtSecurityTokenHandler();
var utcNow = DateTime.UtcNow;
var token = handler.CreateJwtSecurityToken(jwtSettings.Issuer, jwtSettings.Issuer, subject,
utcNow, utcNow.AddMilliseconds(timeSpan.TotalMilliseconds), utcNow, signingCreds, encryptionCreds);
return handler.WriteToken(token);
}
*Note - I manually created the signatureKey and encryptionKey with a separate console application I made that utilized secure/tested encryption libraries such as RNGCryptoServiceProvider() and HMACSHA256() and I stored these values securely in Environment Variables on the server for both domains as well as secrets.json for each project.
The registration link is valid for 10 minutes (lifetime of the JWT).
From a controller on seconddomain.com, I verify the JWT -
var user = await userManager.FindByIdAsync(userId);
try
{
var principal = jsonWebTokenService.ProcessEncryptedToken(code);
var name = principal.FindFirstValue(ClaimTypes.Name);
var role = principal.FindFirstValue(ClaimTypes.Role);
if (name != user.Id || role != "ROLESTRING")
throw new ArgumentException("User not found.");
user.EmailConfirmed = true;
await userManager.UpdateAsync(user);
await signInManager.SignOutAsync();
return RedirectToAction("CompleteRegistration", new { userId = userId, code = code });
}
catch (Exception ex)
{
logger.LogWarning(ex, "Failed to confirm email for {UserId}", userId);
return BadRequest();
}
Again, from JsonWebTokenService.cs, I validate the JWT -
public ClaimsPrincipal ProcessEncryptedToken(string token)
{
var tokenHandler = new JwtSecurityTokenHandler();
var validationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateIssuer = true,
IssuerSigningKey = signatureKey,
ValidIssuer = jwtSettings.Issuer,
ValidateAudience = true,
ValidAudience = jwtSettings.Issuer,
TokenDecryptionKey = encryptionKey,
ClockSkew = TimeSpan.Zero
};
// throws exceptions if the token is no longer valid
return tokenHandler
.ValidateToken(token, validationParameters, out SecurityToken validatedToken);
}
When the token is successfully validated, the user is redirected to complete their registration.
I hope this helps someone else who might be stuck on a similar issue. There is a ton of information on JWTs if you do your research. But, it took me hours to conjure this one up as there was not much info on using JWTs for email registration verification. JWTs are often used for authentication AFTER a user has already entered valid credentials on a site. This solution has provided a way for us to validate email registrations across domains as an alternative to the ASP.NET Core Identity UserManager.
I'm using the Okta example for implementing OpenIdConnect in an Asp.NET 4.6.x MVC web application. The application uses Unity for Dependency Injection and one of the dependencies is a custom set of classes for the Identity Framework. I'm not using the Okta API because the IdP is not actually Okta and I'm assuming there's proprietary stuff in it. So it's all .NET standard libraries for the OpenId portions.
I can walk through the code after clicking login and it will carry me to the IdP and I can log in with my account, and then it will bring me back and I can see all of the information from them for my login. But it doesn't log me in or anything as it does in the example from Okta's GitHub.
Basically I'm wondering if the identity customization is what's interfering with the login and if there's a way to get in the middle of that and specify what I need it to do?
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions {
ClientId = clientId
, ClientSecret = clientSecret
, Authority = authority
, RedirectUri = redirectUri
, AuthenticationMode = Microsoft.Owin.Security.AuthenticationMode.Passive
, ResponseType = OpenIdConnectResponseType.CodeIdToken
, Scope = OpenIdConnectScope.OpenIdProfile
, PostLogoutRedirectUri = postLogoutRedirectUri
, TokenValidationParameters = new TokenValidationParameters { NameClaimType = "name" }
, Notifications = new OpenIdConnectAuthenticationNotifications {
AuthorizationCodeReceived = async n =>
{
//var tokenClient = new TokenClient($"{authority}/oauth2/v1/token", clientId, clientSecret);
var tokenClient = new TokenClient($"{authority}/connect/token", clientId, clientSecret);
var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(n.Code, redirectUri);
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
//var userInfoClient = new UserInfoClient($"{authority}/oauth2/v1/userinfo");
var userInfoClient = new UserInfoClient($"{authority}/connect/userinfo");
var userInfoResponse = await userInfoClient.GetAsync(tokenResponse.AccessToken);
var claims = new List<System.Security.Claims.Claim>();
claims.AddRange(userInfoResponse.Claims);
claims.Add(new System.Security.Claims.Claim("id_token", tokenResponse.IdentityToken));
claims.Add(new System.Security.Claims.Claim("access_token", tokenResponse.AccessToken));
if (!string.IsNullOrEmpty(tokenResponse.RefreshToken))
{
claims.Add(new System.Security.Claims.Claim("refresh_token", tokenResponse.RefreshToken));
}
n.AuthenticationTicket.Identity.AddClaims(claims);
return;
}
, RedirectToIdentityProvider = n =>
{
// If signing out, add the id_token_hint
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout)
{
var idTokenClaim = n.OwinContext.Authentication.User.FindFirst("id_token");
if (idTokenClaim != null)
{
n.ProtocolMessage.IdTokenHint = idTokenClaim.Value;
}
}
return Task.CompletedTask;
}
}
});
The token(s) returned by Okta have to be managed by your application in order to perform the login action. The OIDC token returned will need to be verified and validated by you, and then a decision made as to whether to accept the OIDC token. If so, you take action to log the user into your application. Recieving an OIDC token as a result of an OpenID Connect flow doesn't by itself log you into an app. The app needs to do some more work based on the token content before taking a login or reject action.
In my .net core 1.1 code I'm doing the authentication as follows(which is sending the bearer token to external URL and look for claims in the return token). This code is in Configure method
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationScheme = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
AuthenticationScheme = "oidc",
SignInScheme = "Cookies",
Authority = signinAuthority,
RequireHttpsMetadata = signinHTTPS,
ClientId = "skybus",
ClientSecret = "secret",
ResponseType = "code id_token",
Scope = { "api1", "offline_access" },
GetClaimsFromUserInfoEndpoint = true,
SaveTokens = true
});
Now I upgraded my code to .net Core 2.0 the both UseCookieAuthentication & UseOpenIdConnectAuthentication are changed. I'm finding it difficult to find what needs to be done in this case
What I changed it to is as follows in ConfigureServices method
services.AddAuthentication(options => {
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(o =>
{
o.Authority = signinAuthority;
o.SignInScheme = "Cookies";
o.RequireHttpsMetadata = signinHTTPS;
o.ClientId = "skybus";
o.ClientSecret = "secret";
o.ResponseType = "code id_token";
o.GetClaimsFromUserInfoEndpoint = true;
o.SaveTokens = true;
o.Scope.Add("api1");
o.Scope.Add("offline_access");
});
In Browser I see this URL after the above changes. It should either show me the external login page if user is not logged in or return to home page of my website
http://localhost:5000/Account/Login?ReturnUrl=%2F
I followed this link from microsoft to start my migration. Most of the migration is covered by the this link, but I faced issue where most of my claims are missing.
With the ASP.NET Core 1.x, client would have received the claims: nbf, exp, iss, aud, nonce, iat, c_hash, sid, sub, auth_time, idp, amr.
In Core 2.0 we only get sid, sub and idp. What happened?
Microsoft added a new concept to their OpenID Connect handler called ClaimActions. Claim actions allow modifying how claims from an external provider are mapped (or not) to a claim in your ClaimsPrincipal. Looking at the ctor of the OpenIdConnectOptions, you can see that the handler will now skip the following claims by default:
ClaimActions.DeleteClaim("nonce");
ClaimActions.DeleteClaim("aud");
ClaimActions.DeleteClaim("azp");
ClaimActions.DeleteClaim("acr");
ClaimActions.DeleteClaim("amr");
ClaimActions.DeleteClaim("iss");
ClaimActions.DeleteClaim("iat");
ClaimActions.DeleteClaim("nbf");
ClaimActions.DeleteClaim("exp");
ClaimActions.DeleteClaim("at_hash");
ClaimActions.DeleteClaim("c_hash");
ClaimActions.DeleteClaim("auth_time");
ClaimActions.DeleteClaim("ipaddr");
ClaimActions.DeleteClaim("platf");
ClaimActions.DeleteClaim("ver");
If you want to “un-skip” a claim, you need to delete a specific claim action when setting up the handler. The following is the very intuitive syntax to get the amr claim back:
options.ClaimActions.Remove("amr");
Requesting more claims from the OIDC provider
When you are requesting more scopes, e.g. profile or custom scopes that result in more claims, there is another confusing detail to be aware of.
Depending on the response_type in the OIDC protocol, some claims are transferred via the id_token and some via the userinfo endpoint.
So first of all, you need to enable support for the userinfo endpoint in the handler:
options.GetClaimsFromUserInfoEndpoint = true;
In the end you need to add the following class to import all other custom claims
public class MapAllClaimsAction : ClaimAction
{
public MapAllClaimsAction() : base(string.Empty, string.Empty)
{
}
public override void Run(JObject userData, ClaimsIdentity identity, string issuer)
{
foreach (var claim in identity.Claims)
{
// If this claimType is mapped by the JwtSeurityTokenHandler, then this property will be set
var shortClaimTypeName = claim.Properties.ContainsKey(JwtSecurityTokenHandler.ShortClaimTypeProperty) ?
claim.Properties[JwtSecurityTokenHandler.ShortClaimTypeProperty] : string.Empty;
// checking if claim in the identity (generated from id_token) has the same type as a claim retrieved from userinfo endpoint
JToken value;
var isClaimIncluded = userData.TryGetValue(claim.Type, out value) || userData.TryGetValue(shortClaimTypeName, out value);
// if a same claim exists (matching both type and value) both in id_token identity and userinfo response, remove the json entry from the userinfo response
if (isClaimIncluded && claim.Value.Equals(value.ToString(), StringComparison.Ordinal))
{
if (!userData.Remove(claim.Type))
{
userData.Remove(shortClaimTypeName);
}
}
}
// adding remaining unique claims from userinfo endpoint to the identity
foreach (var pair in userData)
{
JToken value;
var claimValue = userData.TryGetValue(pair.Key, out value) ? value.ToString() : null;
identity.AddClaim(new Claim(pair.Key, claimValue, ClaimValueTypes.String, issuer));
}
}
}
and then use the above class code to add it to ClaimActions
options.ClaimActions.Add(new MapAllClaimsAction());
I don't know if you are using IdentityServer4 but they have provided some samples on github for ASP.NET Core 2.0.
Hope this helps you.
Sample Project
Did you take the authentication middleware into use?
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAuthentication();
...
I ran the ADAL JS sample SPA project from Github against my Azure AD.
That works well, but I want to add claims to the token after authentication.
In the SPA sample, you add middle-ware as follows:
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Audience = ConfigurationManager.AppSettings["ida:Audience"],
Tenant = ConfigurationManager.AppSettings["ida:Tenant"]
});
From here, do you have to add additional OAuth middleware to get access to something like Notifications to get to the ClaimsIdentity and AddClaim?
You can use the TokenValidationParamenters. See ValidateToken or TokenValidationParameters.CreateClaimsIdentity
I found a great sample that handles exactly this... the magic happens inside Provider = new OAuthBearerAuthenticationProvider.
You see that additional claims are added to identity.
// Add bearer token authentication middleware.
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
{
// This is where the magic happens. In this handler we can perform additional
// validations against the authenticated principal or modify the principal.
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);
// Credential representing our client application in Azure AD.
var clientCredential = new ClientCredential(clientId, applicationKey);
// Get a token on behalf of the current user that lets Azure AD Graph API access
// our Azure AD tenant.
var authenticationResult = await authenticationContext.AcquireTokenAsync(
azureGraphApiUrl, clientCredential, userAssertion).ConfigureAwait(false);
// Create Graph API client and give it the acquired token.
var activeDirectoryClient = new ActiveDirectoryClient(
graphApiServiceRootUrl, () => Task.FromResult(authenticationResult.AccessToken));
// Get current user groups.
var pagedUserGroups =
await activeDirectoryClient.Me.MemberOf.ExecuteAsync().ConfigureAwait(false);
do
{
// Collect groups and add them as role claims to our current principal.
var directoryObjects = pagedUserGroups.CurrentPage.ToList();
foreach (var directoryObject in directoryObjects)
{
var group = directoryObject as Group;
if (group != null)
{
// Add ObjectId of group to current identity as role claim.
identity.AddClaim(new Claim(identity.RoleClaimType, group.ObjectId));
}
}
pagedUserGroups = await pagedUserGroups.GetNextPageAsync().ConfigureAwait(false);
} while (pagedUserGroups != null);
}
catch (Exception e)
{
throw;
}
}
}
});