My ASP.NET 5 (MVC 6 + beta7) web application (MVC + WebAPI) is required to get back an access_token from WebAPI login calls.
So far, from googling, I have created the following code for startup.cs:
app.UseOAuthBearerAuthentication(options => {
options.AutomaticAuthentication = true;
options.Audience = "http://localhost:62100/";
options.Authority = "http://localhost:62100/";
});
My client side is:
var login = function ()
{
var url = "http://localhost:62100/";
var data = $("#userData").serialize();
data = data + "&grant_type=password";
$.post(url, data)
.success(saveAccessToken)
.always(showResponse);
return false;
};
Is it required to use UseOpenIdConnectServer? If so, how do I use SigningCredentials so that I get a token (e.g. MVC5 ApplicationOAuthProvider)?
Please note that my site is simple demo HTTP site and I do not need any SSL.
Is it required to use UseOpenIdConnectServer?
Using AspNet.Security.OpenIdConnect.Server is not "required". You're - of course - free to opt for another server (like IdentityServer) or for a custom solution.
Being the main developer behind aspnet-contrib, I'm not really objective, so I'll necessarily suggest going with app.UseOpenIdConnectServer().
If so, how do I use SigningCredentials so that I get a token (e.g. MVC5 ApplicationOAuthProvider)?
When implementing the password and using the default token type, registering a signing key/certificate is not mandatory.
Here's how you can get started:
ASP.NET Core 1.x:
Startup.cs
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication();
}
public void Configure(IApplicationBuilder app)
{
// Add a new middleware validating the encrypted
// access tokens issued by the OIDC server.
app.UseOAuthValidation();
// Add a new middleware issuing tokens.
app.UseOpenIdConnectServer(options =>
{
options.TokenEndpointPath = "/connect/token";
// Override OnValidateTokenRequest to skip client authentication.
options.Provider.OnValidateTokenRequest = context =>
{
// Reject the token requests that don't use
// grant_type=password or grant_type=refresh_token.
if (!context.Request.IsPasswordGrantType() &&
!context.Request.IsRefreshTokenGrantType())
{
context.Reject(
error: OpenIdConnectConstants.Errors.UnsupportedGrantType,
description: "Only grant_type=password and refresh_token " +
"requests are accepted by this server.");
return Task.FromResult(0);
}
// Since there's only one application and since it's a public client
// (i.e a client that cannot keep its credentials private),
// call Skip() to inform the server the request should be
// accepted without enforcing client authentication.
context.Skip();
return Task.FromResult(0);
};
// Override OnHandleTokenRequest to support
// grant_type=password token requests.
options.Provider.OnHandleTokenRequest = context =>
{
// Only handle grant_type=password token requests and let the
// OpenID Connect server middleware handle the other grant types.
if (context.Request.IsPasswordGrantType())
{
// Do your credentials validation here.
// Note: you can call Reject() with a message
// to indicate that authentication failed.
var identity = new ClaimsIdentity(context.Options.AuthenticationScheme);
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "[unique id]");
// By default, claims are not serialized
// in the access and identity tokens.
// Use the overload taking a "destinations"
// parameter to make sure your claims
// are correctly inserted in the appropriate tokens.
identity.AddClaim("urn:customclaim", "value",
OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
new AuthenticationProperties(),
context.Options.AuthenticationScheme);
// Call SetScopes with the list of scopes you want to grant
// (specify offline_access to issue a refresh token).
ticket.SetScopes("profile", "offline_access");
context.Validate(ticket);
}
return Task.FromResult(0);
};
});
}
}
.csproj
<ItemGroup>
<PackageReference Include="AspNet.Security.OpenIdConnect.Server" Version="1.0.2" />
</ItemGroup>
ASP.NET Core 2.x:
Startup.cs
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication()
// Add a new middleware validating the encrypted
// access tokens issued by the OIDC server.
.AddOAuthValidation()
// Add a new middleware issuing tokens.
.AddOpenIdConnectServer(options =>
{
options.TokenEndpointPath = "/connect/token";
// Override OnValidateTokenRequest to skip client authentication.
options.Provider.OnValidateTokenRequest = context =>
{
// Reject the token requests that don't use
// grant_type=password or grant_type=refresh_token.
if (!context.Request.IsPasswordGrantType() &&
!context.Request.IsRefreshTokenGrantType())
{
context.Reject(
error: OpenIdConnectConstants.Errors.UnsupportedGrantType,
description: "Only grant_type=password and refresh_token " +
"requests are accepted by this server.");
return Task.CompletedTask;
}
// Since there's only one application and since it's a public client
// (i.e a client that cannot keep its credentials private),
// call Skip() to inform the server the request should be
// accepted without enforcing client authentication.
context.Skip();
return Task.CompletedTask;
};
// Override OnHandleTokenRequest to support
// grant_type=password token requests.
options.Provider.OnHandleTokenRequest = context =>
{
// Only handle grant_type=password token requests and let the
// OpenID Connect server middleware handle the other grant types.
if (context.Request.IsPasswordGrantType())
{
// Do your credentials validation here.
// Note: you can call Reject() with a message
// to indicate that authentication failed.
var identity = new ClaimsIdentity(context.Scheme.Name);
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "[unique id]");
// By default, claims are not serialized
// in the access and identity tokens.
// Use the overload taking a "destinations"
// parameter to make sure your claims
// are correctly inserted in the appropriate tokens.
identity.AddClaim("urn:customclaim", "value",
OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
new AuthenticationProperties(),
context.Scheme.Name);
// Call SetScopes with the list of scopes you want to grant
// (specify offline_access to issue a refresh token).
ticket.SetScopes("profile", "offline_access");
context.Validate(ticket);
}
return Task.CompletedTask;
};
});
}
}
.csproj
<ItemGroup>
<PackageReference Include="AspNet.Security.OpenIdConnect.Server" Version="2.0.0-*" />
</ItemGroup>
You can also read this blog post, that explains how to implement the resource owner password grant: http://kevinchalet.com/2016/07/13/creating-your-own-openid-connect-server-with-asos-implementing-the-resource-owner-password-credentials-grant/
Related
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
Let's have web application from Visual Studio template using netcoreapp3.1.
It uses asp net identity, e.g. page gets refreshed upon click on Login button.
What I'm trying to achieve is to have SignalR Core hub method like this
[HttpGet]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<bool> Login(string email, string password)
{
var result = await _signInManager.PasswordSignInAsync(email,
password, true, lockoutOnFailure: false).ConfigureAwait(false);
if (result.Succeeded)
{
return true;
}
....
....
}
unfortunately for my naive attempt I 'll get InvalidOperationException: Headers are read-only, response has already started.
With horribly long stack trace ending with
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpHeaders.ThrowHeadersReadOnlyException()
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpHeaders.Microsoft.AspNetCore.Http.IHeaderDictionary.set_Item(String key, StringValues value)
at Microsoft.AspNetCore.Http.ResponseCookies.Append(String key, String value, CookieOptions options)
at Microsoft.AspNetCore.CookiePolicy.ResponseCookiesWrapper.Append(String key, String value, CookieOptions options)
at Microsoft.AspNetCore.Authentication.Cookies.ChunkingCookieManager.AppendResponseCookie(HttpContext context, String key, String value, CookieOptions options)
at Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler.<HandleSignInAsync>d__25.MoveNext()
I found that for similar use-cases it's common to interact with HttpContext but I can't find way how it could play role in this scenario as ApplicationSignInManager seemed relatively independant to that.
I realize it's quite possible I'm missing something from conceptual point of view so every idea about how to get closer to desired goal is welcome.
Seems to be described here github issue so I'll need to think about redesign probably.
You can achieve a controllerless model if you switch to Bearer Token authentication.
All the following examples and code are from Authentication and authorization in ASP.NET Core SignalR.
typescript connection
// Connect, using the token we got.
this.connection = new signalR.HubConnectionBuilder()
.withUrl("/hubs/chat", { accessTokenFactory: () => this.loginToken })
.build();
C# hub builder
var connection = new HubConnectionBuilder()
.WithUrl("https://example.com/myhub", options =>
{
options.AccessTokenProvider = () => Task.FromResult(_myAccessToken);
})
.Build();
The access token function you provide is called before every HTTP request made by SignalR. If you need to renew the token in order to keep the connection active (because it may expire during the connection), do so from within this function and return the updated token.
In standard web APIs, bearer tokens are sent in an HTTP header. However, SignalR is unable to set these headers in browsers when using some transports. When using WebSockets and Server-Sent Events, the token is transmitted as a query string parameter. To support this on the server, additional configuration is required:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddAuthentication(options =>
{
// Identity made Cookie authentication the default.
// However, we want JWT Bearer Auth to be the default.
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
// Configure the Authority to the expected value for your authentication provider
// This ensures the token is appropriately validated
options.Authority = /* TODO: Insert Authority URL here */;
// We have to hook the OnMessageReceived event in order to
// allow the JWT authentication handler to read the access
// token from the query string when a WebSocket or
// Server-Sent Events request comes in.
// Sending the access token in the query string is required due to
// a limitation in Browser APIs. We restrict it to only calls to the
// SignalR hub in this code.
// See https://learn.microsoft.com/aspnet/core/signalr/security#access-token-logging
// for more information about security considerations when using
// the query string to transmit the access token.
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
// If the request is for our hub...
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
(path.StartsWithSegments("/hubs/chat")))
{
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddSignalR();
// Change to use Name as the user identifier for SignalR
// WARNING: This requires that the source of your JWT token
// ensures that the Name claim is unique!
// If the Name claim isn't unique, users could receive messages
// intended for a different user!
services.AddSingleton<IUserIdProvider, NameUserIdProvider>();
// Change to use email as the user identifier for SignalR
// services.AddSingleton<IUserIdProvider, EmailBasedUserIdProvider>();
// WARNING: use *either* the NameUserIdProvider *or* the
// EmailBasedUserIdProvider, but do not use both.
}
Seems it does not make any sense to try perform login inside hub method.
It's more convenient to add Controller into project and perform login operation there instead of inside SignalR Hub.
My motivation was to avoid necessity of having Controllers inside project as there were no need of them due application design but it just can't compensate difficulties which comes with that.
I achieved my goal by just following this link and using fetch(...).
still periodically struggling with OpenAuth using OpenIdDict (credentials flow) in ASP.NET Core, I updated to the latest OpenIdDict bits and VS2017 my old sample code you can find at https://github.com/Myrmex/repro-oidang, with a full step-by-step guidance to create an essential startup template. Hope this can be useful to the community to help getting started with simple security scenarios, so any contribution to that simple example code is welcome.
Essentially I followed the credentials flow sample from the OpenIdDict author, and I can get my token back when requesting it like (using Fiddler):
POST http://localhost:50728/connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=password&scope=offline_access profile email roles&resource=http://localhost:4200&username=zeus&password=P4ssw0rd!
Problem is that when I try to use this token, I keep getting a 401, without any other hint: no exception, nothing logged. The request is like:
GET http://localhost:50728/api/values
Content-Type: application/json
Authorization: Bearer ...
Here is my relevant code: first Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
// setup options with DI
// https://docs.asp.net/en/latest/fundamentals/configuration.html
services.AddOptions();
// CORS (note: if using Azure, remember to enable CORS in the portal, too!)
services.AddCors();
// add entity framework and its context(s) using in-memory
// (or use the commented line to use a connection string to a real DB)
services.AddEntityFrameworkSqlServer()
.AddDbContext<ApplicationDbContext>(options =>
{
// options.UseSqlServer(Configuration.GetConnectionString("Authentication")));
options.UseInMemoryDatabase();
// register the entity sets needed by OpenIddict.
// Note: use the generic overload if you need
// to replace the default OpenIddict entities.
options.UseOpenIddict();
});
// register the Identity services
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// configure Identity to use the same JWT claims as OpenIddict instead
// of the legacy WS-Federation claims it uses by default (ClaimTypes),
// which saves you from doing the mapping in your authorization controller.
services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.UserNameClaimType = OpenIdConnectConstants.Claims.Name;
options.ClaimsIdentity.UserIdClaimType = OpenIdConnectConstants.Claims.Subject;
options.ClaimsIdentity.RoleClaimType = OpenIdConnectConstants.Claims.Role;
});
// 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
// to action methods. Alternatively, you can still use the lower-level
// HttpContext.GetOpenIdConnectRequest() API.
options.AddMvcBinders();
// enable the endpoints
options.EnableTokenEndpoint("/connect/token");
options.EnableLogoutEndpoint("/connect/logout");
// http://openid.net/specs/openid-connect-core-1_0.html#UserInfo
options.EnableUserinfoEndpoint("/connect/userinfo");
// enable the password flow
options.AllowPasswordFlow();
options.AllowRefreshTokenFlow();
// during development, you can disable the HTTPS requirement
options.DisableHttpsRequirement();
// Note: to use JWT access tokens instead of the default
// encrypted format, the following lines are required:
// options.UseJsonWebTokens();
// options.AddEphemeralSigningKey();
});
// add framework services
services.AddMvc()
.AddJsonOptions(options =>
{
options.SerializerSettings.ContractResolver =
new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver();
});
// seed the database with the demo user details
services.AddTransient<IDatabaseInitializer, DatabaseInitializer>();
// swagger
services.AddSwaggerGen();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory,
IDatabaseInitializer databaseInitializer)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
loggerFactory.AddNLog();
// https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling
if (env.IsDevelopment()) app.UseDeveloperExceptionPage();
// to serve up index.html
app.UseDefaultFiles();
app.UseStaticFiles();
// CORS
// https://docs.asp.net/en/latest/security/cors.html
app.UseCors(builder =>
builder.WithOrigins("http://localhost:4200")
.AllowAnyHeader()
.AllowAnyMethod());
// add a middleware used to validate access tokens and protect the API endpoints
app.UseOAuthValidation();
app.UseOpenIddict();
app.UseMvc();
// app.UseMvcWithDefaultRoute();
// app.UseWelcomePage();
// seed the database
databaseInitializer.Seed().GetAwaiter().GetResult();
// swagger
// enable middleware to serve generated Swagger as a JSON endpoint
app.UseSwagger();
// enable middleware to serve swagger-ui assets (HTML, JS, CSS etc.)
app.UseSwaggerUi();
}
And then my controller (you can find the whole solution in the repository quoted above):
public sealed class AuthorizationController : Controller
{
private readonly IOptions<IdentityOptions> _identityOptions;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly UserManager<ApplicationUser> _userManager;
public AuthorizationController(
IOptions<IdentityOptions> identityOptions,
SignInManager<ApplicationUser> signInManager,
UserManager<ApplicationUser> userManager)
{
_identityOptions = identityOptions;
_signInManager = signInManager;
_userManager = userManager;
}
private async Task<AuthenticationTicket> CreateTicketAsync(OpenIdConnectRequest request, ApplicationUser user)
{
// Create a new ClaimsPrincipal containing the claims that
// will be used to create an id_token, a token or a code.
ClaimsPrincipal principal = await _signInManager.CreateUserPrincipalAsync(user);
// Create a new authentication ticket holding the user identity.
AuthenticationTicket ticket = new AuthenticationTicket(
principal, new AuthenticationProperties(),
OpenIdConnectServerDefaults.AuthenticationScheme);
// Set the list of scopes granted to the client application.
// Note: the offline_access scope must be granted
// to allow OpenIddict to return a refresh token.
ticket.SetScopes(new[] {
OpenIdConnectConstants.Scopes.OpenId,
OpenIdConnectConstants.Scopes.Email,
OpenIdConnectConstants.Scopes.Profile,
OpenIdConnectConstants.Scopes.OfflineAccess,
OpenIddictConstants.Scopes.Roles
}.Intersect(request.GetScopes()));
ticket.SetResources("resource-server");
// 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 ticket.Principal.Claims)
{
// Never include the security stamp in the access and identity tokens, as it's a secret value.
if (claim.Type == _identityOptions.Value.ClaimsIdentity.SecurityStampClaimType)
continue;
List<string> destinations = new List<string>
{
OpenIdConnectConstants.Destinations.AccessToken
};
// Only add the iterated claim to the id_token if the corresponding scope was granted to the client application.
// The other claims will only be added to the access_token, which is encrypted when using the default format.
if (claim.Type == OpenIdConnectConstants.Claims.Name &&
ticket.HasScope(OpenIdConnectConstants.Scopes.Profile) ||
claim.Type == OpenIdConnectConstants.Claims.Email &&
ticket.HasScope(OpenIdConnectConstants.Scopes.Email) ||
claim.Type == OpenIdConnectConstants.Claims.Role &&
ticket.HasScope(OpenIddictConstants.Claims.Roles))
{
destinations.Add(OpenIdConnectConstants.Destinations.IdentityToken);
}
claim.SetDestinations(OpenIdConnectConstants.Destinations.AccessToken);
}
return ticket;
}
[HttpPost("~/connect/token"), Produces("application/json")]
public async Task<IActionResult> Exchange(OpenIdConnectRequest request)
{
// if you prefer not to bind the request as a parameter, you can still use:
// OpenIdConnectRequest request = HttpContext.GetOpenIdConnectRequest();
Debug.Assert(request.IsTokenRequest(),
"The OpenIddict binder for ASP.NET Core MVC is not registered. " +
"Make sure services.AddOpenIddict().AddMvcBinders() is correctly called.");
if (!request.IsPasswordGrantType())
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.UnsupportedGrantType,
ErrorDescription = "The specified grant type is not supported."
});
}
ApplicationUser user = await _userManager.FindByNameAsync(request.Username);
if (user == null)
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The username/password couple is invalid."
});
}
// Ensure the user is allowed to sign in.
if (!await _signInManager.CanSignInAsync(user))
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The specified user is not allowed to sign in."
});
}
// Reject the token request if two-factor authentication has been enabled by the user.
if (_userManager.SupportsUserTwoFactor && await _userManager.GetTwoFactorEnabledAsync(user))
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The specified user is not allowed to sign in."
});
}
// Ensure the user is not already locked out.
if (_userManager.SupportsUserLockout && await _userManager.IsLockedOutAsync(user))
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The username/password couple is invalid."
});
}
// Ensure the password is valid.
if (!await _userManager.CheckPasswordAsync(user, request.Password))
{
if (_userManager.SupportsUserLockout)
await _userManager.AccessFailedAsync(user);
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The username/password couple is invalid."
});
}
if (_userManager.SupportsUserLockout)
await _userManager.ResetAccessFailedCountAsync(user);
// Create a new authentication ticket.
AuthenticationTicket ticket = await CreateTicketAsync(request, user);
var result = SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
return result;
// return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}
[HttpGet("~/connect/logout")]
public async Task<IActionResult> Logout()
{
// Extract the authorization request from the ASP.NET environment.
OpenIdConnectRequest request = HttpContext.GetOpenIdConnectRequest();
// Ask ASP.NET Core Identity to delete the local and external cookies created
// when the user agent is redirected from the external identity provider
// after a successful authentication flow (e.g Google or Facebook).
await _signInManager.SignOutAsync();
// Returning a SignOutResult will ask OpenIddict to redirect the user agent
// to the post_logout_redirect_uri specified by the client application.
return SignOut(OpenIdConnectServerDefaults.AuthenticationScheme);
}
// http://openid.net/specs/openid-connect-core-1_0.html#UserInfo
[Authorize]
[HttpGet("~/connect/userinfo")]
public async Task<IActionResult> GetUserInfo()
{
ApplicationUser user = await _userManager.GetUserAsync(User);
// to simplify, in this demo we just have 1 role for users: either admin or editor
string sRole = await _userManager.IsInRoleAsync(user, "admin")
? "admin"
: "editor";
// http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
return Ok(new
{
sub = user.Id,
given_name = user.FirstName,
family_name = user.LastName,
name = user.UserName,
user.Email,
email_verified = user.EmailConfirmed,
roles = sRole
});
}
}
As mentioned in this blog post, the token format used by OpenIddict slightly changed recently, which makes tokens issued by the latest OpenIddict bits incompatible with the old OAuth2 validation middleware version you're using.
Migrate to AspNet.Security.OAuth.Validation 1.0.0 and it should work.
I have a Web API with tons of methods that all require a bearer token to be present in order to be used. These methods all extract information from the bearer token.
I want to test whether the API is properly populating the bearer token upon its generation. I'm using the Microsoft.Owin.Testing framework to write my tests. I have a test that looks like this:
[TestMethod]
public async Task test_Login()
{
using (var server = TestServer.Create<Startup>())
{
var req = server.CreateRequest("/authtoken");
req.AddHeader("Content-Type", "application/x-www-form-urlencoded");
req.And(x => x.Content = new StringContent("grant_type=password&username=test&password=1234", System.Text.Encoding.ASCII));
var response = await req.GetAsync();
// Did the request produce a 200 OK response?
Assert.AreEqual(response.StatusCode, System.Net.HttpStatusCode.OK);
// Retrieve the content of the response
string responseBody = await response.Content.ReadAsStringAsync();
// this uses a custom method for deserializing JSON to a dictionary of objects using JSON.NET
Dictionary<string, object> responseData = deserializeToDictionary(responseBody);
// Did the response come with an access token?
Assert.IsTrue(responseData.ContainsKey("access_token"));
}
}
So I'm able to retrieve the string that represents the token. But now I want to actually access that token's contents, and make sure that certain claims were provided.
Code that I would use in an actual authenticated method to check the claims looks like this:
var identity = (ClaimsIdentity)User.Identity;
IEnumerable<Claim> claims = identity.Claims;
var claimTypes = from x in claims select x.Type;
if (!claimTypes.Contains("customData"))
throw new InvalidOperationException("Not authorized");
So what I want to be able to do is, within my test itself, provide the bearer token string and reeceive a User.Identity object or in some other way gain access to the claims that token contains. This is how I want to test whether my method is properly adding the necessary claims to the token.
The "naive" approach could be to write a method in my API that simply returns all the claims in the bearer token it is given. But it feels like this should be unnecessary. ASP.NET is somehow decoding the given token to an object before my controller's method is called. I want to replicate the same action on my own, in my test code.
Can this be done? If so, how?
EDIT: My OWIN startup class instantiates an authentication token provider that I have coded which handles authentication and token generation. In my startup class I have this:
public void Configuration(IAppBuilder app)
{
// Setup configuration object
HttpConfiguration config = new HttpConfiguration();
// Web API configuration and services
// Configure Web API to use only bearer token authentication.
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
// configure the OAUTH server
OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
//AllowInsecureHttp = false,
AllowInsecureHttp = true, // THIS HAS TO BE CHANGED BEFORE PUBLISHING!
TokenEndpointPath = new PathString("/authtoken"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
Provider = new API.Middleware.MyOAuthProvider()
};
// Now we setup the actual OWIN pipeline.
// setup CORS support
// in production we will only allow from the correct URLs.
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
// Token Generation
app.UseOAuthAuthorizationServer(OAuthServerOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
// insert actual web API and we're off!
app.UseWebApi(config);
}
Here is the relevant code from my OAuth provider:
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
// Will be used near end of function
bool isValidUser = false;
// Simple sanity check: all usernames must begin with a lowercase character
Match testCheck = Regex.Match(context.UserName, "^[a-z]{1}.+$");
if (testCheck.Success==false)
{
context.SetError("invalid_grant", "Invalid credentials.");
return;
}
string userExtraInfo;
// Here we check the database for a valid user.
// If the user is valid, isValidUser will be set to True.
// Invalid authentications will return null from the method below.
userExtraInfo = DBAccess.getUserInfo(context.UserName, context.Password);
if (userExtraInfo != null) isValidUser = true;
if (!isValidUser)
{
context.SetError("invalid_grant", "Invalid credentials.");
return;
}
// The database validated the user. We will include the username in the token.
string userName = context.UserName;
// generate a claims object
var identity = new ClaimsIdentity(context.Options.AuthenticationType);
// add the username to the token
identity.AddClaim(new Claim(ClaimTypes.Sid, userName));
// add the custom data on the user to the token.
identity.AddClaim(new Claim("customData", userExtraInfo));
// store token expiry so the consumer can determine expiration time
DateTime expiresAt = DateTime.Now.Add(context.Options.AccessTokenExpireTimeSpan);
identity.AddClaim(new Claim("expiry", expiresAt.ToString()));
// Validate the request and generate a token.
context.Validated(identity);
}
The unit test would want to ensure that the customData claim is in fact present in the authentication token. So thus my need for a way to evaluate the token provided to test which claims it contains.
EDIT 2: I've spent some time looking over the Katana source code and searching out some other posts online, and it looks like it's important that I'm hosting this app on IIS, so I would be using SystemWeb. It looks like SystemWeb uses Machine Key encryption for the token. It also looks like the AccessTokenFormat parameter in the options is relevant here.
So now what I'm wondering is if I can instantiate my own "decoder" based on this knowledge. Assuming I will only ever be hosting on IIS, can I instantiate a decoder that can then decode the token and convert it into a Claims object?
The docs on this are kind of sparse and the code seems to throw you all over the place, a lot to try to keep straight in my head.
EDIT 3: I found a project that contains what is supposed to be a bearer token deserializer. I adapted the code in its "API" library and have been trying to use it to decrypt the tokens generated by my API.
I generated a <machineKey...> value using a PowerShell script from Microsoft and placed it both in the Web.config file of the API itself and the App.confg file in the test project.
The tokens still fail to decrypt, however. I receive a Exception thrown: System.Security.Cryptography.CryptographicException with the message "Error occurred during a cryptographic operation." The following is the stacktrace of the error:
at System.Web.Security.Cryptography.HomogenizingCryptoServiceWrapper.HomogenizeErrors(Func`2 func, Byte[] input)
at System.Web.Security.Cryptography.HomogenizingCryptoServiceWrapper.Unprotect(Byte[] protectedData)
at System.Web.Security.MachineKey.Unprotect(ICryptoServiceProvider cryptoServiceProvider, Byte[] protectedData, String[] purposes)
at System.Web.Security.MachineKey.Unprotect(Byte[] protectedData, String[] purposes)
at MyAPI.Tests.BearerTokenAPI.MachineKeyDataProtector.Unprotect(Byte[] protectedData) in D:\Source\MyAPI\MyAPI.WebAPI.Tests\BearerTokenAPI.cs:line 251
at MyAPI.Tests.BearerTokenAPI.SecureDataFormat`1.Unprotect(String protectedText) in D:\Source\MyAPI\MyAPI.WebAPI.Tests\BearerTokenAPI.cs:line 287
At this point I'm stumped. With the MachineKey value set to the same across the entire project, I don't see why I'm unable to decrypt the tokens. I'm guessing the cryptographic error is being deliberately vague, but I am not sure where to start with figuring this out now.
And all I wanted to do was test that the token contains the desired data in a unit test.... :-)
I was finally able to figure out a solution. I added a public variable to my Startup class that exposes the OAuthBearerAuthenticationOptions object passed into the UseBearerTokenAuthentication method. From that object, I'm able to call AccessTokenFormat.Unprotect and get a decrypted token.
I also rewrote my test to instantiate the Startup class separately, so that I have access to the value from within the test.
I still don't understand why the MachineKey thing isn't working, why I'm not able to directly unprotect the token. It would seem that as long as the MachineKey's match, I should be able to decrypt the token, even manually. But at least this seems to work, even if it's not the best solution.
This could probably be done more cleanly, for instance perhaps the Startup class could somehow detect if it's being started under test and pass the object to the test class in some other fashion rather than leaving it hanging out there in the breeze. But for now this seems to do exactly what I needed.
My startup class exposes the variable this way:
public partial class Startup
{
public OAuthBearerAuthenticationOptions oabao;
public void Configuration(IAppBuilder app)
{
// repeated code omitted
// Token Generation
app.UseOAuthAuthorizationServer(OAuthServerOptions);
oabao = new OAuthBearerAuthenticationOptions();
app.UseOAuthBearerAuthentication(oabao);
// insert actual web API and we're off!
app.UseWebApi(config);
}
}
My test now looks like this:
[TestMethod]
public async Task Test_SignIn()
{
Startup owinStartup = new Startup();
Action<IAppBuilder> owinStartupAction = new Action<IAppBuilder>(owinStartup.Configuration);
using (var server = TestServer.Create(owinStartupAction))
{
var req = server.CreateRequest("/authtoken");
req.AddHeader("Content-Type", "application/x-www-form-urlencoded");
// repeated code omitted
// Is the access token of an appropriate length?
string access_token = responseData["access_token"].ToString();
Assert.IsTrue(access_token.Length > 32);
AuthenticationTicket token = owinStartup.oabao.AccessTokenFormat.Unprotect(access_token);
// now I can check whatever I want on the token.
}
}
Hopefully all my efforts will help someone else trying to do something similar.
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.