feat(oidc): add configurable OIDC properties for JWKS and JWT handling
Fixes intermittent OIDC JWT validation failed errors in v1.10/v1.11 by implementing proper network timeouts, dynamic algorithm support, and compatibility fixes for Keycloak and Authentik.
What was broken
Random authentication failures in containerized environments:
-
OIDC JWT validation failed(intermittent, timing-dependent) -
401 Unauthorizedwith Keycloak Access Tokens - Algorithm mismatch errors with Authentik (ES256)
what caused these:
-
Timeout regression: v1.11 had a bug where JWKS retrieval fell back to 1ms timeouts instead of JVM defaults. In Docker/K8s with any DNS latency or GC pause, this meant instant failures during startup or token validation.
-
Hardcoded RS256: Algorithm support was hardcoded to RS256, breaking any provider using ES256, PS256, or other JWS algorithms.
-
Missing azp validation: Keycloak Access Tokens often omit the
audclaim and useazpinstead. We weren't checking it.
TODO;
Any chance you can kick off a build/tag off this branch for testing this fix? I use this in a k3s cluster and would rather not build this myself to test it.
Unfortunately, I don't really have the ability to create test builds for this branch. However, if you're running k3s, you could test this relatively quickly by making a script that:
- Pulls the PR branch using GitHub CLI
- Builds the image with docker build
- Updates your k3s deployment to use the new image
- Cleans up afterward
Well, at least that's what I was going to do, but life got in the way. I can get back to this around next Tuesday/Wednesday. In the meantime, I've only had the bandwidth to tackle simpler PRs that don't require as much focus.
If it's urgent, perhaps @adityachandelgit (sorry for the ping) could create a test build if they have time. I raised this on Discord, that we should have (if possible) preview deployments/container of PRs (right now this is only the case for merged commits). In-case this is not in the cards right now, script should be good intermediate solutions.
Unfortunately, I don't really have the ability to create test builds for this branch. However, if you're running k3s, you could test this relatively quickly by making a script that:
- Pulls the PR branch using GitHub CLI
- Builds the image with docker build
- Updates your k3s deployment to use the new image
- Cleans up afterward
Well, at least that's what I was going to do, but life got in the way. I can get back to this around next Tuesday/Wednesday. In the meantime, I've only had the bandwidth to tackle simpler PRs that don't require as much focus.
If it's urgent, perhaps @adityachandelgit (sorry for the ping) could create a test build if they have time. I raised this on Discord, that we should have (if possible) preview deployments/container of PRs (right now this is only the case for merged commits). In-case this is not in the cards right now, script should be good intermediate solutions.
Yeah, makes sense. I was just lazy, apologies I just assumed you had the perms to do so. Appreciate the work on this :)
Hi @tamuseanmiller,
Apologies for the ping, as I am getting closer to the end, I started to test with "real" configs (so far I've done so with Authelia). As far I can tell at this point most of the major hurdles has been cleared from my side, however I mostly tested via devtools (my-oidc, JUnit testing and Authelia as mentioned, so additional world testing would be great), I am wondering if I can get your initial impression on this:
https://github.com/balazs-szucs/booklore/pull/41#issuecomment-3622462050
Note: it's an arm image
https://github.com/balazs-szucs/booklore/pull/45#issuecomment-3622684971
linux/arm64, linux/amd64 (although untested)
Thanks for your time!
Hi @tamuseanmiller,
Apologies for the ping, as I am getting closer to the end, I started to test with "real" configs (so far I've done so with Authelia). As far I can tell at this point most of the major hurdles has been cleared from my side, however I mostly tested via devtools (my-oidc, JUnit testing and Authelia as mentioned, so additional world testing would be great), I am wondering if I can get your initial impression on this:
Note: it's an arm image
linux/arm64, linux/amd64 (although untested)
Thanks for your time!
Hey! Gave it a quick shot, I use authentik as my OIDC provider at the moment. Still seems to be failing, saw these logs
2025-12-07T14:40:32.283-05:00 INFO 10 --- [booklore-api] [io-8080-exec-16] c.a.b.c.s.OidcJwtAuthenticationConverter : 🔐 OIDC Authentication - JWT Claims Analysis:
2025-12-07T14:40:32.283-05:00 INFO 10 --- [booklore-api] [io-8080-exec-16] c.a.b.c.s.OidcJwtAuthenticationConverter : └─ Username claim 'preferred_username' = 'admin'
2025-12-07T14:40:32.283-05:00 INFO 10 --- [booklore-api] [io-8080-exec-16] c.a.b.c.s.OidcJwtAuthenticationConverter : └─ Email claim = 'null'
2025-12-07T14:40:32.283-05:00 INFO 10 --- [booklore-api] [io-8080-exec-16] c.a.b.c.s.OidcJwtAuthenticationConverter : └─ Name claim = 'null'
2025-12-07T14:40:32.283-05:00 INFO 10 --- [booklore-api] [io-8080-exec-16] c.a.b.c.s.OidcJwtAuthenticationConverter : └─ Subject (sub) = 'admin'
2025-12-07T14:40:32.283-05:00 INFO 10 --- [booklore-api] [io-8080-exec-16] c.a.b.c.s.OidcJwtAuthenticationConverter : └─ All claims: [sub, exp, userId, isDefaultPassword, iat]
2025-12-07T14:40:32.285-05:00 WARN 10 --- [booklore-api] [io-8080-exec-16] c.a.b.s.user.UserProvisioningService : ⚠️ OIDC IdP did not provide 'name' claim. Using username 'admin' as display name.
2025-12-07T14:40:32.285-05:00 WARN 10 --- [booklore-api] [io-8080-exec-16] c.a.b.s.user.UserProvisioningService : 💡 To fix: Configure your IdP to include 'name' claim in ID tokens.
2025-12-07T14:40:32.285-05:00 INFO 10 --- [booklore-api] [io-8080-exec-16] c.a.b.s.user.UserProvisioningService : 📝 Provisioning OIDC user: username='admin', email='null', name='admin'
Goes without saying this was working before. I can set up pocket id real quick and see how that behaves.
Ah, apologies. Back to the keyboard I guess. Thanks for the feedback!
Sure :) I see the same thing with pocket id as well. Give me a ping and I'll try and run a quick test for you if you want. Thanks again for taking a look!
Hi @tamuseanmiller, I have a new container :)
I tested this one with actual Authentik, so it should work (:pray:)
docker pull balazsszucs/booklore:pr-50
(code: https://github.com/balazs-szucs/booklore/pull/50)
This is the container. There is currently a really minor race condition on the frontend which might give you an error in the browser (but it does NOT affect anything in a negative way; the frontend makes another request immediately after, so it's not a regression). Some features are still missing, but it should be considerably more capable than the old OIDC.
Anyway, TLDR: this one works for me, so I am once again quite confident. Any feedback is welcome!
Thanks again for your time!
Hi @tamuseanmiller, I have a new container :)
I tested this one with actual Authentik, so it should work (:pray:)
docker pull balazsszucs/booklore:pr-50
(code: balazs-szucs#50)
This is the container. There is currently a really minor race condition on the frontend which might give you an error in the browser (but it does NOT affect anything in a negative way; the frontend makes another request immediately after, so it's not a regression). Some features are still missing, but it should be considerably more capable than the old OIDC.
Anyway, TLDR: this one works for me, so I am once again quite confident. Any feedback is welcome!
Thanks again for your time!
Hey, thanks for the ping. I gave this another shot really quickly with this image and I can't get the webserver to come up:
at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:513) ~[spring-beans-6.2.14.jar!/:6.2.14]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1375) ~[spring-beans-6.2.14.jar!/:6.2.14]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1205) ~[spring-beans-6.2.14.jar!/:6.2.14]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:569) ~[spring-beans-6.2.14.jar!/:6.2.14]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:529) ~[spring-beans-6.2.14.jar!/:6.2.14]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:339) ~[spring-beans-6.2.14.jar!/:6.2.14]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:373) ~[spring-beans-6.2.14.jar!/:6.2.14]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:337) ~[spring-beans-6.2.14.jar!/:6.2.14]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202) ~[spring-beans-6.2.14.jar!/:6.2.14]
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:365) ~[spring-beans-6.2.14.jar!/:6.2.14]
... 110 common frames omitted
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'flywayInitializer' defined in class path resource [org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration$FlywayConfiguration.class]: Validate failed: Migrations have failed validation
Migration checksum mismatch for migration version 51
-> Applied to database : -1382141364
-> Resolved locally : 372495746
Either revert the changes to the migration, or run repair to update the schema history.
Detected applied migration not resolved locally: 52.
If you removed this migration intentionally, run repair to mark the migration as deleted.
Need more flexibility with validation rules? Learn more: https://rd.gt/3AbJUZE
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1826) ~[spring-beans-6.2.14.jar!/:6.2.14]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:607) ~[spring-beans-6.2.14.jar!/:6.2.14]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:529) ~[spring-beans-6.2.14.jar!/:6.2.14]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:339) ~[spring-beans-6.2.14.jar!/:6.2.14]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:373) ~[spring-beans-6.2.14.jar!/:6.2.14]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:337) ~[spring-beans-6.2.14.jar!/:6.2.14]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202) ~[spring-beans-6.2.14.jar!/:6.2.14]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:315) ~[spring-beans-6.2.14.jar!/:6.2.14]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202) ~[spring-beans-6.2.14.jar!/:6.2.14]
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:365) ~[spring-beans-6.2.14.jar!/:6.2.14]
... 122 common frames omitted
Caused by: org.flywaydb.core.api.exception.FlywayValidateException: Validate failed: Migrations have failed validation
Migration checksum mismatch for migration version 51
-> Applied to database : -1382141364
-> Resolved locally : 372495746
Either revert the changes to the migration, or run repair to update the schema history.
Detected applied migration not resolved locally: 52.
If you removed this migration intentionally, run repair to mark the migration as deleted.
Need more flexibility with validation rules? Learn more: https://rd.gt/3AbJUZE
at org.flywaydb.core.Flyway.lambda$migrate$1(Flyway.java:201) ~[flyway-core-11.7.2.jar!/:na]
at org.flywaydb.core.FlywayExecutor.execute(FlywayExecutor.java:210) ~[flyway-core-11.7.2.jar!/:na]
at org.flywaydb.core.Flyway.migrate(Flyway.java:188) ~[flyway-core-11.7.2.jar!/:na]
at org.springframework.boot.autoconfigure.flyway.FlywayMigrationInitializer.afterPropertiesSet(FlywayMigrationInitializer.java:66) ~[spring-boot-autoconfigure-3.5.8.jar!/:3.5.8]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1873) ~[spring-beans-6.2.14.jar!/:6.2.14]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1822) ~[spring-beans-6.2.14.jar!/:6.2.14]
... 131 common frames omitted
Edit: Just realizing I didn't copy all of the logs. Let me go run this again and see if I can collect them
booklore-7ff849998d-4k6mt.log As promised, here is the full log.
Hi,
those errors are unrelated to OIDC, but they problematic because of them it won't boot up. They are basically saying 2 things:
- Migration 51 has a checksum mismatch (the file content changed, but the database still has the old version)
- Migration 52 exists in the database but isn't found locally (BL may have deleted or renamed it)
I am afraid, this might be a byproduct of jumping ahead so many version or a local DB corruption.
Fixes:
docker-compose down -v # Remove the database volume
docker-compose up -d # Rebuild everything
(this will RESET your DB so do NOT do it if the contents matter for you)
Otherwise if you want repair your DB, I have to research I'm afraid how to do that.
TLDR: that is unrelated to the OIDC code, sadly.
Giving the BL env the following variable might help:
SPRING_FLYWAY_REPAIR_ON_MIGRATE: "true"
But not sure.
Hi,
those errors are unrelated to OIDC, but they problematic because of them it won't boot up. They are basically saying 2 things:
- Migration 51 has a checksum mismatch (the file content changed, but the database still has the old version)
- Migration 52 exists in the database but isn't found locally (you may have deleted or renamed it)
I am afraid, this might be a byproduct of jumping ahead so many version or a local DB corruption.
Fixes:
docker-compose down -v # Remove the database volume docker-compose up -d # Rebuild everything(this will RESET your DB so do NOT do it if the contents matter for you)
Otherwise if you want repair your DB, I have to research I'm afraid how to do that.
TLDR: that is unrelated to the OIDC code, sadly.
Yeah, I was pretty sure that was the case. I would rather not dump all of the data. Reverting back to the base booklore image doesn't seem to have any issues. I run this in k3s, let me give the env var a shot.
Perhaps if you update/restart the MariaDB alongside of Booklore that might also help. Perhaps you updated the BL image to the PR container without restarting the MariaDB, that also may be the culprit. So I would also try to restart both of them at the same time, or alternatively, restart MariaDB and then BL if that earlier option fails.
Works for me. Great work. Thanks!
Edit: I started with a clean db to get round the migration issue.
Works for me. Great work. Thanks!
That certainly takes a weight off my shoulders! Thanks!
Edit: I started with a clean db to get round the migration issue.
Well, it sounds like one door closes and another opens. The grind begins again, I guess. Generally, I don't want users to lose their data, that would make me feel quite shitty. I'll see what I can do about this migration issue.
Okay it took me a while but I did something I probably shouldn't have and just went through and modified the database manually. Looks like 4 of the migrations were wonky and I just edited each one in place until the checksums matched. Now everything seems to be working fine! I'll have to see if I broke anything haha.
Yeah I broke something haha. Just going to revert from the backup I took and see if you can handle some of the migration issues. Let me know if you need the migration ID and checksums that changed and I'll see if I can find them if not in the logs.
Nice!
Many thanks, guys, for the help! One more quick thing: can you click through the stuff you would normally do with your OIDC config (e.g., provision a new user, etc.) and let me know if there is anything out of the ordinary?
Also, regarding loading time: in my tests, this is quite a bit slower than the previous implementation because it actually does a bunch of new stuff under the hood. How are you finding the speed? Is it too slow?
I think @tamuseanmiller had an offer/suggestion about PocketID. After the excitement wears off and we have a stable build, can you see how PocketID is looking? (I have not tested that yet.)
Yeah I broke something haha. Just going to revert from the backup I took and see if you can handle some of the migration issues. Let me know if you need the migration ID and checksums that changed and I'll see if I can find them if not in the logs.
Ah, I just saw this. Apologies for the DB inconvenience :cry:. I don't want to waste your time, but one thing that might solve it, to start bumping BL versions. E.g., bump 1 version up, wait for flyway migration to complete, do the next and so on. That should work I think.
Nice!
Many thanks, guys, for the help! One more quick thing: can you click through the stuff you would normally do with your OIDC config (e.g., provision a new user, etc.) and let me know if there is anything out of the ordinary?
Also, regarding loading time: in my tests, this is quite a bit slower than the previous implementation because it actually does a bunch of new stuff under the hood. How are you finding the speed? Is it too slow?
I did notice the loading time was quite a while. Wasn't sure if it was load on my server or something else but it probably took a good 10-12 seconds after logging in before the webui was fully loaded.
I think @tamuseanmiller had an offer/suggestion about PocketID. After the excitement wears off and we have a stable build, can you see how PocketID is looking? (I have not tested that yet.)
Yeah I can make the db changes again and see if it works.
Yeah I broke something haha. Just going to revert from the backup I took and see if you can handle some of the migration issues. Let me know if you need the migration ID and checksums that changed and I'll see if I can find them if not in the logs.
Ah, I just saw this. Apologies for the DB inconvenience 😢. I don't want to waste your time, but one thing that might solve it, to start bumping BL versions. E.g., bump 1 version up, wait for flyway migration to complete, do the next and so on. That should work I think.
I am on the latest version, would it not have bumped already? Or perhaps bump on develop versions?
The very first login will definitely take much more time (there quite a lot of migration involved, sadly there not much I can do about, I am talking more so about the logins afterwards.). However, I am debating with myself how aggressive the caching/multi-threaded code optimization should be. Baby steps, obviously. I wanted to get this off the ground first, but these user experience things are definitely also a priority.
I am on the latest version, would it not have bumped already? Or perhaps bump on develop versions?
Ah, nope I though you updated from 1.10 to this PR version. In this case feel free to disregard that remark.
I don't find it too slow but the brief flash of the local login page is odd (see video). Provisioning new users works if usernames match. Also, Automatic user provisioning works too. Any chance we can auto provision the Admin role in Booklore based on membership of a specific group in our OIDC Provider?
https://github.com/user-attachments/assets/86fecfd8-e0e7-42e4-b610-0dc94f089df8
Okay, I gave pocket id a shot. I'm not sure if I'm doing something wrong, I might have it configured incorrectly but I see PocketID authentication encountered 1 error(s), possibly due to timeouts or server issues. with no logs that seem to mention anything related to the auth around this time. Authentik is fine though
I will note that I can't change any OIDC changes without a pod/container restart. If I change and save something it requires a restart, not sure if that's new from the pre-warming or has always been that way but the behavior is confusing because you'll just get a white screen and then failure.
I don't find it too slow but the brief flash of the local login page is odd (see video). Provisioning new users works if usernames match
That’s strange. I can reproduce it in Chromium-based browsers but not in Firefox. I am looking into it.
Any chance we can auto provision the Admin role in Booklore based on membership of a specific group in our OIDC Provider?
Yes, that is definitely planned. I haven't gotten to it yet, though.
Okay, I gave pocket id a shot...
I'll test this too. I was hoping it would work without requiring even more configuration on the code side, but what can you do. :'D
To give some perspective on why this is such a headache:
Every single OIDC provider is a little different, different enough that you can't just assume that if one works, the others will too.
- Different providers use different claim names and scopes for basics like email and username, so Booklore cannot assume a single email or preferred_username claim will always be present.
- Group membership is even less standardized: some providers expose it as groups, others via custom claims or only on the userinfo endpoint, and mapping rules differ per product.
Because of this, each new OIDC provider usually needs a bit of per‑provider mapping or configuration rather than "it works once, therefore it works everywhere."
I'll pull down PocketID and specifically test for that as well. I guess then I'll have all the Infinity Stones (or OIDC providers downloaded and ready to test...): Authelia, Authentik, PocketID (and I'll probably try Keycloak too) :'D
To give some perspective on why this is such a headache:
Every single OIDC provider is a little different, different enough that you can't just assume that if one works, the others will too.
- Different providers use different claim names and scopes for basics like email and username, so Booklore cannot assume a single email or preferred_username claim will always be present.
- Group membership is even less standardized: some providers expose it as groups, others via custom claims or only on the userinfo endpoint, and mapping rules differ per product.
Because of this, each new OIDC provider usually needs a bit of per‑provider mapping or configuration rather than "it works once, therefore it works everywhere."
I'll pull down PocketID and specifically test for that as well. I guess then I'll have all the Infinity Stones (or OIDC providers downloaded and ready to test...): Authelia, Authentik, PocketID (and I'll probably try Keycloak too) :'D
Would it be easier to configure this in a similar way to something like portainer/grafana where there you have to fill out each expected URL, group, claim and all? It's nice to just provide like 3 fields and everything just works but the code for this will probably become a little crazy.
Would it be easier to configure this in a similar way to something like portainer/grafana where there you have to fill out each expected URL, group, claim and all? It's nice to just provide like 3 fields and everything just works but the code for this will probably become a little crazy.
Technically yes, but I’m opposed to it philosophically. Even though self-hosting implies some technical know-how, I want to keep the app accessible to a much broader audience than just sysadmins. (Admittedly this stuff isn't hard to figure out, but I think some would still struggle.)
Also, consider that this isn’t as serious or enterprise-focused of a project as those tools. At this stage of Booklore, I can explicitly say: Hey, I made this. I know it’s not optimal from every perspective, but I’m consciously choosing this trade-off and I take responsibility for it (which I fully intend to do, so you can expect to deal with me for a while :joy:. I hope that's alright!)
We’ve actually made a few similar trade-offs recently, accepting more complex code in exchange for a better user experience. For instance, Booklore search runs against pre-compiled search queries. That approach is faster and provides a smoother UX, but it’s definitely harder to maintain on the backend (e.g., keeping the pre-compiled search field in sync with source fields like name).
Would it be easier to configure this in a similar way to something like portainer/grafana where there you have to fill out each expected URL, group, claim and all? It's nice to just provide like 3 fields and everything just works but the code for this will probably become a little crazy.
Technically yes, but I’m opposed to it philosophically. Even though self-hosting implies some technical know-how, I want to keep the app accessible to a much broader audience than just sysadmins. (Admittedly this stuff isn't hard to figure out, but I think some would still struggle.)
Also, consider that this isn’t as serious or enterprise-focused of a project as those tools. At this stage of Booklore, I can explicitly say: Hey, I made this. I know it’s not optimal from every perspective, but I’m consciously choosing this trade-off and I take responsibility for it (which I fully intend to do, so you can expect to deal with me for a while 😂. I hope that's alright!)
Haha that is absolutely alright and I very much enjoy that I can hand this to non-technical people and they can mess around with it easily. I work on an enterprise product so I don't get to make those kinds of trade-offs very often, appreciate all the work you're doing here!
https://github.com/balazs-szucs/booklore/pull/50#issuecomment-3644177736
New image should address most of the initial feedback. No breaking change, should be generally faster frontend loading is fixed, though still takes sometime. I haven't specifically looked at PocketID yet.
Superseded by the formal PR for this: #1829
You can still ping/message in this PR (and you are encouraged to do so, if you have feedback or questions), however I want to keep #1829 on point and clean of off-topic convos.