Setting up custom scopes (The specified scope type is not compatible with the Entity Framework Core stores)
See original GitHub issueConfirm you’ve already contributed to this project or that you sponsor it
- I confirm I’m a sponsor or a contributor
Version
3.x
Question
Hey Kevin, great initiative with openiddict, I love the framework and I have been trying to wrap my head around some of the features. I have been through most of the samples and I have been through “all” relevant issues to my problem and of course StackOverflow. Unfortunately, I cant seem to find a good tutorial, readme, or answer to my issues.
My goal is to create a centralized authentication server where I can allow certain clients to ask for permissions for certain audiences. E.g.
- client1 can ask for auth for resource server A
- client2 can ask for auth for resource server A & resource server B
- client 3 can ask for auth for resource server C
to my knowledge:
- we use audience to differentiate these resource servers(?)
- we can also also create custom scopes for custom resource servers
- e.g. we can say scope ‘scopeA’ is only valid for ‘resource server B’
1. how do i set an audience for a client? looking at the /userinfo it seems as if clientid == audience, is this correct?
2. How to create custom scopes? What i’ve found is required to setup custom scopes is
- register opendict
options.UseOpenIddict<CustomApplication, CustomAuthorization, CustomScope, CustomToken, long>();
- replace default entities
options.UseEntityFrameworkCore().UseDbContext<MyDbContext>().ReplaceDefaultEntities<CustomApplication, CustomAuthorization, CustomScope, CustomToken, long>();
- register scope:
options.RegisterScopes(Scopes.Email, Scopes.Profile, Scopes.Roles, Scopes.OfflineAccess, Scopes.OpenId, "custom_scope_example");
- create models that inherit from their respectable parents (maybe step 1): documentation
- Register the models as a DbSet
public DbSet<CustomApplication> Application { get; set; }
public DbSet<CustomAuthorization> Authorization { get; set; }
public DbSet<CustomScope> Scope{ get; set; }
public DbSet<CustomToken> Token { get; set; }
public DbSet<OpenIddictEntityFrameworkCoreScope> OpenIddictEntityFrameworkCoreScope { get; set; }
//Why do I have to add OpenIddictEntityFrameworkCoreScope? Is this documented anywhere?
if all that goes well and I get my custom_scope_example
into the database I get another error (which is the one I’ve been really struggling with):
Postman call: (scope is irrelevant here, same error comes regardless)
System.InvalidOperationException: The specified scope type is not compatible with the Entity Framework Core stores.
When enabling the Entity Framework Core stores, make sure you use the built-in 'OpenIddictEntityFrameworkCoreScope' entity or a custom entity that inherits from the generic 'OpenIddictEntityFrameworkCoreScope' entity.
at OpenIddict.EntityFrameworkCore.OpenIddictEntityFrameworkCoreScopeStoreResolver.<Get>b__4_0[TScope](Type key)
at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
at OpenIddict.EntityFrameworkCore.OpenIddictEntityFrameworkCoreScopeStoreResolver.Get[TScope]()
at OpenIddict.Core.OpenIddictScopeCache`1..ctor(IOptionsMonitor`1 options, IOpenIddictScopeStoreResolver resolver)
at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
at System.Reflection.ConstructorInvoker.Invoke(Object obj, IntPtr* args, BindingFlags invokeAttr)
at System.Reflection.RuntimeConstructorInfo.Invoke(BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.DynamicServiceProviderEngine.<>c__DisplayClass2_0.<RealizeService>b__0(ServiceProviderEngineScope scope)
at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope)
at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.GetService(IServiceProvider sp, Type type, Type requiredBy, Boolean isDefaultParameterRequired)
at lambda_method229(Closure, IServiceProvider, Object[])
at Microsoft.AspNetCore.Mvc.Controllers.ControllerFactoryProvider.<>c__DisplayClass6_0.<CreateControllerFactory>g__CreateController|0(ControllerContext controllerContext)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|25_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeFilterPipelineAsync()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
at Elmah.Io.AspNetCore.ElmahIoMiddleware.Invoke(HttpContext context) in /_/src/Elmah.Io.AspNetCore/ElmahIoMiddleware.cs:line 40
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
If anyone needs to see more code:
program.cs
using FaID.Context;
using FaID.Models;
using FaID.Services.AuthorizationServer;
using FaID.Services.Data;
using FaID.Settings;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Abstractions;
using System.Data;
using System.Security.Cryptography.X509Certificates;
using static OpenIddict.Abstractions.OpenIddictConstants;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddDbContext<FaDbContext>(opt =>
{
opt.UseNpgsql(builder.Configuration.GetConnectionString("dev"));
opt.UseOpenIddict<CustomApplication, CustomAuthorization, CustomScope, CustomToken, long>();
});
builder.Services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.UserNameClaimType = OpenIddictConstants.Claims.Name;
options.ClaimsIdentity.UserIdClaimType = OpenIddictConstants.Claims.Subject;
options.ClaimsIdentity.RoleClaimType = OpenIddictConstants.Claims.Role;
});
builder.Services.AddOpenIddict().AddCore(options =>
{
options.UseEntityFrameworkCore().UseDbContext<FaDbContext>().ReplaceDefaultEntities<CustomApplication, CustomAuthorization, CustomScope, CustomToken, long>();
})
.AddServer(options =>
{
options
.AllowClientCredentialsFlow()
.AllowPasswordFlow()
.AllowRefreshTokenFlow();
options.RegisterScopes(Scopes.Email, Scopes.Profile, Scopes.Roles, Scopes.OfflineAccess, Scopes.OpenId, "faid_client_scope");
options.SetTokenEndpointUris("/token")
.SetAuthorizationEndpointUris("/connect/authorize")
.SetUserinfoEndpointUris("/connect/userinfo")
.SetVerificationEndpointUris("/connect/verify");
//temp code
X509Certificate2 privateKey;
var bytes = File.ReadAllBytes(builder.Configuration["Auth:PrivateKeyPath"] ?? "");
privateKey = new X509Certificate2(bytes, "password");
X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly);
X509Certificate2 encryptionKey;
var bytes2 = File.ReadAllBytes(builder.Configuration["Auth:EncryptionkeyPath"] ?? "");
encryptionKey = new X509Certificate2(bytes2, "password");
X509Store store2 = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly);
options
.AddSigningCertificate(privateKey)
.AddEncryptionCertificate(encryptionKey).DisableAccessTokenEncryption();
options.SetAccessTokenLifetime(TimeSpan.FromMinutes(60));
options.SetRefreshTokenLifetime(TimeSpan.FromDays(60));
options
.UseAspNetCore()
.EnableTokenEndpointPassthrough()
.EnableAuthorizationEndpointPassthrough();
}
).AddValidation(options =>
{
options.AddAudiences("faid_client");
options.UseLocalServer();
options.UseAspNetCore();
});
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = OpenIddictConstants.Schemes.Bearer;
options.DefaultChallengeScheme = OpenIddictConstants.Schemes.Bearer;
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("faid_client", builder =>
{
builder.RequireAuthenticatedUser();
builder.RequireAssertion(context => context.User.HasScope("faid_client_scope"));
});
//Add openiddict client
builder.Services.AddHostedService<TestData>();
builder.Services.AddIdentity<User, IdentityRole>()
.AddSignInManager()
.AddEntityFrameworkStores<FaDbContext>()
.AddDefaultTokenProviders();
builder.Services.Configure<IdentityOptions>(o =>
{
// Password settings.
o.Password.RequiredUniqueChars = 0;
o.Password.RequiredLength = 1;
o.Password.RequireNonAlphanumeric = false;
o.Password.RequireDigit = false;
o.Password.RequireUppercase = false;
// User settings.
o.User.AllowedUserNameCharacters =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-.@";
o.User.RequireUniqueEmail = false;
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseCors(o => {
o.AllowAnyHeader();
o.AllowAnyMethod();
o.SetIsOriginAllowedToAllowWildcardSubdomains().WithOrigins("http://localhost:3000/", "\"http://localhost:3000/\"", "http://localhost:3000", "http://localhost:3000/*", "http://localhost:3000/signin");
o.AllowCredentials();
});
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.UseElmahIo();
app.Run();
testdata.cs
(client & custom scope)
using FaID.Context;
using FaID.Models;
using Microsoft.EntityFrameworkCore;
using OpenIddict.Abstractions;
using OpenIddict.Core;
using OpenIddict.EntityFrameworkCore.Models;
using static OpenIddict.Abstractions.OpenIddictConstants;
namespace FaID.Services.AuthorizationServer
{
public class TestData : IHostedService
{
private readonly IServiceProvider _serviceProvider;
public TestData(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
using var scope = _serviceProvider.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<FaDbContext>();
await context.Database.EnsureCreatedAsync(cancellationToken);
var manager = scope.ServiceProvider.GetRequiredService<IOpenIddictApplicationManager>();
var client = await manager.FindByClientIdAsync("postman", cancellationToken);
if (client is null)
{
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = "postman",
ClientSecret = "postman-secret",
DisplayName = "Postman Client",
RedirectUris = { new Uri("https://oauth.pstmn.io/v1/callback") },
Permissions =
{
OpenIddictConstants.Permissions.Endpoints.Authorization,
OpenIddictConstants.Permissions.Endpoints.Token,
OpenIddictConstants.Permissions.GrantTypes.RefreshToken,
OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode,
OpenIddictConstants.Permissions.GrantTypes.ClientCredentials,
OpenIddictConstants.Permissions.GrantTypes.Password,
OpenIddictConstants.Permissions.Scopes.Email,
OpenIddictConstants.Permissions.Scopes.Roles,
OpenIddictConstants.Permissions.Scopes.Address,
OpenIddictConstants.Permissions.Scopes.Phone,
OpenIddictConstants.Permissions.Prefixes.Scope + "faid_client_scope",
OpenIddictConstants.Permissions.ResponseTypes.Code,
OpenIddictConstants.Permissions.ResponseTypes.IdToken,
OpenIddictConstants.Permissions.ResponseTypes.CodeIdToken,
OpenIddictConstants.Permissions.ResponseTypes.Token
},
}, cancellationToken);
}
var scopeManager = scope.ServiceProvider.GetRequiredService<OpenIddictScopeManager<OpenIddictEntityFrameworkCoreScope>>();
if (await scopeManager.FindByNameAsync("faid_client_scope") == null)
await scopeManager.CreateAsync(new OpenIddictScopeDescriptor
{
DisplayName = "FAICS",
Name = "faid_client_scope",
Resources = { "faid_client" }
}, cancellationToken);
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
}
FaDbContext.cs
public class FaDbContext : IdentityDbContext<User>
{
public DbSet<User> Users { get; set; }
public DbSet<CustomApplication> Application { get; set; }
public DbSet<CustomAuthorization> Authorization { get; set; }
public DbSet<CustomScope> Scope{ get; set; }
public DbSet<CustomToken> Token { get; set; }
public DbSet<OpenIddictEntityFrameworkCoreScope> OpenIddictEntityFrameworkCoreScope { get; set; }
public FaDbContext(DbContextOptions<FaDbContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
}
}
openiddictentities.cs
(mainly for testing purposes)
Do i need this class just to create custom scopes with audience?
public class CustomApplication : OpenIddictEntityFrameworkCoreApplication<long, CustomAuthorization, CustomToken> {public string CustomProperty { get; set; } = "";}
public class CustomAuthorization : OpenIddictEntityFrameworkCoreAuthorization<long, CustomApplication, CustomToken> {public string CustomProperty { get; set; } = "";}
public class CustomScope : OpenIddictEntityFrameworkCoreScope<long>{ public string CustomProperty { get; set; } = "";}
public class CustomToken : OpenIddictEntityFrameworkCoreToken<long, CustomApplication, CustomAuthorization>{public string CustomProperty { get; set; } = "";}
database
tables:
OpenIddictEntityFrameworkCoreScope table:
“5897161b-3d85-401f-a144-2e75ea00a4fb” | “5a3c21bb-86b1-4a43-ab80-eacef19cd4c9” | “FAICS” | “faid_client_scope” | “[”“faid_client”“]” | “[”“ept:authorization”“,”“ept:token”“,”“gt:refresh_token”“,”“gt:authorization_code”“,”“gt:client_credentials”“,”“gt:password”“,”“scp:email”“,”“scp:roles”“,”“scp:address”“,”“scp:phone”“,”“scp:faid_client_scope”“,”“rst:code”“,”“rst:id_token”“,”“rst:code id_token”“,”“rst:token”“]” | “[”“https://oauth.pstmn.io/v1/callback”“]” | “confidential” |
---|---|---|---|---|---|---|---|
OpeniddictScopes table (empty):
OpenIddictApplications:
1 | “postman” | “AQAAAAEAACcQAAAAEEpBV+j+D0E18NMbc64js7A65HArrCVdiAXUxfqm+TsapbG+AytKwmUHi9bGFgi1pg==” | “b77de4d4-5634-4e7a-a707-59acdb71eb6c” | “Postman Client” | “[”“ept:authorization”“,”“ept:token”“,”“gt:refresh_token”“,”“gt:authorization_code”“,”“gt:client_credentials”“,”“gt:password”“,”“scp:email”“,”“scp:roles”“,”“scp:address”“,”“scp:phone”“,”“scp:faid_client_scope”“,”“rst:code”“,”“rst:id_token”“,”“rst:code id_token”“,”“rst:token”“]” | “[”“https://oauth.pstmn.io/v1/callback”“]” | “confidential” |
---|---|---|---|---|---|---|---|
Id: 1
CustomProperty: “”
ClientId: “postman”
ClientSecret: “AQAAAAEAACcQAAAAEEpBV+j+D0E18NMbc64js7A65HArrCVdiAXUxfqm+TsapbG+AytKwmUHi9bGFgi1pg==”
ConcurrencyToken: “b77de4d4-5634-4e7a-a707-59acdb71eb6c”
ConsentType: null
Display Name: “Postman Client”
Display Names: null
Permissions: “[""ept:authorization"",""ept:token"",""gt:refresh_token"",""gt:authorization_code"",""gt:client_credentials"",""gt:password"",""scp:email"",""scp:roles"",""scp:address"",""scp:phone"",""scp:faid_client_scope"",""rst:code"",""rst:id_token"",""rst:code id_token"",""rst:token""]" PostLogoutRedirectUris: null RedirectUris: "[""https://oauth.pstmn.io/v1/callback""]
”
Type: “confidential”
Note: i am using EF Core + Identity Core + Openiddict
Issue Analytics
- State:
- Created 9 months ago
- Comments:7 (4 by maintainers)
Top GitHub Comments
Awesome!
Happy holidays! ❄️
Audiences of access tokens are controlled using
principal.SetResources(...)
. To make that dynamic, you can use scopes and attach a list of resources viaOpenIddictScopeDescriptor.Resources
.In practice, a client application is never the audience of an access token (the exception is when this application corresponds to a resource server doing OAuth 2.0 introspection: in this case, you have a client_id for the API and the resources must contain this client identifier).