Repository navigation
server: accept offline_access scope - #209
Merged
Merged
Conversation
Clients that ask for a refresh token the standard way, with the offline_access scope from OpenID Connect Core 1.0 section 11, were rejected at /authorize with invalid_scope. BookLore is one such client. Add offline_access to the supported scopes so /authorize accepts it and both discovery documents advertise it. This does not change what tsidp issues: the token endpoint already returns a refresh token for every authorization code grant, whether or not the client asks for offline_access. A new test covers the flow with and without the scope, so a later change to when refresh tokens are issued has to be deliberate. Fixes tailscale#109 Signed-off-by: Graham Mann <graham.mann14@gmail.com>
Real tailnet nodes always have one. Without it the test would fail once refresh grants look the user up again by node address. Signed-off-by: Graham Mann <graham.mann14@gmail.com>
jentfoo
approved these changes
Nov 10, 2026
jentfoo
left a comment
Member
There was a problem hiding this comment.
Looks reasonable to me, I don't think this needs a config flag.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #109
@mostlygeek, this is the PR you asked for on #109.
Clients that ask for a refresh token the standard way, by adding
offline_accessto the scope (OIDC Core 1.0 section 11), geterror=invalid_scopeback from/authorize, so the login fails before it reaches the token endpoint. BookLore is one of these clients.This adds
offline_accesstoopenIDSupportedScopes./authorizenow accepts it, and both/.well-known/openid-configurationand/.well-known/oauth-authorization-serverlist it inscopes_supported. The scope is stored with the request and shows up in introspection like any other scope. Nothing else reads it.What doesn't change
Refresh token issuance. The token endpoint already returns a refresh token for every authorization code grant, whether or not the client asks for
offline_access(30-day lifetime, single use, held in memory). After this change, a client that asks foroffline_accessgets exactly what a client that doesn't ask has always got.TestOfflineAccessScoperuns authorize, token and refresh with and without the scope and checks that both get a working refresh token, so a future change to when refresh tokens are issued will have to update that test on purpose.Client authentication, Funnel rejection at
/authorize, and scope case-sensitivity are also unchanged.OFFLINE_ACCESSin upper case is still rejected.Spec note on consent
Section 11 says the OP must obtain consent before returning a refresh token for offline access, unless other conditions permit it, and that refresh tokens may also be issued in other contexts. tsidp has no consent step and already issues refresh tokens to every registered client. I read the admin's decision to register the client, plus the user being signed in to the tailnet, as those other conditions. That matches how tsidp behaves today; this PR only stops rejecting clients that ask. If you'd prefer tsidp to issue refresh tokens only when
offline_accessis requested, that would break clients that rely on getting one without asking, so I've left it out of this change.Two smaller notes:
On the configurable
extraScopesidea from the issue and #176: I kept this to the one scope the OIDC spec defines, since every client means the same thing by it. A configurable list could sit on top of this default.Testing
TestValidateScopes:openid offline_accessandopenid email profile offline_accessare accepted;OFFLINE_ACCESSis rejected.TestScopeHandling: anoffline_accesscase through the code exchange.TestOfflineAccessScope(new): authorize, token and refresh throughserveAuthorizeandserveToken, foropenid,openid offline_accessand all four scopes. Checks the code is issued, the scopes are stored, a refresh token is returned, and the refreshed token keeps the scopes.TestScopesSupportedMetadata(new): both discovery documents listopenid email profile offline_access.offline_accessfail onmainand pass with the change.gofmt -l .,go vet ./...,go test ./...andgo test -race ./serverare clean.Claude Code also ran the real server handler on a loopback port, with the LocalAPI WhoIs call stubbed the same way
server/helpers_test.godoes it, and went through the flow with curl. Onmain,/authorizewithscope=openid offline_accessredirected to the client witherror=invalid_scope. With this change it returned a code,/tokenreturned an ID token, access token and refresh token, introspection showedscope: "openid offline_access", the refresh grant worked, and reusing the old refresh token returnedinvalid_grant.I used Claude Code to help write this change and the tests, and it also ran them.