Seed ApiScopes for ApiResourcesScopes, align PostIt client
EnsureDefaultApplicationScopes was inserting every entry of Constants.ApiResourcesScopes (admin, moderation, performer, client, blogs) into the IdentityResources table, as Profile-derived rows. That made them visible to /connect/discovery's scopes_supported under the identity section, but no API resource would ever issue a token bearing them — IdentityServer then rejected clients that requested any of these scopes with 'invalid_scope' at the token endpoint. The most visible casualty was PostIt, a public PKCE client whose postit-settings.json asks for scope=openid profile offline_access blogs. 'blogs' is the scope that gates the Yavsc.Blogs deployment (blogs.pschneider.fr), so the login flow died at the token step. Fix: - Constants.ApiResourcesScopes entries are now seeded as ApiScope rows (with Name + DisplayName). IdentityResources stays limited to the actual OpenID Connect profile (openid, profile). - EnsureDefaultConfiguration gains an idempotent AlignPostItClientScopes pass that adds any missing scope from PostItScopes to the existing 'postit' client's AllowedScopes. Nothing is removed — manual revocation stays manual. Existing live databases pick up both changes on next startup: missing ApiScope rows are inserted, and the postit client's ClientScope rows catch up.
This commit is contained in:
parent
99f4361e2d
commit
3744017127
1 changed files with 75 additions and 11 deletions
|
|
@ -539,7 +539,10 @@ public static class HostingExtensions
|
||||||
var identityResources = context.Set<IdentityServer8.EntityFramework.Entities.IdentityResource>();
|
var identityResources = context.Set<IdentityServer8.EntityFramework.Entities.IdentityResource>();
|
||||||
var apiScopes = context.Set<IdentityServer8.EntityFramework.Entities.ApiScope>();
|
var apiScopes = context.Set<IdentityServer8.EntityFramework.Entities.ApiScope>();
|
||||||
|
|
||||||
// IdentityResources standards
|
// IdentityResources standards (OpenId + Profile only).
|
||||||
|
// Application-defined API scopes from Constants.ApiResourcesScopes
|
||||||
|
// are NOT identity resources — they belong to the ApiScopes table
|
||||||
|
// and are seeded as such further down.
|
||||||
if (!identityResources.Any(r => r.Name == "openid"))
|
if (!identityResources.Any(r => r.Name == "openid"))
|
||||||
{
|
{
|
||||||
var openid = new IdentityResources.OpenId().ToEntity();
|
var openid = new IdentityResources.OpenId().ToEntity();
|
||||||
|
|
@ -551,18 +554,29 @@ public static class HostingExtensions
|
||||||
var profile = new IdentityResources.Profile().ToEntity();
|
var profile = new IdentityResources.Profile().ToEntity();
|
||||||
identityResources.Add(profile);
|
identityResources.Add(profile);
|
||||||
}
|
}
|
||||||
foreach (var scope in Constants.ApiResourcesScopes)
|
|
||||||
{
|
|
||||||
|
|
||||||
if (!identityResources.Any(s => s.Name == scope.ScopeName))
|
// Application-defined API scopes (admin, moderation, performer,
|
||||||
|
// client, blogs, …). Seeded into ApiScopes, not IdentityResources:
|
||||||
|
// these gate access to API resources (e.g. the Yavsc.Blogs
|
||||||
|
// deployment requires the "blogs" scope) and must therefore be
|
||||||
|
// discoverable through /connect/discovery's
|
||||||
|
// scopes_supported of type resource, not identity.
|
||||||
|
//
|
||||||
|
// NOTE: prior versions inserted these into IdentityResources,
|
||||||
|
// which made them visible to /connect/authorize but unfulfillable
|
||||||
|
// (no API resource recognises an identity-scoped consent as
|
||||||
|
// access to a downstream resource). Clients like PostIt that
|
||||||
|
// request one of these scopes were rejected with "invalid_scope"
|
||||||
|
// at the token endpoint. Keep this in ApiScopes.
|
||||||
|
foreach (var scopeSpec in Constants.ApiResourcesScopes)
|
||||||
|
{
|
||||||
|
if (!apiScopes.Any(s => s.Name == scopeSpec.ScopeName))
|
||||||
{
|
{
|
||||||
identityResources.Add(
|
apiScopes.Add(new IdentityServer8.EntityFramework.Entities.ApiScope
|
||||||
new IdentityResources.Profile()
|
{
|
||||||
{
|
Name = scopeSpec.ScopeName,
|
||||||
Name = scope.ScopeName,
|
DisplayName = scopeSpec.Description,
|
||||||
DisplayName = scope.Description,
|
});
|
||||||
Enabled = true
|
|
||||||
}.ToEntity());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
context.SaveChanges();
|
context.SaveChanges();
|
||||||
|
|
@ -618,6 +632,16 @@ public static class HostingExtensions
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The PostIt client was already seeded in a previous run.
|
||||||
|
// Make sure its AllowedScopes still match what the server
|
||||||
|
// actually exposes — for instance, "blogs" only exists as an
|
||||||
|
// ApiScope since we fixed the seed (see EnsureDefaultApplicationScopes
|
||||||
|
// above). Without this pass, a client created before the fix
|
||||||
|
// would still ask for a scope the server no longer recognises
|
||||||
|
// and IdentityServer would answer "invalid_scope" at the token
|
||||||
|
// endpoint. Idempotent: missing scopes are added, nothing is
|
||||||
|
// removed (manual revocation stays manual).
|
||||||
|
AlignPostItClientScopes(context, existingClient);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -674,6 +698,46 @@ public static class HostingExtensions
|
||||||
context.SaveChanges();
|
context.SaveChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Idempotent reconciliation of the PostIt client's AllowedScopes
|
||||||
|
/// against the scopes the server actually publishes. Adds any missing
|
||||||
|
/// scope as a ClientScope row; never removes anything (revocation is a
|
||||||
|
/// manual operation, not a seed concern). Called on every startup from
|
||||||
|
/// <see cref="EnsureDefaultConfiguration"/> so a client created before
|
||||||
|
/// a scope was introduced (or before the seed was corrected) catches up
|
||||||
|
/// automatically.
|
||||||
|
/// </summary>
|
||||||
|
private static void AlignPostItClientScopes(
|
||||||
|
DbContext context,
|
||||||
|
IdentityServer8.EntityFramework.Entities.Client postitClient
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var clientScopes = context.Set<ClientScope>();
|
||||||
|
var existingScopeNames = clientScopes
|
||||||
|
.Where(s => s.Client == postitClient || s.ClientId == postitClient.Id)
|
||||||
|
.Select(s => s.Scope)
|
||||||
|
.ToHashSet();
|
||||||
|
|
||||||
|
bool changed = false;
|
||||||
|
foreach (var scope in PostItScopes)
|
||||||
|
{
|
||||||
|
if (existingScopeNames.Contains(scope))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
clientScopes.Add(new IdentityServer8.EntityFramework.Entities.ClientScope
|
||||||
|
{
|
||||||
|
Client = postitClient,
|
||||||
|
Scope = scope
|
||||||
|
});
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed)
|
||||||
|
{
|
||||||
|
context.SaveChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Compose the full set of redirect URIs for the PostIt client. The base
|
/// Compose the full set of redirect URIs for the PostIt client. The base
|
||||||
/// URIs cover the standalone desktop/mobile flows; the value of
|
/// URIs cover the standalone desktop/mobile flows; the value of
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue