Closed halter73 closed 1 year ago
To provide self-hosted identity auth endpoints suitable for SPA apps and non-browser apps.
// Microsoft.AspNetCore.Identity.dll (In the ASP.NET Core shared framework)
namespace Microsoft.AspNetCore.Identity;
public class IdentityConstants
{
private const string IdentityPrefix = "Identity"
public static readonly string ApplicationScheme = IdentityPrefix + ".Application";
+ public static readonly string BearerScheme = IdentityPrefix + ".Bearer";
// ...
}
// Microsoft.AspNetCore.Identity.Endpoints.dll (Proposed as NuGet package)
// Could be moved to the shared framework and/or merged with Microsoft.AspNetCore.Identity.dll
namespace Microsoft.AspNetCore.Identity;
public sealed class IdentityBearerOptions : AuthenticationSchemeOptions
{
public TimeSpan BearerTokenExpiration { get; set; } = TimeSpan.FromHours(1);
public ISecureDataFormat<AuthenticationTicket>? BearerTokenProtector { get; set; }
public IDataProtectionProvider? DataProtectionProvider { get; set; }
public string? MissingBearerTokenFallbackScheme { get; set; }
public Func<HttpContext, ValueTask<string?>>? ExtractBearerToken { get; set; }
}
namespace Microsoft.Extensions.DependencyInjection;
// M.E.D.I namespace matches AddCookie, AddJwtBearer, etc... despite AuthenticationBuilder living elsewhere.
// The most consistent thing would be to call this IdentityBearerExtensions though.
public static class IdentityBearerAuthenticationBuilderExtensions
{
public static AuthenticationBuilder AddIdentityBearer(this AuthenticationBuilder builder, Action<IdentityBearerOptions>? configure);
}
public static class IdentityEndpointsServiceCollectionExtensions
{
// Adds everything required for MapIdentity including both the identity bearer and cookie auth handlers
public static IdentityBuilder AddIdentityEndpoints<TUser>(this IServiceCollection services)
where TUser : class, new();
public static IdentityBuilder AddIdentityEndpoints<TUser>(this IServiceCollection services, Action<IdentityOptions> configure)
where TUser : class, new();
// Adds everything erquired for MapIdenity except the auth handlers.
// AddIdentityCookie and/or AddIdentityBearer need to be called seperately.
public static IdentityBuilder AddIdentityEndpointsCore<TUser>(this IServiceCollection services, Action<IdentityOptions> configure)
where TUser : class, new();
}
namespace Microsoft.AspNetCore.Routing;
public static class IdentityEndpointRouteBuilderExtensions
{
public static IEndpointConventionBuilder MapIdentity<TUser>(this IEndpointRouteBuilder endpoints) where TUser : class, new();
}
using System.Security.Claims;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization();
builder.Services.AddDbContext<ApplicationDbContext>(
options => options.UseSqlite(builder.Configuration["ConnectionString"]));
builder.Services.AddIdentityEndpoints<IdentityUser>()
.AddEntityFrameworkStores<ApplicationDbContext>();
var app = builder.Build();
app.MapGroup("/identity").MapIdentity<IdentityUser>();
app.MapGet("/requires-auth", (ClaimsPrincipal user) => $"Hello, {user.Identity?.Name}!").RequireAuthorization();
app.Run();
public class ApplicationDbContext : IdentityDbContext<IdentityUser>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{
}
}
// Same usings as above
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(IdentityConstants.BearerScheme)
.AddIdentityBearer(options => { });
builder.Services.AddAuthorization();
builder.Services.AddDbContext<ApplicationDbContext>(
options => options.UseSqlite(builder.Configuration["ConnectionString"]));
builder.Services.AddIdentityEndpointsCore<IdentityUser>(options => { })
.AddEntityFrameworkStores<ApplicationDbContext>();
var app = builder.Build();
// Same as above
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization();
builder.Services.AddDbContext<ApplicationDbContext>(
options => options.UseSqlite(builder.Configuration["ConnectionString"]));
builder.Services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
var app = builder.Build();
OR
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(IdentityConstants.ApplicationScheme)
.AddIdentityCookies();
builder.Services.AddAuthorization();
builder.Services.AddDbContext<ApplicationDbContext>(
options => options.UseSqlite(builder.Configuration["ConnectionString"]));
builder.Services.AddIdentityEndpointsCore<IdentityUser>(options => { })
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddSignInManager();
var app = builder.Build();
Assume httpClient
, username
and password
are already initialized.
// Email confirmation will be added later.
// The request body is: { "username": "<username>", "password": "<password>" }
await httpClient.PostAsJsonAsync("/identity/v1/register", new { username, password });
// 2fa flow will be added later.
// The request body is: { "username": "<username>", "password": "<password>" }
var loginResponse = await httpClient.PostAsJsonAsync("/identity/v1/login", new { username, password });
// loginResponse is similar to the "Access Token Response" defined in the OAuth 2 spec
// {
// "token_type": "Bearer",
// "access_token": "...",
// "expires_in": 3600
// }
// refresh token is likely to be added later
var loginContent = await loginResponse.Content.ReadFromJsonAsync<JsonElement>();
var accessToken = loginContent.GetProperty("access_token").GetString();
httpClient.DefaultRequestHeaders.Authorization = new("Bearer", accessToken);
Console.WriteLine(await httpClient.GetStringAsync("/requires-auth"));
// HttpClientHandler.UseCookies is true by default on supported platforms.
// The request body is: { "username": "<username>", "password": "<password>", "cookieMode": true }
await httpClient.PostAsJsonAsync("/identity/v1/login", new { username, password, cookieMode = true });
Console.WriteLine(await cookieClient.GetStringAsync("/requires-auth"));
MapIdentity
could optionally take a string for the route pattern so you do not have to call app.MapGroup("/identity")
or similar to prefix the endpoints with anything more than v1
.AddIdentityEndpoints
could be renamed to AddIdentityApis
or AddIdentityApiEndpoints
.
IdentityBearerOptions
could be put in a new Microsoft.AspNetCore.Identity.Bearer
namespace
IdentityBearerOptions.Events
.IdentityBearerAuthenticationBuilderExtensions
could be shortened to IdentityBearerExtensions
CookieExtensions.AddCookie
and JwtBearerExtensions.AddJwtBearer
use shorter names.JwtBearerEvents.OnMessageReceived
.
authBuilder.AddJwtBearer(options =>
{
// Events is marked non-nullable but is appears to be null here.
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.HttpContext.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;
}
};
});
vs:
// or services.Configure<IdentityBearerOptions>(IdentityConstants.BearerScheme, options =>
authBuilder.AddIdentityBearer(options =>
{
options.ExtractBearerToken = httpContext =>
{
var accessToken = httpContext.Request.Query["access_token"];
// If the request is for our hub...
var path = httpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs/chat"))
{
return new(accessToken);
}
return default;
}
};
})
This is low risk since it's entirely new API. And all of it is in a new NuGet package aside from IdentityConstants.BearerScheme
.
Thank you for submitting this for API review. This will be reviewed by @dotnet/aspnet-api-review at the next meeting of the ASP.NET Core API Review group. Please ensure you take a look at the API review process documentation and ensure that:
I'm temporarily removing api-ready-for-review
, since I'll be OOF for the next two weeks. I want to be there for the review.
I think the API proposal is complete though, and would appreciate any feedback people have prior to the official API review.
API Review Notes:
MapIdentity
out into another package.AddIdentityBearer
go into?IdentityBearerAuthenticationHandler
public?
MissingBearerTokenFallbackScheme
and use a composite policy scheme to fall back to cookies if necessary after the BearerTokenHandler fails to authenticate.BearerTokenDefaults
.Options.DataProtectionProvider
?
TicketDataFormat
.MapIdentity
-> MapIdentityApi
MapIdentityApis
? We think non-plural API could be encapsulate many endpoints as part of the overall API.MapIdentityApi
?
Add
methods anyway.API Approved!
// Microsoft.AspNetCore.Identity.dll (In the ASP.NET Core shared framework)
namespace Microsoft.AspNetCore.Identity;
public class IdentityConstants
{
private const string IdentityPrefix = "Identity"
public static readonly string ApplicationScheme = IdentityPrefix + ".Application";
+ public static readonly string BearerScheme = IdentityPrefix + ".Bearer";
// ...
}
// Microsoft.AspNetCore.Authentication.BearerToken.dll (Shared framework)
namespace Microsoft.AspNetCore.Authentication.BearerToken;
public static class BearerTokenDefaults
{
// JwtBearer's default "AuthenticationScheme" is "Bearer" :(
public const string AuthenticationScheme = "BearerToken";
}
public sealed class BearerTokenOptions : AuthenticationSchemeOptions
{
public TimeSpan BearerTokenExpiration { get; set; } = TimeSpan.FromHours(1);
public ISecureDataFormat<AuthenticationTicket>? BearerTokenProtector { get; set; }
public Func<HttpContext, ValueTask<string?>>? ExtractBearerToken { get; set; }
}
namespace Microsoft.Extensions.DependencyInjection;
public static class BearerTokenExtensions
{
public static AuthenticationBuilder AddBearerToken(this AuthenticationBuilder builder);
public static AuthenticationBuilder AddBearerToken(this AuthenticationBuilder builder, string authenticationScheme);
public static AuthenticationBuilder AddBearerToken(this AuthenticationBuilder builder, Action<BearerTokenOptions>? configure);
public static AuthenticationBuilder AddBearerToken(this AuthenticationBuilder builder, string authenticationScheme, Action<BearerTokenOptions>? configure);
}
// Microsoft.AspNetCore.Identity.dll (Shared framework)
namespace Microsoft.Extensions.DependencyInjection;
public static class IdentityApiEndpointsServiceCollectionExtensions
{
// Adds everything required for MapIdentity including both the identity bearer and cookie auth handlers
public static IdentityBuilder AddIdentityApiEndpoints<TUser>(this IServiceCollection services)
where TUser : class, new();
public static IdentityBuilder AddIdentityApiEndpoints<TUser>(this IServiceCollection services, Action<IdentityOptions> configure)
where TUser : class, new();
}
namespace Microsoft.AspNetCore.Identity;
public static class IdentityApiEndpointsIdentityBuilderExtensions
{
// Adds everything required for MapIdentity except the auth handlers.
// AddIdentityCookies and/or AddIdentityBearer need to be called separately.
public static IdentityBuilder AddApiEndpoints(this IdentityBuilder builder);
}
namespace Microsoft.AspNetCore.Routing;
public static class IdentityApiEndpointRouteBuilderExtensions
{
public static IEndpointConventionBuilder MapIdentityApi<TUser>(this IEndpointRouteBuilder endpoints) where TUser : class, new();
}
Using MapGroup in MapIdentityApi Is NOT Clear!
app.MapIdentityApi<IdentityUser>(basePath: "/identity");
This will expose Login and Registration endpoints.