-
Notifications
You must be signed in to change notification settings - Fork 10.3k
Add API endpoints for generating identity tokens #47227
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Background and MotivationTo provide self-hosted identity auth endpoints suitable for SPA apps and non-browser apps. Proposed API// 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();
} Usage ExamplesServerBearer token and cookies enabledusing 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)
{
}
} Bearer token only// 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 Cookie onlyvar 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(); ClientAssume Register// Email confirmation will be added later.
// The request body is: { "username": "<username>", "password": "<password>" }
await httpClient.PostAsJsonAsync("/identity/v1/register", new { username, password }); Login (Bearer token)// 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")); Login (Cookie)// 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")); Alternative Designs
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;
}
};
}) RisksThis is low risk since it's entirely new API. And all of it is in a new NuGet package aside from |
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 I think the API proposal is complete though, and would appreciate any feedback people have prior to the official API review. |
API Review Notes:
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.
The text was updated successfully, but these errors were encountered: