Some users lose their custom claims after page reload - c#

I am using .NET Core, with Azure Active Directory authentication through my free Azure account. The goal is for the user to log in, my web app to poll the database and figure out what access they have, and add that access into claims. Then, when a page is reloaded, I can use .NET Core's new Policies or order to validate the user.
This seems to work perfectly for my Microsoft account, however, when I try to use an account that I created in my Azure Active Directory, the claims are lost as soon as the user tries to change pages. I've verified via debugging that the claims are added after login, and are there on the initial front-page load, but are lost as soon as the user changes pages or refreshes.
So, in my ConfigureServices method in Startup.cs I have:
services.AddAuthorization(options =>
{
foreach(var accessPolicy in AccessPolicy.GetAllPolicies())
{
options.AddPolicy(accessPolicy, policy => policy.Requirements.Add(new CanAccessRequirement(accessPolicy)));
}
});
services.AddSingleton<IAuthorizationHandler, CanAccessHandler>();
And, in the Configure method I have:
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
ClientId = ApiLogin.ClientId,
ClientSecret = ApiLogin.ClientSecret,
Authority = ApiLogin.Authority,
PostLogoutRedirectUri = Configuration["AzureAd:PostLogoutRedirectUri"],
ResponseType = OpenIdConnectResponseType.CodeIdToken,
CallbackPath = Configuration["Authentication:AzureAd:CallbackPath"],
GetClaimsFromUserInfoEndpoint = false,
Events = new OpenIdConnectEvents()
{
OnRemoteFailure = OnAuthenticationFailed,
OnAuthorizationCodeReceived = OnAuthorizationCodeReceived
}
});
private Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedContext context)
{
if (!Session.Keys.Contains(Constants.AUTH_TOKEN))
{
var email = user.FindFirst(ClaimTypes.Email)?.Value ?? user.Identity.Name;
if (email == null || !email.Contains('#')) return;
// This is the security token needed to talk to the API.
// Putting it as a magic value, since it doesn't have anything
// to do with the current problem
string token = "123";
Session.Set(Constants.AUTH_TOKEN, Encoding.ASCII.GetBytes(token));
// Add the claims
var claims = ApiLogin.LoginUser(token, email) ?? new List<string> { };
(user.Identity as ClaimsIdentity).AddClaims(claims.Select(p => new Claim(p.ToString(), "True")));
}
return Task.FromResult(0);
}
Finally, in my controllers, I have lines like this above my classes, where AccessPolicy just holds constant strings.
[Authorize(Policy = AccessPolicy.EditInvoicePrivilege)]
I cannot figure out why this works for me on my #hotmail account but does not work when I try to use my #[TenantId] account.
Am I doing something wrong with the claims? And if so, why does it work for one account, and not the other?
Please let me know if you need any more information. Unfortunately, there's a lot of code, so recreating with a minimal amount of code is still a lot of code.

Related

How do I get Azure Active Directory App Roles to show in Claims?

I'm using Azure Active Directory to provide authentication to the Backoffice on my website running Umbraco version 11.0.
This is working nicely and I can log in but I want to improve the experience by using app roles within Azure to manage the user's group within Umbraco.
My Azure setup
I've created an App Registration within Azure with the following configuration:
Added a Redirection URI:
URI: https://localhost:44391/umbraco-signin-microsoft/
Enabled Access tokens (used for implicit flows)
Enabled ID tokens (used for implicit and hybrid flows)
Supported account types: Accounts in this organizational directory only (Example only - Single tenant)
Added App Roles
Administrator
Editor
In Enterprise Applications, I've also added the App Roles above to my users:
My code
Login Provider
namespace Example.Api.Features.Authentication.Extensions;
public static class UmbracoBuilderExtensions
{
public static IUmbracoBuilder ConfigureAuthentication(this IUmbracoBuilder builder)
{
builder.Services.ConfigureOptions<OpenIdConnectBackOfficeExternalLoginProviderOptions>();
builder.AddBackOfficeExternalLogins(logins =>
{
const string schema = MicrosoftAccountDefaults.AuthenticationScheme;
logins.AddBackOfficeLogin(
backOfficeAuthenticationBuilder =>
{
backOfficeAuthenticationBuilder.AddMicrosoftAccount(
// the scheme must be set with this method to work for the back office
backOfficeAuthenticationBuilder.SchemeForBackOffice(OpenIdConnectBackOfficeExternalLoginProviderOptions.SchemeName) ?? string.Empty,
options =>
{
//By default this is '/signin-microsoft' but it needs to be changed to this
options.CallbackPath = "/umbraco-signin-microsoft/";
//Obtained from the AZURE AD B2C WEB APP
options.ClientId = "CLIENT_ID";
//Obtained from the AZURE AD B2C WEB APP
options.ClientSecret = "CLIENT_SECRET";
options.TokenEndpoint = $"https://login.microsoftonline.com/TENANT/oauth2/v2.0/token";
options.AuthorizationEndpoint = $"https://login.microsoftonline.com/TENANT/oauth2/v2.0/authorize";
});
});
});
return builder;
}
}
Auto-linking accounts
namespace Example.Api.Features.Configuration;
public class OpenIdConnectBackOfficeExternalLoginProviderOptions : IConfigureNamedOptions<BackOfficeExternalLoginProviderOptions>
{
public const string SchemeName = "OpenIdConnect";
public void Configure(string name, BackOfficeExternalLoginProviderOptions options)
{
if (name != "Umbraco." + SchemeName)
{
return;
}
Configure(options);
}
public void Configure(BackOfficeExternalLoginProviderOptions options)
{
options.AutoLinkOptions = new ExternalSignInAutoLinkOptions(
// must be true for auto-linking to be enabled
autoLinkExternalAccount: true,
// Optionally specify default user group, else
// assign in the OnAutoLinking callback
// (default is editor)
defaultUserGroups: new[] { Constants.Security.EditorGroupAlias },
// Optionally you can disable the ability to link/unlink
// manually from within the back office. Set this to false
// if you don't want the user to unlink from this external
// provider.
allowManualLinking: false
)
{
// Optional callback
OnAutoLinking = (autoLinkUser, loginInfo) =>
{
// You can customize the user before it's linked.
// i.e. Modify the user's groups based on the Claims returned
// in the externalLogin info
autoLinkUser.IsApproved = true;
},
OnExternalLogin = (user, loginInfo) =>
{
// You can customize the user before it's saved whenever they have
// logged in with the external provider.
// i.e. Sync the user's name based on the Claims returned
// in the externalLogin info
return true; //returns a boolean indicating if sign in should continue or not.
}
};
// Optionally you can disable the ability for users
// to login with a username/password. If this is set
// to true, it will disable username/password login
// even if there are other external login providers installed.
options.DenyLocalLogin = true;
// Optionally choose to automatically redirect to the
// external login provider so the user doesn't have
// to click the login button. This is
options.AutoRedirectLoginToExternalProvider = true;
}
}
In this file, I'd ideally do as the comment says and i.e. Modify the user's groups based on the Claims returned in the externalLogin info.
Also registered in my Startup file
services.AddUmbraco(_env, _config)
.AddBackOffice()
.AddWebsite()
.AddComposers()
.ConfigureAuthentication()
.Build();
I've attempted to give the following permissions to the application, with no luck:
Current state of play is that I can login just fine but if I debug externalInfo, there's nothing in there about the users having either the Administrator or Editor App Role as configured above.
My gut feeling is that I'm missing something with the Azure Active Directory setup but I've tried a few different configurations and can't seem to get the App Roles to come back.
Thanks,
Ben
EDIT - 15.02.2023:
I can see that the roles come back when I hit the https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token endpoint using client_credentials as the grant_type. It looks like the .NET application using authorization_code instead. I've decoded the token retrieved from this and it doesn't contain the roles.
I wonder if there's some kind of configuration on the .NET application that allows me to add the roles.
To get App Roles in token claims, you can use client credentials flow to generate access tokens by granting admin consent.
I tried to reproduce the same in my environment via Postman and got the below results:
I registered one Azure AD web application and created App roles like below:
Now I assigned these App roles to users under it's Enterprise application like below:
Add these App roles in API permissions of application like below:
You can see App roles under Application permissions like below:
Make sure to grant admin consent to above permissions like this:
While generating access token, scope should be your Application ID URI ending with /.default
Now, I generated access token using client credentials flow via Postman with below parameters:
POST https://login.microsoftonline.com/<tenantID>/oauth2/v2.0/token
client_id: <appID>
grant_type:client_credentials
scope: api://<appID>/.default
client_secret: secret
Response:
When I decoded the above token in jwt.ms, I got App roles in roles claim successfully like below:
Note that App roles are Application permissions that will work only with flows like client credentials which do not involve user interaction.
So, if you use delegated flows like authorization code flow, username password flow, etc..., you won't get App roles in token claims.
UPDATE:
You can use below c# code in getting access token from client credentials flow like this:
using Microsoft.Identity.Client;
var clientID = "bbb739ad-98a4-4566-8408-dxxxxxxxx3b";
var clientSecret = "K.k8Q~hwtxxxxxxxxxxxxxxxU";
var tenantID = "fb134080-e4d2-45f4-9562-xxxxxx";
var authority = $"https://login.microsoftonline.com/{tenantID}";
var clientApplication = ConfidentialClientApplicationBuilder.Create(clientID)
.WithClientSecret(clientSecret)
.WithAuthority(authority)
.Build();
var scopes = new string[] { "api://bbb739ad-98a4-4566-8408-xxxxxx/.default" };
var authenticationResult = await clientApplication.AcquireTokenForClient(scopes)
.ExecuteAsync()
.ConfigureAwait(false);
var accesstoken = authenticationResult.AccessToken;
Console.WriteLine(accesstoken);
Response:
When I decoded the above token, it has roles claim with App roles like below:
To solve this, I ended up swapping out the AddMicrosoftAccount AuthenticationBuilder in favour of AddOpenIdConnect. This appears to respect the claims in the tokens.
This is the code I am now using in the ConfigureAuthentication method.
public static IUmbracoBuilder ConfigureAuthentication(this IUmbracoBuilder builder)
{
// Register OpenIdConnectBackOfficeExternalLoginProviderOptions here rather than require it in startup
builder.Services.ConfigureOptions<OpenIdConnectBackOfficeExternalLoginProviderOptions>();
builder.AddBackOfficeExternalLogins(logins =>
{
logins.AddBackOfficeLogin(
backOfficeAuthenticationBuilder =>
{
backOfficeAuthenticationBuilder.AddOpenIdConnect(
// The scheme must be set with this method to work for the back office
backOfficeAuthenticationBuilder.SchemeForBackOffice(OpenIdConnectBackOfficeExternalLoginProviderOptions.SchemeName),
options =>
{
options.CallbackPath = "/umbraco-signin-microsoft/";
// use cookies
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
// pass configured options along
options.Authority = "https://login.microsoftonline.com/{tenantId}/v2.0";
options.ClientId = "{clientId}";
options.ClientSecret = "{clientSecret}";
// Use the authorization code flow
options.ResponseType = OpenIdConnectResponseType.Code;
options.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet;
// map claims
options.TokenValidationParameters.NameClaimType = "name";
options.TokenValidationParameters.RoleClaimType = "role";
options.RequireHttpsMetadata = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
options.UsePkce = true;
options.Scope.Add("email");
});
});
});
return builder;
}

