Claims added in Startup not found in Blazor page - c#

In order to get group membership functionality in our Blazor app based on Azure B2C, I add some claims in the Server project in Startup.cs. When I inspect the User object in the razor pages, the claims I added are not present.
Startup.cs, public void Configure():
app.Use(async (context, next) =>
{
if (context.User != null && context.User.Identity.IsAuthenticated)
{
var groups = GetGroupsFromAzureB2c(context.User);
// Attempt A, separate Identity
ClaimsIdentity i = new(context.User.Identity);
i.AddClaims(groups.Select(g => new Claim("Group", g)));
context.User.AddIdentity(i);
// Attempt B: Adding claims to the existing Identity
((ClaimsIdentity)context.User.Identity).AddClaims(groups.Select(g => new Claim("Group", g));
}
await next();
});
page.razor, protected override async Task OnInitializedAsync():
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
if (user.Identity.IsAuthenticated)
{
var _claims = user.Claims;
}
_claims only holds the 12 claims that were added automatically, never the 4 claims I added in Startup.cs.
Are there different instances of the User object? Am I adding the claims incorrectly?

Apparently there are two separate Identities at play in a Blazor app. One in the Client code and a separate one in the Server code.
We fixed this by adding a CustomUserFactory that inherits from AccountClaimsPrincipalFactory<RemoteUserAccount>. We retrieve the AzureB2C roles from a central method using the Graph API for the client side which we call in Program.cs with
builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAdB2C", options.ProviderOptions.Authentication);
options.ProviderOptions.DefaultAccessTokenScopes.Add("xxx");
})
// here we hook up our custom factory
.AddAccountClaimsPrincipalFactory<CustomUserFactory>();
For the server side code, we use the same central method to get the roles from AzureB2C which we call from Startup.cs with
services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options => {
options.Events.OnTokenValidated = async context => {/*add roles here*/}

Related

ClaimsIdentity cant access methods in official OpenIdDict Samples

I have been very frustrated trying to use openiddict. I can't use any of the pre-existing sample since their ClaimsIdentity uses methods that to me aren't available, for example the identity.SetClaims(), identity.SetScopes() and identity.GetScopes() don't work for me.
This is the official sample Zirku.Server:
var builder = WebApplication.CreateBuilder(args);
// OpenIddict offers native integration with Quartz.NET to perform scheduled tasks
// (like pruning orphaned authorizations/tokens from the database) at regular intervals.
builder.Services.AddQuartz(options =>
{
options.UseMicrosoftDependencyInjectionJobFactory();
options.UseSimpleTypeLoader();
options.UseInMemoryStore();
});
// Register the Quartz.NET service and configure it to block shutdown until jobs are complete.
builder.Services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true);
builder.Services.AddDbContext<DbContext>(options =>
{
// Configure the context to use Microsoft SQL Server.
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
// Register the entity sets needed by OpenIddict.
// Note: use the generic overload if you need
// to replace the default OpenIddict entities.
options.UseOpenIddict();
});
builder.Services.AddOpenIddict()
// Register the OpenIddict core components.
.AddCore(options =>
{
options.UseEntityFrameworkCore()
.UseDbContext<DbContext>();
})
// Register the OpenIddict server components.
.AddServer(options =>
{
// Enable the authorization, introspection and token endpoints.
options.SetAuthorizationEndpointUris("/authorize")
.SetIntrospectionEndpointUris("/introspect")
.SetTokenEndpointUris("/token");
// Note: this sample only uses the authorization code flow but you can enable
// the other flows if you need to support implicit, password or client credentials.
options.AllowAuthorizationCodeFlow();
// Register the signing credentials.
options.AddDevelopmentSigningCertificate();
// Register the ASP.NET Core host and configure the ASP.NET Core-specific options.
//
// Note: unlike other samples, this sample doesn't use token endpoint pass-through
// to handle token requests in a custom MVC action. As such, the token requests
// will be automatically handled by OpenIddict, that will reuse the identity
// resolved from the authorization code to produce access and identity tokens.
//
options.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough();
})
// Register the OpenIddict validation components.
.AddValidation(options =>
{
// Import the configuration from the local OpenIddict server instance.
options.UseLocalServer();
// Register the ASP.NET Core host.
options.UseAspNetCore();
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseHttpsRedirection();
// Create new application registrations matching the values configured in Zirku.Client and Zirku.Api1.
// Note: in a real world application, this step should be part of a setup script.
await using (var scope = app.Services.CreateAsyncScope())
{
var context = scope.ServiceProvider.GetRequiredService<DbContext>();
await context.Database.EnsureCreatedAsync();
await CreateApplicationsAsync();
await CreateScopesAsync();
async Task CreateApplicationsAsync()
{
var manager = scope.ServiceProvider.GetRequiredService<IOpenIddictApplicationManager>();
if (await manager.FindByClientIdAsync("console_app") is null)
{
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = "console_app",
RedirectUris =
{
new Uri("http://localhost:8739/")
},
Permissions =
{
Permissions.Endpoints.Authorization,
Permissions.Endpoints.Token,
Permissions.GrantTypes.AuthorizationCode,
Permissions.ResponseTypes.Code,
Permissions.Scopes.Email,
Permissions.Scopes.Profile,
Permissions.Scopes.Roles,
Permissions.Prefixes.Scope + "api1",
Permissions.Prefixes.Scope + "api2"
}
});
}
if (await manager.FindByClientIdAsync("resource_server_1") is null)
{
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = "resource_server_1",
ClientSecret = "846B62D0-DEF9-4215-A99D-86E6B8DAB342",
Permissions =
{
Permissions.Endpoints.Introspection
}
});
}
// Note: no client registration is created for resource_server_2
// as it uses local token validation instead of introspection.
}
async Task CreateScopesAsync()
{
var manager = scope.ServiceProvider.GetRequiredService<IOpenIddictScopeManager>();
if (await manager.FindByNameAsync("api1") is null)
{
await manager.CreateAsync(new OpenIddictScopeDescriptor
{
Name = "api1",
Resources =
{
"resource_server_1"
}
});
}
if (await manager.FindByNameAsync("api2") is null)
{
await manager.CreateAsync(new OpenIddictScopeDescriptor
{
Name = "api2",
Resources =
{
"resource_server_2"
}
});
}
}
}
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/api", [Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)]
(ClaimsPrincipal user) => user.Identity!.Name);
app.MapGet("/authorize", async (HttpContext context, IOpenIddictScopeManager manager) =>
{
// Retrieve the OpenIddict server request from the HTTP context.
var request = context.GetOpenIddictServerRequest();
var identifier = (int?)request["hardcoded_identity_id"];
if (identifier is not (1 or 2))
{
return Results.Challenge(
authenticationSchemes: new[] { OpenIddictServerAspNetCoreDefaults.AuthenticationScheme },
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidRequest,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The specified hardcoded identity is invalid."
}));
}
// Create the claims-based identity that will be used by OpenIddict to generate tokens.
var identity = new ClaimsIdentity(
authenticationType: TokenValidationParameters.DefaultAuthenticationType,
nameType: Claims.Name,
roleType: Claims.Role);
// Add the claims that will be persisted in the tokens.
identity.AddClaim(new Claim(Claims.Subject, identifier.Value.ToString(CultureInfo.InvariantCulture)));
identity.AddClaim(new Claim(Claims.Name, identifier switch
{
1 => "Alice",
2 => "Bob",
_ => throw new InvalidOperationException()
}));
// Note: in this sample, the client is granted all the requested scopes for the first identity (Alice)
// but for the second one (Bob), only the "api1" scope can be granted, which will cause requests sent
// to Zirku.Api2 on behalf of Bob to be automatically rejected by the OpenIddict validation handler,
// as the access token representing Bob won't contain the "resource_server_2" audience required by Api2.
identity.SetScopes(identifier switch
{
1 => request.GetScopes(),
2 => new[] { "api1" }.Intersect(request.GetScopes()),
_ => throw new InvalidOperationException()
});
identity.SetResources(await manager.ListResourcesAsync(identity.GetScopes()).ToListAsync());
// Allow all claims to be added in the access tokens.
identity.SetDestinations(claim => new[] { Destinations.AccessToken });
return Results.SignIn(new ClaimsPrincipal(identity), properties: null, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
});
app.Run();
I have been trying to convert the samples to identity.AddClaim(), but I am not sure that it works as expected.
Please tell me what I am doing wrong. I am new to authorization and authentication so, as you can imagine, I am not good enough to figure out what is going wrong.
PS If you have any good up-to-date sources so that I can read up on the subject, that would be great.
PS 2 Excuse my english, it is not my first language

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");

ASP.Net Core 2.1 make code reusable for multiple controllers with authentification

Let's say we have code which retrieves a list of items from a database. The resulting list depends on the user permissions.
What I want is to make this code reusable, i.e. callable from multiple controllers. What is the standard way of doing this?
For authentification in my code I load a User from the database via
User user = dbContext.User.SingleOrDefault(x => x.access_token == User.Identity.Name);
This is the authentification schmeme I'm using in Startup.cs:
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
}).AddCookie("Cookies", options =>
{
options.Cookie.Name = "auth_cookie";
options.Cookie.SameSite = SameSiteMode.None;
options.Events = new CookieAuthenticationEvents
{
OnRedirectToLogin = redirectContext =>
{
redirectContext.HttpContext.Response.StatusCode = 401;
return Task.CompletedTask;
}
};
});
I tried to put the code retrieving the list directly in one controller and for the other controllers which need the code to call that controller via
services.AddControllersAsServices()
and then
(MyController)HttpContext.RequestServices.GetService(typeof(MyController));
but the User property from ControllerBase was null.
I have read all of the documentation I could find but could not find a solution that the User or HttpContext properties have a value.

Asp.net Core 2.0 external authentication not persisting

I am using asp.net Core 2.0 with various external authentications (Google, Twitter, etc).
Everything is working, and I get a validated response back with the OnTicketReceived event, however, all subsequent calls to the site do not persist the user.
Here is my startup:
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentity<IdentityUser, IdentityRole>()
.AddDefaultTokenProviders();
services.AddAuthentication()
.AddGoogle(googleOptions =>
{
googleOptions.ClientId = Configuration["Google:ClientId"];
googleOptions.ClientSecret = Configuration["Google:ClientSecret"];
googleOptions.CallbackPath = "/auth/google";
googleOptions.SaveTokens = true;
googleOptions.Events = new OAuthEvents {OnTicketReceived = async e => await HandleOnTicketReceived(authService, e),};
})
.AddTwitter(twitterOptions =>
{
twitterOptions.ConsumerKey = Configuration["Twitter:ClientId"];
twitterOptions.ConsumerSecret = Configuration["Twitter:ClientSecret"];
twitterOptions.CallbackPath = "/auth/twitter";
twitterOptions.RetrieveUserDetails = true;
twitterOptions.SaveTokens = true;
twitterOptions.Events = new TwitterEvents {OnTicketReceived = async e => await HandleOnTicketReceived(authService, e),};
});
services.AddMvc();
}
This all seems to work great. I get all the necessary data from each OAuth provider. Here is an example:
properties
private async Task HandleOnTicketReceived(IAuthService authService, TicketReceivedContext e)
{
var identity = e.Principal.Identity as ClaimsIdentity;
// HERE identity is correct for all external auth providers, so it is working this far
}
The problem is every subsequent call to the site has lost the user's external identity. What was there is now gone.
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var identity = User.Identity as ClaimsIdentity;
// HERE is the problem -- on all subsequent calls, identity is basically empty
base.OnActionExecuting(filterContext);
}
How can I get the user identity to persist across calls? The cookie "Identity.External" is set and seems correct.
Edit:
I have also added a sql backend with
services.AddDbContext<ApplicationDbContext>();
and
services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
The AspNet user tables are created but never populated. I am at a loss for what is missing here.
I should clarify that i am using dbContext.Database.Migrate() to create the authentication tables, so I have confidence the ApplicationDbContext is working. However, there is no error thrown, the tables just don't get populated and subsequent calls just don't get the user's identity set.

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