Replace IdentityServer4 with OpenIddict and upgrade to .net 10#28
Replace IdentityServer4 with OpenIddict and upgrade to .net 10#28juliangiebel wants to merge 43 commits into
Conversation
Upgrade nuget packages Fix npgsql errors from upgrade Note: (IpAddress, int) is now NpgsqlCidr
Migrate to nullable
This reverts commit 7b0520a.
Implement unit tests for multiple secret handling methods
Implement custom url validator
Work on admin oauth app settings page
Implement legacy token handling test
|
|
||
| var certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(10)); | ||
|
|
||
| File.WriteAllBytes("server-encryption-certificate.pfx", certificate.Export(X509ContentType.Pfx, string.Empty)); |
There was a problem hiding this comment.
Might want to add a check if any of these files exist and throw hard, so we don't accidentally the certs.
There was a problem hiding this comment.
Considering this tool will be ran once I doubt it's really an issue. I guess it's quick though fo code just in case.
There was a problem hiding this comment.
Lowercase SQL is a crime
This is necessary to keep the endpoint path that's been used previously while having the authorize logic inside the consent page. That avoids having to haul around the auth request data between redirects (which would be annoying to implement at this point)
Fildrance
left a comment
There was a problem hiding this comment.
Overall it works! Looks slim, which is nice. Almost all of my comments are kinda nitpicks and are dismissable, bragging about inconsistencies in codestyle (which is funny, considering we review stuff with razor pages, kek). I tried my best not to devolve into poking questionable linebreaks :>
Tested it with local calls, with OAuthTest, analyzed OAuthTest and it seems to be statifying all in all.
I do, however, propose some more additions before we release it:
- adding settings for blocking user/'oauth client' creation and updates (with some setting-provided message) - so we could bascially make database read-only and not have any data-loss in case of backup if something goes poorly during release
- remove tables deletion from migrations so old stuff would still work fine if we just revert app
- prepare migration up for migrating down
- hook migration script into c# migration class :>
| using Microsoft.Extensions.Logging; | ||
| using OpenIddict.Core; | ||
| using SS14.Auth.Shared.Data; | ||
| using SS14.Web.Extensions; |
| Scopes: x.Scopes | ||
| )); | ||
|
|
||
| StringToFile(zip, "UserOAuthClients.json", data); |
There was a problem hiding this comment.
now it is unused but it is good method to have i think, so let it be?
| { | ||
| private readonly IHttpContextAccessor _httpContextAccessor; | ||
| private readonly UserManager<SpaceUser> _userManager; | ||
| private readonly SignInManager<SpaceUser> _signInManager; |
| if (_httpContextAccessor.HttpContext is not { } context) | ||
| return null; | ||
|
|
||
| var user = await _userManager.GetUserAsync(context.User); |
There was a problem hiding this comment.
Its a little bit sad that we have to do 2 calls for storage - GetUserAsync and GetUserIdAsync, but yeah, i don't see any nicer way... sadge
| /// <summary> | ||
| /// Provides claims for the specified identity. | ||
| /// </summary> | ||
| /// <param name="userId"></param> |
There was a problem hiding this comment.
don't leave them empty :godo:
|
|
||
| public class TestMultipleClientSecretHandling | ||
| { | ||
| [TestCase( "0.1755342104.key1111.desc", 0, ExpectedResult = 0)] |
There was a problem hiding this comment.
odd whitespaces at start of test case and unused usings
|
|
||
| certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(10)); | ||
|
|
||
| File.WriteAllBytes("server-signing-certificate-rsa-pss.pfx", certificate.Export(X509ContentType.Pfx, string.Empty)); |
There was a problem hiding this comment.
I would say it would be nice to have something added in readme.md - what it does (what for certs should be used with openiddict in current scheme), when to use it.
| (select "SpaceUserId" from public."UserOAuthClients" uc where c."Id" = uc."ClientId"), | ||
| "LogoUri", null as ApplicationType, c."ClientId", | ||
| (select string_agg(array_to_string(array [ | ||
| cs."Id"::text, | ||
| floor(extract(epoch from cs."Created"))::text, | ||
| '_OLD_' || cs."Value", | ||
| replace(cs."Description", '*', '') | ||
| ], '.'), ',') from "IS4"."ClientSecrets" cs where cs."ClientId" = c."Id") ClientSecret, | ||
| 'confidential' as ClientType, gen_random_uuid() as ConcurrencyToken, | ||
| case when "RequireConsent" then 'Explicit' else 'Implicit' end as "ConsentType", | ||
| "ClientName" as DisplayName, null as DisplayNames, null as JsonWebKeySet, | ||
| '["ept:authorization","ept:token","ept:introspection","ept:end_session","gt:refresh_token","gt:authorization_code","rst:code","scp:email","scp:profile","scp:roles"]' | ||
| as Permissions, | ||
| null as PostLogoutRedirectUris, | ||
| null as Properties, | ||
| (select json_agg("RedirectUri") from "IS4"."ClientRedirectUris" cr where cr."ClientId" = c."Id")::text "RedirectUris", | ||
| case when "RequirePkce" then '["ft:pkce"]' end as "Requirements", | ||
| json_build_object( | ||
| 'space:AllowPlainPkce', "AllowPlainTextPkce"::TEXT, | ||
| 'space:SigningAlgorithm', "AllowedIdentityTokenSigningAlgorithms", | ||
| 'space:Disabled', ("Enabled" = false)::TEXT | ||
| )::text as "Settings", | ||
| "ClientUri" as WebsiteUrl |
There was a problem hiding this comment.
| (select "SpaceUserId" from public."UserOAuthClients" uc where c."Id" = uc."ClientId"), | |
| "LogoUri", null as ApplicationType, c."ClientId", | |
| (select string_agg(array_to_string(array [ | |
| cs."Id"::text, | |
| floor(extract(epoch from cs."Created"))::text, | |
| '_OLD_' || cs."Value", | |
| replace(cs."Description", '*', '') | |
| ], '.'), ',') from "IS4"."ClientSecrets" cs where cs."ClientId" = c."Id") ClientSecret, | |
| 'confidential' as ClientType, gen_random_uuid() as ConcurrencyToken, | |
| case when "RequireConsent" then 'Explicit' else 'Implicit' end as "ConsentType", | |
| "ClientName" as DisplayName, null as DisplayNames, null as JsonWebKeySet, | |
| '["ept:authorization","ept:token","ept:introspection","ept:end_session","gt:refresh_token","gt:authorization_code","rst:code","scp:email","scp:profile","scp:roles"]' | |
| as Permissions, | |
| null as PostLogoutRedirectUris, | |
| null as Properties, | |
| (select json_agg("RedirectUri") from "IS4"."ClientRedirectUris" cr where cr."ClientId" = c."Id")::text "RedirectUris", | |
| case when "RequirePkce" then '["ft:pkce"]' end as "Requirements", | |
| json_build_object( | |
| 'space:AllowPlainPkce', "AllowPlainTextPkce"::TEXT, | |
| 'space:SigningAlgorithm', "AllowedIdentityTokenSigningAlgorithms", | |
| 'space:Disabled', ("Enabled" = false)::TEXT | |
| )::text as "Settings", | |
| "ClientUri" as WebsiteUrl | |
| (select "SpaceUserId" from public."UserOAuthClients" uc where c."Id" = uc."ClientId"), | |
| "LogoUri", | |
| null as ApplicationType, | |
| c."ClientId", | |
| (select string_agg(array_to_string(array [ | |
| cs."Id"::text, | |
| floor(extract(epoch from cs."Created"))::text, | |
| '_OLD_' || cs."Value", | |
| replace(cs."Description", '*', '') | |
| ], '.'), ',') from "IS4"."ClientSecrets" cs where cs."ClientId" = c."Id") ClientSecret, | |
| 'confidential' as ClientType, | |
| gen_random_uuid() as ConcurrencyToken, | |
| case when "RequireConsent" then 'Explicit' else 'Implicit' end as "ConsentType", | |
| "ClientName" as DisplayName, | |
| null as DisplayNames, | |
| null as JsonWebKeySet, | |
| '["ept:authorization","ept:token","ept:introspection","ept:end_session","gt:refresh_token","gt:authorization_code","rst:code","scp:email","scp:profile","scp:roles"]' | |
| as Permissions, | |
| null as PostLogoutRedirectUris, | |
| null as Properties, | |
| (select json_agg("RedirectUri") from "IS4"."ClientRedirectUris" cr where cr."ClientId" = c."Id")::text "RedirectUris", | |
| case when "RequirePkce" then '["ft:pkce"]' end as "Requirements", | |
| json_build_object( | |
| 'space:AllowPlainPkce', "AllowPlainTextPkce"::TEXT, | |
| 'space:SigningAlgorithm', "AllowedIdentityTokenSigningAlgorithms", | |
| 'space:Disabled', ("Enabled" = false)::TEXT | |
| )::text as "Settings", | |
| "ClientUri" as WebsiteUrl |
There was a problem hiding this comment.
fix indent and line breaks (1 field max per line)
| */appsettings.Secret.yml | ||
| */tempkey.jwk No newline at end of file | ||
| */tempkey.jwk | ||
| cert |
There was a problem hiding this comment.
By default they are generated in tools folder, so i would propose ignoring them there and pathing them there :|
| @@ -0,0 +1,26 @@ | |||
| insert into public."OpenIddictApplications" | |||
| select gen_random_uuid() as Id, | |||
| (select "SpaceUserId" from public."UserOAuthClients" uc where c."Id" = uc."ClientId"), | |||
There was a problem hiding this comment.
have we not deleted UserOAuthClients in AddSpaceAppHomePageProperty migration?
!!!This PR was locally tested extensively. It should still be tested with different oauth clients used in production though. Esp. the wiki as it can potentially break those oauth clients/application. Ideally upgrading SS14.Web is tested on a separate instance created from a snapshot first!!!
This PR updates the project to .net 9 and replaces IdentityServer4 with OpenIddict while trying to ensure that existing OAuth applications keep working. For that purpose there is a data migration sql script under
tools/identityserver4_to_openiddict_data_migration.sql.Differences between IdentityServer4 and OpenIddict
The biggest difference between the two OIDC solutions is the way client/applications are stored and the amount of extra features and settings.
OpenIddict uses a lot less database entities to store applications as it stores most non queried settings and configurations as json inside the application entity. It also doesn't normalize entities to quite the extent IS4 does.
This means there wasn't a need for a separate schema.
Also a lot of settings that weren't used got removed from the UI as implementing them with OpenIddict would mean adding custom settings and implementing custom event handlers (Which this PR already does for settings that had to be re-implemented like "PlainPkce" and "Allow PS256")
Encryption and Signing keys have to be supplied as certificates using PFX files.
OpenIddict also uses a different hashing algorithm for secrets and it salts them so migrated keys from IS4 are marked as legacy and handled seperatly using a re-implementation of the the way secrets are hashed in IS4 (Which uses standard .net cryptography methods).
Additional changes
Program.csandStartup.csforSS14.Web/SS14.Webinto a single top leverProgram.csfile.#nullable true.Noteworthy improvements
/.well-known/openid-configurationnow only lists actually supported capabilities and featuresuserinfoendpointGenerating encryption and signing certificates
At least one encryption and one signing certificate needs to be generated and configured for each encryption/signing algorithm that needs to be supported.
For this purpose a csx file exists under
tools\GenerateCerts.csx(I used the .net10 preview to execute it otherwise it needs a proper project file).Executing that file will generate 3 certificates. One encryption certificate using RS265 and two signing certificates using RS265 and PS265 respectively.
Example certificate configuration
Fixes #20
Fixes #17