access_token / id_token doesn't have Role information, possible to add additional information into the token instead of checking on every page?

I have a React project with API (net core). My website menus/fields will be shown/hidden based on the Role of the user. The user will login to my website via external oidc.
However, the access_token and id_token coming from the oidc doesn't have the Role information, it will only have their email, which I will use to check against my Database to determine which Role is the logged in user. Currently I have an API to get Role based on their access_token, so it's something like
public string getRoles(string access_token)
{
//check Database
return role;
}
This function will be called in almost every page so I was wondering is there any more efficient way to do this?
You need to add the claim to the HttpContext.User when the signin is confirmed with the role from the DB. When you define this connection in your startup, be sure to handle the OnTokenValidated event.
.AddOpenIdConnect("oidc", options =>
{
options.Events = new OpenIdConnectEvents
{
OnTokenValidated = async ctx =>
{
var claim = new Claim("Role", "TheirRole");
var identity = new ClaimsIdentity(new[] { claim });
ctx.Principal.AddIdentity(identity);
await Task.CompletedTask;
}
};
}
Then you can access this within the controller (or anywhere with HttpContext) like so
var claim = HttpContext.User.Claims.First(c => c.Role == "TheirRole");

How to grant access to users externally authenticated and not registered in my WebApp using asp.net core 2.2?

Summary
This is my first try with OAuth2 and External Login Mechanisms.
I'm creating a WebApp that will expose API features through a user-friendly UI.
In order to make API calls, I need to receive an access token from QBO that grants access to resources.
So, my WebApp has an external login option which I use to authenticate against QBO, and then authorize my app.
Everything works fine until...
Services Configuration
Based on a tutorial for GitHub authentication, I came up with this.
services.AddAuthentication(o => {
o.DefaultAuthenticateScheme = IdentityConstants.ExternalScheme;
o.DefaultSignInScheme = IdentityConstants.ExternalScheme;
o.DefaultChallengeScheme = IdentityConstants.ExternalScheme;
})
.AddOAuth("qbo", "qbo", o => {
o.CallbackPath = new PathString("/signin-qbo");
o.ClientId = Configuration["ecm.qbo.client-id"];
o.ClientSecret = Configuration["ecm.qbo.client-secret"];
o.SaveTokens = true;
o.Scope.Add("openid");
o.Scope.Add("profile");
o.Scope.Add("email");
o.Scope.Add("com.intuit.quickbooks.accounting");
o.AuthorizationEndpoint = Configuration["ecm.qbo.authorization-endpoint"];
o.TokenEndpoint = Configuration["ecm.qbo.token-endpoint"];
o.UserInformationEndpoint = Configuration["ecm.qbo.user-info-endpoint"];
o.Events.OnCreatingTicket = async context => {
var companyId = context.Request.Query["realmid"].FirstOrDefault() ?? throw new ArgumentNullException("realmId");
var accessToken = context.AccessToken;
var refreshToken = context.RefreshToken;
Configuration["ecm.qbo.access-token"] = accessToken;
Configuration["ecm.qbo.refresh-token"] = refreshToken;
Configuration["ecm.qbo.realm-id"] = companyId;
context.Backchannel.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
context.Backchannel.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var response = await context.Backchannel.GetStringAsync(context.Options.UserInformationEndpoint);
var result = JsonConvert.DeserializeObject<Dictionary<string, string>>(response);
var user = (ClaimsIdentity)context.Principal.Identity;
user.AddClaims(new Claim[] {
new Claim("access_token", accessToken),
new Claim("refresh_token", refreshToken),
new Claim(ClaimTypes.GivenName, result["givenName"]),
new Claim(ClaimTypes.Surname, result["familyName"]),
new Claim(ClaimTypes.Email, result["email"]),
new Claim(ClaimTypes.Name, result["givenName"]+" "+result["familyName"])
});
};
});
This works. I can add my claims based on user information, the context.Principal.Identity indicates that it's authenticated.
For some reasons, it seems to try and redirect to `/Identity/Account/Login?returnUrl=%2F. Why is that?
Login page redirection
Here, I don't get why I get the redirection and this confuses me a lot. So, I added the AccountController just to try and shut it up.
namespace ecm.backoffice.Controllers {
[Authorize]
[Route("[controller]/[action]")]
public class AccountController : Controller {
[AllowAnonymous]
[HttpGet]
public IActionResult Login(string returnUrl = "/") {
return Challenge(new AuthenticationProperties { RedirectUri = returnUrl });
}
[Authorize]
[HttpGet]
public async Task<IActionResult> Logout(string returnUrl = "/") {
await Request.HttpContext.SignOutAsync("qbo");
return Redirect(returnUrl);
}
}
}
And this creates more confusion than it solves, actually. I'm lost here...
Apply Migrations
This Individual User Authentication WebApp seems to use Identity which looks like it creates a lot of behind the scene mechanisms. I first tried to register to my app, and had to "Apply Migrations", which I totally get, since the data model wasn't initialized.
So, I clicked the Apply Migrations button. I though I was okay with this...
Entity Framework Core
I am aware that the app is using Entity Framework Core for its persistence mechanism, hence the registration process, etc. And to configure it, I needed to add these lines to the services configs.
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>()
.AddDefaultUI(UIFramework.Bootstrap4)
.AddEntityFrameworkStores<ApplicationDbContext>();
It looks like it just won't work at all.
Thoughts
At this point, I think that the message says it Entity Framework just can't load the user information from its underlying data store. I totally understand that, and I don't want to register this user. I just want to take for granted that if QBO authenticated the user, it's fine by me and I grant open bar access to the WebApp features, even if the user ain't registered.
How to tell that to my WebApp?
Related Q/A I read prior to ask
Prevent redirect to /Account/Login in asp.net core 2.2
ASP.NET Core (2.1) Web API: Identity and external login provider
asp.net core 2.2 redirects to login after successful sign in
External Login Authentication in Asp.net core 2.1
And many more...

Auth0, with Authorization Extension & ASP.NET Authorization

I'm working at a project using Auth0. We wish to use the permission system in the Authorization Extension to set it up.
E.g.
Role Admin:
users:viewAll
users:edit
users:xyz
Role User:
users:editOwn
users:viewOwn
users:ect
And then in the Project if possible use the [Authorize(Policy = "users:kvm")] tag.
However, I cannot find any resources on how to actually use the Authorization Extension from Auth0. I'm at a complete loss, so if anyone could guide me on where to even look for these, I'd be very happy.
you can use the Authorization Extension to create a permission that represents access to each application.
Note: While creating permission “Name” should reflect the client id of the application
Example image below
Then create role that represent each application and make sure the relevant permission is selected.
In this example: The role name is” SampleClientAccess”
Then create the group and link the role that you have created. Add relevant users to the group
final step. Go to Dashboard > Rules > create custom rule and the following code.
function (user, context, callback) {
// Assume that permission for an application is the client_id of the permission then
 
if (user.permissions.indexOf(context.clientID) === -1 ){
callback(new UnauthorizedError('You are not allowed to access ' + context.clientName + JSON.stringify(user)));
}
callback(null, user, context);
}
Hope this will help you in some way.
I have decided to drop auth0's Authorization and work out a system myself.
Can't wrap my head around the documentation.
The authorization extension is accessible via API
You have to enable the API access and set up a machine to machine communication so that you can hit the endpoints. (like described in the link)
Then use this documentation to manage permissions, roles, groups etc.
Each request has to include a token (JWT) which you have to get beforehand from https://yourdomain.auth0.com/oauth/token via a POST request.
You have to provide four parameters:
grant_type = client_credentials
client_id = {from your auth0 application}
client_secret = {from your auth0 application}
audience=urn:auth0-authz-api
Put the token into the header of each request as "Authorization" : "Bearer #YOURTOKEN#"
You can use any REST client to hit the endpoints. For the start I'd recommend Postman to test the endpoints and check which calls you need. There is a handy collection you can use with some adjustments.
I want to add how I'm using it in both legacy .NET MVC applications and .NET Core 2.0 APIs as I hope it'll save someone a lot of time that I've spent trying to figure this out.
If what you want is just to get the groups, permissions, roles and update user accounts in auth0 then follow the steps in the answer by #StV.
But if you want to check permissions/roles etc in .NET then this is how I've done it:
Add the groups, roles and permissions to either the access or Id token (or both). To do this follow the instructions here
Once you publish the rules from the above config step you have to create another rule yourself in Auth0 to copy the info in to the tokens (this got me for a while). this has to run after the rule published/created by Auth0. Mine looks like this:
function (user, context, callback) {
if(user.app_metadata) {
var namespace = 'https://visionplatform.com/';
context.accessToken[namespace + 'roles'] = user.roles;
context.accessToken[namespace + 'permissions'] = user.permissions;
context.idToken[namespace + 'roles'] = user.roles;
context.idToken[namespace + 'permissions'] = user.permissions;
}
callback(null, user, context);
}
Now if you the user logs in they will have their groups, roles and permissions in their tokens. However keep in mind that ONLY the groups, roles and permissions for the specific client you authenticated against will show (I lost hours to this).
So now you can get/check the permissions in code be decoding the JWT. Here's a few snippets of code how I've done this in a library method (i.e. not an authorize attribute):
First get your TokenValidationPrams
public TokenValidationParameters GetTokenValidationParameter(string domain, string audience)
{
IConfigurationManager<OpenIdConnectConfiguration> configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>($"{domain}.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
var openIdConfig = AsyncHelper.RunSync(async () => await configurationManager.GetConfigurationAsync(CancellationToken.None));
return new TokenValidationParameters
{
ValidIssuer = $"{domain}",
ValidAudiences = new[] { audience },
IssuerSigningKeys = openIdConfig.SigningKeys
};
}
Then decode your JWT to get the claims
private ClaimsPrincipal GetValidatedToken(string token, TokenValidationParameters validationParameters)
{
var handler = new JwtSecurityTokenHandler();
return handler.ValidateToken(token, validationParameters, out var _);
}
Now you can check that claims principle to see if it includes your group, permission or whatever (please note I just check the permission).
public bool ValidateTokenClaimsPermissionExists(string token, string domain, string audience, string permission)
{
var claimsPrincipal = GetValidatedToken(token, _tokenValidationParameters);
var scopePermission = claimsPrincipal.FindFirst(c => c.Type == Constants.PermissionsClaimTypeName && c.Value == permission);
return scopePermission != null;
}
I use the above to make separate calls to check permissions but you could (and probably should) write your own authorize attribute or if you're using .NET Core you can write an AuthorizationHandler middleware to check whatever claims you want as per the documentation here. The one below checks the scopes but you could adapt it to check the permissions as per the above code:
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, HasScopeRequirement requirement)
{
// If user does not have the scope claim, get out of here
if (!context.User.HasClaim(c => c.Type == "scope" && c.Issuer == requirement.Issuer))
{
return Task.CompletedTask;
}
// Split the scopes string into an array
var scopes = context.User.FindFirst(c => c.Type == "scope" && c.Issuer == requirement.Issuer).Value.Split(' ');
// Succeed if the scope array contains the required scope
if (scopes.Any(s => s == requirement.Scope))
context.Succeed(requirement);
return Task.CompletedTask;
}
I'm going to use bits from all of the above to write an authorize attribute for my .NET MVC applications too.
For a simple setup you can set the roles through the Auth0 GUI and use a rule to apply that to the user:
function (user, context, callback) {
// Roles should only be set to verified users.
if (!user.email || !user.email_verified) {
return callback(null, user, context);
}
user.app_metadata = user.app_metadata || {};
const assignedRoles = (context.authorization || {}).roles;
const addRolesToUser = function(user) {
return assignedRoles;
};
const roles = addRolesToUser(user);
user.app_metadata.roles = roles;
auth0.users.updateAppMetadata(user.user_id, user.app_metadata)
.then(function() {
context.idToken['https://schemas.<yourdomain>.com'] = user.app_metadata.roles;
callback(null, user, context);
})
.catch(function (err) {
callback(err);
});
}
Your startup.cs should have something like this:
services.AddAuthorization(options =>
{
options.AddPolicy("Administrator", authBuilder => { authBuilder.RequireRole("Administrator"); });
options.AddPolicy("User", authBuilder => { authBuilder.RequireRole("Administrator", "User"); });
}
And in the Controller for example:
[Authorize(Roles = "Administrator, User")]
<<your code>>

AngularJs, WebAPI, JWT, with (integrated) Windows authentication

I've asked a question before and the answer that was given was correct but the farther I go down this rabbit hole the more I realize; I don't think I was asking the right question.
Let me just explain this in the most simple terms I can... I have a AngularJS single page app (client), that points at an asp.net webapi (OWIN) site (Resource server?), and a separate asp.net "authorization/authentiation" server.
The auth server will provide authentication and authorization for multiple applications. I need to be able to use the Authorize attribute in the resource server, as well as get a token from from angular. I also need to use windows authentication (integrated) for everything, no usernames or passwords. The claims information is stored in a database and needs to be added to the token.
I've done a SSO style claims authoriztion implementation in asp.net core using openiddict with JwtBearerToken and 'password flow?' And wanted to try to do something similar (token, etc). I have a basic understanding of how that works from my previous implmentation, but I am completely lost trying to figure out how to get JWT working with Windows Auth. The answer to my previous question provided some good suggestions but I am having a hard time seeing how that applies in this scenario.
Currently I have been trying to get IdentityServer3 to do this, using the WindowsAuthentication extensions, mainly pulled from the samples. But I am really struggling to tie this together with the client and actually get something working. The current client and server code is below, mind you I really don't know if this is even close to the correct solution.
Client:
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Passive,
AuthenticationType = "windows",
Authority = "http://localhost:21989",
ClientId = "mvc.owin.implicit",
ClientSecret = "api-secret",
RequiredScopes = new[] { "api" }
});
AuthServer:
app.Map("/windows", ConfigureWindowsTokenProvider);
app.Use(async (context, next) =>
{
if (context.Request.Uri.AbsolutePath.EndsWith("/token", StringComparison.OrdinalIgnoreCase))
{
if (context.Authentication.User == null ||
!context.Authentication.User.Identity.IsAuthenticated)
{
context.Response.StatusCode = 401;
return;
}
}
await next();
});
var factory = new IdentityServerServiceFactory()
.UseInMemoryClients(Clients.Get())
.UseInMemoryScopes(Scopes.Get());
var options = new IdentityServerOptions
{
SigningCertificate = Certificate.Load(),
Factory = factory,
AuthenticationOptions = new AuthenticationOptions
{
EnableLocalLogin = false,
IdentityProviders = ConfigureIdentityProviders
},
RequireSsl = false
};
app.UseIdentityServer(options);
private static void ConfigureWindowsTokenProvider(IAppBuilder app)
{
app.UseWindowsAuthenticationService(new WindowsAuthenticationOptions
{
IdpReplyUrl = "http://localhost:21989",
SigningCertificate = Certificate.Load(),
EnableOAuth2Endpoint = false
});
}
private void ConfigureIdentityProviders(IAppBuilder app, string signInAsType)
{
var wsFederation = new WsFederationAuthenticationOptions
{
AuthenticationType = "windows",
Caption = "Windows",
SignInAsAuthenticationType = signInAsType,
MetadataAddress = "http://localhost:21989",
Wtrealm = "urn:idsrv3"
};
app.UseWsFederationAuthentication(wsFederation);
}
EDIT: I see the auth endpoints requests for "/.well-known/openid-configuration" as well as "/.well-known/jwks" and I have the Authorize attribute on a controller action which is being called, but I dont see anything else happening on the auth side. I also added a ICustomClaimsProvider implmentation to the usewindowsauthservice WindowsAuthenticationOptions but that doesnt even get called.
I've done a SSO style claims authoriztion implementation in asp.net core using openiddict with JwtBearerToken and 'password flow?'
If you were to use OpenIddict with Windows authentication, it would be quite easy to implement using the OAuth2/OpenID Connect implicit flow (which is the most appropriate flow for a JS app), without needing any WS-Federation proxy:
Startup configuration:
public void ConfigureServices(IServiceCollection services)
{
// Register the OpenIddict services.
services.AddOpenIddict(options =>
{
// Register the Entity Framework stores.
options.AddEntityFrameworkCoreStores<ApplicationDbContext>();
// Register the ASP.NET Core MVC binder used by OpenIddict.
// Note: if you don't call this method, you won't be able to
// bind OpenIdConnectRequest or OpenIdConnectResponse parameters.
options.AddMvcBinders();
// Enable the authorization endpoint.
options.EnableAuthorizationEndpoint("/connect/authorize");
// Enable the implicit flow.
options.AllowImplicitFlow();
// During development, you can disable the HTTPS requirement.
options.DisableHttpsRequirement();
// Register a new ephemeral key, that is discarded when the application
// shuts down. Tokens signed using this key are automatically invalidated.
// This method should only be used during development.
options.AddEphemeralSigningKey();
});
// Note: when using WebListener instead of IIS/Kestrel, the following lines must be uncommented:
//
// services.Configure<WebListenerOptions>(options =>
// {
// options.ListenerSettings.Authentication.AllowAnonymous = true;
// options.ListenerSettings.Authentication.Schemes = AuthenticationSchemes.Negotiate;
// });
}
Authorization controller:
public class AuthorizationController : Controller
{
// Warning: extreme caution must be taken to ensure the authorization endpoint is not included in a CORS policy
// that would allow an attacker to force a victim to silently authenticate with his Windows credentials
// and retrieve an access token using a cross-domain AJAX call. Avoiding CORS is strongly recommended.
[HttpGet("~/connect/authorize")]
public async Task<IActionResult> Authorize(OpenIdConnectRequest request)
{
// Retrieve the Windows principal: if a null value is returned, apply an HTTP challenge
// to allow IIS/WebListener to initiate the unmanaged integrated authentication dance.
var principal = await HttpContext.Authentication.AuthenticateAsync(IISDefaults.Negotiate);
if (principal == null)
{
return Challenge(IISDefaults.Negotiate);
}
// Note: while the principal is always a WindowsPrincipal object when using Kestrel behind IIS,
// a WindowsPrincipal instance must be manually created from the WindowsIdentity with WebListener.
var ticket = CreateTicket(request, principal as WindowsPrincipal ?? new WindowsPrincipal((WindowsIdentity) principal.Identity));
// Immediately return an authorization response without displaying a consent screen.
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}
private AuthenticationTicket CreateTicket(OpenIdConnectRequest request, WindowsPrincipal principal)
{
// Create a new ClaimsIdentity containing the claims that
// will be used to create an id_token, a token or a code.
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme);
// Note: the JWT/OIDC "sub" claim is required by OpenIddict
// but is not automatically added to the Windows principal, so
// the primary security identifier is used as a fallback value.
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, principal.GetClaim(ClaimTypes.PrimarySid));
// Note: by default, claims are NOT automatically included in the access and identity tokens.
// To allow OpenIddict to serialize them, you must attach them a destination, that specifies
// whether they should be included in access tokens, in identity tokens or in both.
foreach (var claim in principal.Claims)
{
// In this sample, every claim is serialized in both the access and the identity tokens.
// In a real world application, you'd probably want to exclude confidential claims
// or apply a claims policy based on the scopes requested by the client application.
claim.SetDestinations(OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
// Copy the claim from the Windows principal to the new identity.
identity.AddClaim(claim);
}
// Create a new authentication ticket holding the user identity.
return new AuthenticationTicket(
new ClaimsPrincipal(identity),
new AuthenticationProperties(),
OpenIdConnectServerDefaults.AuthenticationScheme);
}
}
A similar scenario can be implemented in legacy ASP.NET apps using the OWIN/Katana version of ASOS, the OpenID Connect server middleware behind OpenIddict:
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseOpenIdConnectServer(options =>
{
// Register a new ephemeral key, that is discarded when the application
// shuts down. Tokens signed using this key are automatically invalidated.
// This method should only be used during development.
options.SigningCredentials.AddEphemeralKey();
// Enable the authorization endpoint.
options.AuthorizationEndpointPath = new PathString("/connect/authorize");
// During development, you can disable the HTTPS requirement.
options.AllowInsecureHttp = true;
// Implement the ValidateAuthorizationRequest event to validate the response_type,
// the client_id and the redirect_uri provided by the client application.
options.Provider.OnValidateAuthorizationRequest = context =>
{
if (!context.Request.IsImplicitFlow())
{
context.Reject(
error: OpenIdConnectConstants.Errors.UnsupportedResponseType,
description: "The provided response_type is invalid.");
return Task.FromResult(0);
}
if (!string.Equals(context.ClientId, "spa-application", StringComparison.Ordinal))
{
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidClient,
description: "The provided client_id is invalid.");
return Task.FromResult(0);
}
if (!string.Equals(context.RedirectUri, "http://spa-app.com/redirect_uri", StringComparison.Ordinal))
{
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidClient,
description: "The provided redirect_uri is invalid.");
return Task.FromResult(0);
}
context.Validate();
return Task.FromResult(0);
};
// Implement the HandleAuthorizationRequest event to return an implicit authorization response.
options.Provider.OnHandleAuthorizationRequest = context =>
{
// Retrieve the Windows principal: if a null value is returned, apply an HTTP challenge
// to allow IIS/SystemWeb to initiate the unmanaged integrated authentication dance.
var principal = context.OwinContext.Authentication.User as WindowsPrincipal;
if (principal == null)
{
context.OwinContext.Authentication.Challenge();
return Task.FromResult(0);
}
// Create a new ClaimsIdentity containing the claims that
// will be used to create an id_token, a token or a code.
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationType);
// Note: the JWT/OIDC "sub" claim is required by OpenIddict
// but is not automatically added to the Windows principal, so
// the primary security identifier is used as a fallback value.
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, principal.GetClaim(ClaimTypes.PrimarySid));
// Note: by default, claims are NOT automatically included in the access and identity tokens.
// To allow OpenIddict to serialize them, you must attach them a destination, that specifies
// whether they should be included in access tokens, in identity tokens or in both.
foreach (var claim in principal.Claims)
{
// In this sample, every claim is serialized in both the access and the identity tokens.
// In a real world application, you'd probably want to exclude confidential claims
// or apply a claims policy based on the scopes requested by the client application.
claim.SetDestinations(OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
// Copy the claim from the Windows principal to the new identity.
identity.AddClaim(claim);
}
context.Validate(identity);
return Task.FromResult(0);
};
});
}
}
The client-side code shouldn't be different from any other JS application using the implicit flow. You can take a look at this sample to see how you can implement it with the oidc-client JS library: https://github.com/openiddict/openiddict-samples/tree/master/samples/ImplicitFlow/AureliaApp
So ultimately the whole point here was to augment claims on the existing ClaimsPrincipal with claims from the database and hopefully be able to use JWT's in the javascript. I was unable to get that to work using IdentityServer3. I ended up rolling my own rudimentary solution by implementing IAuthenticationFilter and IAuthorizationFilter using an attribute on the actions to supply the claim name.
First the authorize attribute does nothing but take the name of the claim that the user should have to access the action.
public class AuthorizeClaimAttribute : Attribute
{
public string ClaimValue;
public AuthorizeClaimAttribute(string value)
{
ClaimValue = value;
}
}
Then the Authorize filter which does nothing but check to see if the user has the claim from the attribute.
public class AuthorizeClaimFilter : AuthorizeAttribute, IAuthorizationFilter
{
private readonly string _claimValue;
public AuthorizeClaimFilter(string claimValue)
{
_claimValue = claimValue;
}
public override async Task OnAuthorizationAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
{
var p = actionContext.RequestContext.Principal as ClaimsPrincipal;
if(!p.HasClaim("process", _claimValue))
HandleUnauthorizedRequest(actionContext);
await Task.FromResult(0);
}
protected override void HandleUnauthorizedRequest(HttpActionContext actionContext)
{
actionContext.Response = new HttpResponseMessage(HttpStatusCode.Forbidden);
}
}
The Authentication filter which calls the webapi endpoint (which is using windows authentication) to get the users list of custom "claims" from the database. The WebAPI is just a standard webapi instance, nothing special at all.
public class ClaimAuthenticationFilter : ActionFilterAttribute, IAuthenticationFilter
{
public ClaimAuthenticationFilter()
{
}
public async Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
if (context.Principal != null && context.Principal.Identity.IsAuthenticated)
{
var windowsPrincipal = context.Principal as WindowsPrincipal;
var handler = new HttpClientHandler()
{
UseDefaultCredentials = true
};
HttpClient client = new HttpClient(handler);
client.BaseAddress = new Uri("http://localhost:21989");// to be stored in config
var response = await client.GetAsync("/Security");
var contents = await response.Content.ReadAsStringAsync();
var claimsmodel = JsonConvert.DeserializeObject<List<ClaimsModel>>(contents);
if (windowsPrincipal != null)
{
var name = windowsPrincipal.Identity.Name;
var identity = new ClaimsIdentity();
foreach (var claim in claimsmodel)
{
identity.AddClaim(new Claim("process", claim.ClaimName));
}
var claimsPrincipal = new ClaimsPrincipal(identity);
context.Principal = claimsPrincipal;
}
}
await Task.FromResult(0);
}
public async Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
{
var challenge = new AuthenticationHeaderValue("Negotiate");
context.Result = new ResultWithChallenge(challenge, context.Result);
await Task.FromResult(0);
}
}
The filters are bound to the attribute using my DI framework (ninject in this case).
this.BindHttpFilter<AuthorizeClaimFilter>(FilterScope.Action)
.WhenActionMethodHas<AuthorizeClaimAttribute>()
.WithConstructorArgumentFromActionAttribute<AuthorizeClaimAttribute>("claimValue", o => o.ClaimValue);
This works for my purposes, and the web api endpoint consumable both in the WebAPI instance and in the AngularJS app. However it is obviously NOT ideal. I really would have preferred to use 'real' authentication/authorization processes. I hesitate to say this is the answer to the question, but it is the only solution I could come up with the time that I had to make something work.

Categories

Resources