mirror of
https://github.com/juanfont/headscale.git
synced 2026-01-11 20:00:28 +01:00
Enhance OIDC Authentication with refresh_token Support #544
Open
opened 2025-12-29 02:19:47 +01:00 by adam
·
17 comments
No Branch/Tag Specified
main
update_flake_lock_action
gh-pages
kradalby/release-v0.27.2
dependabot/go_modules/golang.org/x/crypto-0.45.0
dependabot/go_modules/github.com/opencontainers/runc-1.3.3
copilot/investigate-headscale-issue-2788
copilot/investigate-visibility-issue-2788
copilot/investigate-issue-2833
copilot/debug-issue-2846
copilot/fix-issue-2847
dependabot/go_modules/github.com/go-viper/mapstructure/v2-2.4.0
dependabot/go_modules/github.com/docker/docker-28.3.3incompatible
kradalby/cli-experiement3
doc/0.26.1
doc/0.25.1
doc/0.25.0
doc/0.24.3
doc/0.24.2
doc/0.24.1
doc/0.24.0
kradalby/build-docker-on-pr
topic/docu-versioning
topic/docker-kos
juanfont/fix-crash-node-id
juanfont/better-disclaimer
update-contributors
topic/prettier
revert-1893-add-test-stage-to-docs
add-test-stage-to-docs
remove-node-check-interval
fix-empty-prefix
fix-ephemeral-reusable
bug_report-debuginfo
autogroups
logs-to-stderr
revert-1414-topic/fix_unix_socket
rename-machine-node
port-embedded-derp-tests-v2
port-derp-tests
duplicate-word-linter
update-tailscale-1.36
warn-against-apache
ko-fi-link
more-acl-tests
fix-typo-standalone
parallel-nolint
tparallel-fix
rerouting
ssh-changelog-docs
oidc-cleanup
web-auth-flow-tests
kradalby-gh-runner
fix-proto-lint
remove-funding-links
go-1.19
enable-1.30-in-tests
0.16.x
cosmetic-changes-integration
tmp-fix-integration-docker
fix-integration-docker
configurable-update-interval
show-nodes-online
hs2021
acl-syntax-fixes
ts2021-implementation
fix-spurious-updates
unstable-integration-tests
mandatory-stun
embedded-derp
prtemplate-fix
v0.28.0-beta.1
v0.27.2-rc.1
v0.27.1
v0.27.0
v0.27.0-beta.2
v0.27.0-beta.1
v0.26.1
v0.26.0
v0.26.0-beta.2
v0.26.0-beta.1
v0.25.1
v0.25.0
v0.25.0-beta.2
v0.24.3
v0.25.0-beta.1
v0.24.2
v0.24.1
v0.24.0
v0.24.0-beta.2
v0.24.0-beta.1
v0.23.0
v0.23.0-rc.1
v0.23.0-beta.5
v0.23.0-beta.4
v0.23.0-beta3
v0.23.0-beta2
v0.23.0-beta1
v0.23.0-alpha12
v0.23.0-alpha11
v0.23.0-alpha10
v0.23.0-alpha9
v0.23.0-alpha8
v0.23.0-alpha7
v0.23.0-alpha6
v0.23.0-alpha5
v0.23.0-alpha4
v0.23.0-alpha4-docker-ko-test9
v0.23.0-alpha4-docker-ko-test8
v0.23.0-alpha4-docker-ko-test7
v0.23.0-alpha4-docker-ko-test6
v0.23.0-alpha4-docker-ko-test5
v0.23.0-alpha-docker-release-test-debug2
v0.23.0-alpha-docker-release-test-debug
v0.23.0-alpha4-docker-ko-test4
v0.23.0-alpha4-docker-ko-test3
v0.23.0-alpha4-docker-ko-test2
v0.23.0-alpha4-docker-ko-test
v0.23.0-alpha3
v0.23.0-alpha2
v0.23.0-alpha1
v0.22.3
v0.22.2
v0.23.0-alpha-docker-release-test
v0.22.1
v0.22.0
v0.22.0-alpha3
v0.22.0-alpha2
v0.22.0-alpha1
v0.22.0-nfpmtest
v0.21.0
v0.20.0
v0.19.0
v0.19.0-beta2
v0.19.0-beta1
v0.18.0
v0.18.0-beta4
v0.18.0-beta3
v0.18.0-beta2
v0.18.0-beta1
v0.17.1
v0.17.0
v0.17.0-beta5
v0.17.0-beta4
v0.17.0-beta3
v0.17.0-beta2
v0.17.0-beta1
v0.17.0-alpha4
v0.17.0-alpha3
v0.17.0-alpha2
v0.17.0-alpha1
v0.16.4
v0.16.3
v0.16.2
v0.16.1
v0.16.0
v0.16.0-beta7
v0.16.0-beta6
v0.16.0-beta5
v0.16.0-beta4
v0.16.0-beta3
v0.16.0-beta2
v0.16.0-beta1
v0.15.0
v0.15.0-beta6
v0.15.0-beta5
v0.15.0-beta4
v0.15.0-beta3
v0.15.0-beta2
v0.15.0-beta1
v0.14.0
v0.14.0-beta2
v0.14.0-beta1
v0.13.0
v0.13.0-beta3
v0.13.0-beta2
v0.13.0-beta1
upstream/v0.12.4
v0.12.4
v0.12.3
v0.12.2
v0.12.2-beta1
v0.12.1
v0.12.0-beta2
v0.12.0-beta1
v0.11.0
v0.10.8
v0.10.7
v0.10.6
v0.10.5
v0.10.4
v0.10.3
v0.10.2
v0.10.1
v0.10.0
v0.9.3
v0.9.2
v0.9.1
v0.9.0
v0.8.1
v0.8.0
v0.7.1
v0.7.0
v0.6.1
v0.6.0
v0.5.2
v0.5.1
v0.5.0
v0.4.0
v0.3.6
v0.3.5
v0.3.4
v0.3.3
v0.3.2
v0.3.1
v0.3.0
v0.2.2
v0.2.1
v0.2.0
v0.1.1
v0.1.0
Labels
Clear labels
CLI
DERP
DNS
Nix
OIDC
SSH
bug
database
documentation
duplicate
enhancement
faq
good first issue
grants
help wanted
might-come
needs design doc
needs investigation
no-stale-bot
out of scope
performance
policy 📝
pull-request
question
regression
routes
stale
tags
tailscale-feature-gap
well described ❤️
wontfix
Mirrored from GitHub Pull Request
Milestone
No items
No Milestone
Projects
Clear projects
No project
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: starred/headscale#544
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @vsychov on GitHub (Aug 21, 2023).
Hello,
First and foremost, I'd like to express my appreciation for the excellent product you have built.
Currently,
headscaleoffers OIDC authorization, but its functionality appears to be somewhat limited. As it stands, it's challenging to provide both a smooth user experience and maintain security due toheadscale's inability to utilizerefresh_token. This limitation presents us with a dilemma: either we grant the user an extended lifespan, during which their access might be revoked by IdAM, or we ask the user to re-authenticate periodically (e.g., daily).A feasible solution would be to use both
accessandrefreshtokens. Implementing this would primarily involve storing therefresh_tokenin the database and periodically updating the existingaccess_token. It's worth noting that the current OIDC implementation (fromcoreos/go-oidc) supports renewing theaccess_tokenvia therefresh_token, as demonstrated here. Should an attempt to refresh theaccess_tokenfail, nodes authenticated by it should be logged out (set expire tonow).@juanfont , @kradalby I'm curious to hear your thoughts on this proposal. If this aligns with the project's direction, I would be more than happy to submit a pull request with the necessary modifications.
Thank you for your time and consideration.
@almereyda commented on GitHub (Sep 13, 2023):
PRs are set to wait until #1473 is resolved. There it was announced end of May that no contributions are taken for the time being.
@github-actions[bot] commented on GitHub (Dec 13, 2023):
This issue is stale because it has been open for 90 days with no activity.
@vsychov commented on GitHub (Dec 13, 2023):
/remove-lifecycle stale
@github-actions[bot] commented on GitHub (Mar 14, 2024):
This issue is stale because it has been open for 90 days with no activity.
@github-actions[bot] commented on GitHub (Mar 21, 2024):
This issue was closed because it has been inactive for 14 days since being marked as stale.
@vsychov commented on GitHub (Mar 23, 2024):
/remove-lifecycle stale
@github-actions[bot] commented on GitHub (Jun 27, 2024):
This issue is stale because it has been open for 90 days with no activity.
@github-actions[bot] commented on GitHub (Jul 4, 2024):
This issue was closed because it has been inactive for 14 days since being marked as stale.
@mitchellkellett commented on GitHub (Aug 18, 2024):
@kradalby did this ever get resolved? It looks like it went stale a few times.
I’m having a similar issue where it’s not using the refresh token. Not sure if it should be to start with.
@baznikin commented on GitHub (Aug 27, 2024):
/remove-lifecycle stale
@baznikin commented on GitHub (Aug 27, 2024):
Please, implement
refresh_tokensupport@kradalby commented on GitHub (Sep 18, 2024):
I've tried to read up on this and understand the usecase, and how the oauth/oidc part plays in and I am still a bit unsure about somethings, so I will lay it out how I understand it and I need someone to elaborate on the things I have misunderstood and usecases.
First I want to establish that from what I understand, Tailscale SaaS does not implement neither using the access token nor refresh tokens for expiry. They use their own key expiry time, which we also do as default.
In addition, headscale has a config option for making the expiry being set to the access token's expiry time.
About OIDC (or I suppose OAuth2 technically), access tokens are a part of OAuth2 to let authorise access to a service, typically from a program or another service. In headscale, access tokens are not used for anything.
OIDC is about Authentication, which is what headscale does, you log in with OIDC, we take notice that you were allowed to login and then we mark the node as authenticated.
This part does not really have any expiry, you log in, your approved, and headscale chooses the expiry.
Refresh tokens are an additional (optional) layer of security to OAuth2 to mitigate some attacks. It work by issuing a token with a longer expiry, the refresh token, which is authorised to issue new, short lived access tokens.
Two examples:
OIDC provider 1 is using refresh tokens, when a user logs inn, they are issued a refresh token which is valid for 2 days. The user (or the program using the token) can use this key to create access tokens which are valid for 5 minutes.
When the 2 days are up, the user will have to log in again.
OIDC provider 2 only uses access tokens, they have a validity of 1 day, and after 1 day the user will have to log in again.
as mentioned, headscale currently has a flag to make a nodes authentication use the expiry time of the access token, that means that with provider 1, the node will expire after 5 minutes, while with provider 2, it will expire after 1 day.
I think an important thing to notice here is that headscale does not need the OAuth/authorisation part of this. The access token does not play into how it works, and the only meaningful use of the protocol is to authenticate the user to see if it can access the service.
It sounds misguided to me that a third party system (the OIDC provider) should dictate the expiry time of a node logged into by a user. It sounds more logical to me that the operator of headscale would set the desired expiry time in headscale to achieve the security configuration that they are after.
There was a big discussion for when access tokens was addressed in https://github.com/juanfont/headscale/issues/1176. I think using access token's expiry is still wrong, but I do not plan to remove it.
@mitchellkellett commented on GitHub (Sep 19, 2024):
Thanks for taking a look Kristoffer.
I think you're right with saying this, though my thoughts regarding the refresh token are that when the node expires, Headscale will automatically try and refresh the nodes expiry by using the refresh token. If the user's account is disabled/deleted, then Headscale won't be able to.
Currently, when a node expires, the user has to go back through the OIDC flow to reauthenticate the node, if we can use the refresh token, then this step would happen automatically and only require intervention when it can't use the refresh token automatically.
My ideal outcome would be that I can set the Headscale nodes to expire daily and then let refresh tokens run their course. That way that if a user is disabled/deleted in my IdP, then the next day their nodes are expired.
@kradalby commented on GitHub (Sep 19, 2024):
It wont be instantly reactive no, but you could solve this by using the API to expire/delete the node.
While I dont disagree that it sounds like a nice experience, it sounds problematic for a couple of reasons,
Since headscale is software that is very much in the security path, and has a very small maintainer team, keeping the auth path as simple as possible is desirable, any bugs in this path will put us in a very bad position. Implementing something like this does not seem worth the effort and risk. I dont think Tailscale SaaS implements this either, telling me that if the software targeting business dont do it, then its not a good thing to invest time in.
If you want users to log in often, then you should do a short expiry window, and make them reauth with oidc.
If there is a risk that your users might have been deleted in the meantime, use the API to purge them.
The access token support is there, it will not be remove, but I would not recommend using it, and I'll probably mark it as such.
@vsychov commented on GitHub (Sep 21, 2024):
Access tokens are a well-established technology that, when configured and managed correctly, can significantly enhance both security and user experience.
What risks do you see in someone stealing refresh tokens? The access token that the user receives doesn’t have any real privileges. So, using the refresh token doesn’t give the attacker any advantage. Essentially, the access token is only needed for headscale to verify that the user is who they claim to be. The refresh token, typically single-use, just enables automatic renewal of the access token. It seems that there’s no significant security risk if token permissions are set correctly.
As for storing and refreshing tokens, it doesn’t seem like a particularly complex task. A single database table could store the tokens, and a background goroutine could refresh them at regular intervals. Additionally, the libraries that headscale uses for OIDC already support this mechanism.
Regarding automated testing, it also seems manageable. There are already existing tests for this functionality, and they would only need to be slightly expanded rather than completely rewritten.
I’d love to hear your thoughts on this!
@baznikin commented on GitHub (May 21, 2025):
https://github.com/PacktPublishing/Kubernetes---An-Enterprise-Guide-2E/blob/main/chapter5/B17950_Chapter_05.pdf - here is a good explanation of how it is worked (chapter from a book, released by authors for free).
With short-lived
access-tokenand long-livedrefresh-tokenwe can have allow to work without frequent re-authentication (lets sayaccess-tokenwith 10min, 1H or 24H expiration andrefresh-token1week or 1moth), but still can log-out user withinaccess-tokenexpiration time-span by dropping his session on IdP server.When we run IdP service - headscale is usually one of many tenths of services which we want to manage with one central user/group facility. Yeah, someone can script anything, but why? OpenID protocol provide all possibilities to implement it in a clean way without ugly hacks.
@mazlumtoprak commented on GitHub (Jul 24, 2025):
I have followed this issue and identified it also as a requirement for us. From my point of view, the combination of security and convenience are why headscale should implement
refresh_tokens.With the current solution (using
use_expiry_from_token), the node is expiring, when theaccess_tokenis no longer valid. Once this expiry happens, the user has to authenticate via OIDC again, which can be disrupting as he otherwise would lose access to the tailnet until re-authenticated. Depending on the OIDC provider, this can range from 5 minutes to 90 minutes. We could use the headscale implementation of the expiry but this would leave us with a security concern.In case a user's personal device is compromised, I want to be able to disable this user in the Identity Provider and limit the potential damage this way for all applications the user has access to. As @baznikin has mentioned, that can get quite fast out of hand with a lot of services. When using the headscale implementation we would need to manually think about deactivating the user on the headscale instance, whereas with refresh tokens, the user can't issue a new
access_token, once the user has been disabled on the IDP and the oldaccess_tokenhas expired.To revive this issue, we have decided to a create a small proof of concept with refresh tokens. I hope this doesn't get you on the wrong foot. We have read the contributions guidelines and assumed that this issue is already a good ground for discussions.
Here's the draft PR that I have made: https://github.com/juanfont/headscale/pull/2704 . I did this with trying to change as little as possible while introducing
refresh_tokenmechanism. I hope that this PR can be the ground for further discussion and potentially can be improved to be merged upstream. It's possible I didn't catch all the edge cases yet and am open for feedback!Edit: Need to fix merge conflicts. branched away from v0.26.1