api/v2: name the token's tags in the token answer

Only a token that can assign tags names them, as Tailscale's answer
does.
This commit is contained in:
Kristoffer Dalby committed 2026-10-28 14:42:30 +00:00
1 parent 8d4adb66e3
commit 92dd36a432
3 files changed
+25 -16

No files matched your search

+13 -2
View File
@@ -35,6 +35,9 @@ type tokenResponse struct {
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
Scope string `json:"scope,omitempty"`
// Tags names the tags of a token that can assign them, as Tailscale's
// answer does; a reader token's answer has none.
Tags string `json:"tags,omitempty"`
}
// oauthTokenHandler implements the client-credentials grant (RFC 6749 §4.4):
@@ -110,12 +113,20 @@ func oauthTokenHandler(b Backend) http.HandlerFunc {
return
}
writeJSON(w, http.StatusOK, tokenResponse{
resp := tokenResponse{
AccessToken: tokenStr,
TokenType: "Bearer",
ExpiresIn: int(accessTokenTTL.Seconds()),
Scope: strings.Join(token.Scopes, " "),
})
}
granted := scope.Parse(token.Scopes)
if scope.Grants(granted, scope.AuthKeys) || scope.Grants(granted, scope.DevicesCore) ||
scope.Grants(granted, scope.OAuthKeys) {
resp.Tags = strings.Join(token.Tags, " ")
}
writeJSON(w, http.StatusOK, resp)
}
}
+3 -14
View File
@@ -56,20 +56,9 @@ var apiv2CompatSkipReasons = map[string]string{
"oauth/O3/0/status": mintScopeRefusalStatus,
"oauth/O4/0/scope": unknownScopeGranted,
"oauth/O4/0/tags": unknownScopeGranted,
"oauth/O4/0/tags": mintTagsNotEchoed,
"oauth/E1/0/tags": mintTagsNotEchoed,
"oauth/E9a/0/tags": mintTagsNotEchoed,
"oauth/T1/0/tags": mintTagsNotEchoed,
"oauth/D0/6/tags": mintTagsNotEchoed,
"oauth/B1/0/tags": mintTagsNotEchoed,
"oauth/B3/0/tags": mintTagsNotEchoed,
"oauth/B11/1/tags": mintTagsNotEchoed,
"key-list/setup-token-keys/0/tags": mintTagsNotEchoed,
"key-list/setup-token-keys/2/tags": mintTagsNotEchoed,
"key-list/setup-token-keys/4/tags": mintTagsNotEchoed,
"key-list/setup-token-keys/6/tags": mintTagsNotEchoed,
"key-list/setup-token-keys/4/tags": mintTagsSorted,
"oauth/K5/0/status": unknownKeyType,
@@ -151,7 +140,7 @@ const (
// Differences no planned change removes yet.
const (
mintScopeRefusalStatus = "TOKEN_SCOPE_REFUSAL_STATUS: SaaS refuses a token scope the client lacks with 403 and a message; headscale with 400 invalid_scope"
mintTagsNotEchoed = "TOKEN_TAGS_NOT_ECHOED: SaaS's token answer names the token's tags; headscale's has no tags"
mintTagsSorted = "TOKEN_TAGS_SORTED: SaaS's token answer names the tags in the order its client was made with; headscale stores a client's tags sorted"
unknownKeyType = "UNKNOWN_KEY_TYPE: SaaS refuses an unknown keyType with 400 before it checks scopes; headscale checks the scope first and answers 403"
tagRefusalStatus = "TAG_REFUSAL_STATUS: SaaS refuses a tag outside the token's grant with 400 requested tags are invalid or not permitted; headscale with 403"
emptyKeyListNull = "EMPTY_KEY_LIST_NULL: SaaS answers a listing that holds no key with keys null; headscale with []"
+9
View File
@@ -215,6 +215,15 @@ func TestAPIv2OAuth_TokenEndpoint(t *testing.T) {
require.Equalf(t, http.StatusOK, statusDup, "%v", mDup)
assert.Equal(t, "auth_keys", mDup["scope"])
// It names the tags of a token that can assign them; a reader token's
// answer has no tags field, whatever its client's tags.
assert.Equal(t, "tag:ci", m["tags"])
_, readerSecret := createClient(t, baseURL, admin, []string{"auth_keys:read"}, []string{"tag:ci"})
statusReader, mReader := mintToken(t, baseURL, "", readerSecret, "", false)
require.Equalf(t, http.StatusOK, statusReader, "%v", mReader)
assert.NotContains(t, mReader, "tags")
// Bad credentials → RFC 6749 invalid_client.
statusBad, mBad := mintToken(t, baseURL, clientID, "hskey-client-deadbeefdead-"+stringOf("0", 64), "", false)
assert.Equal(t, http.StatusUnauthorized, statusBad)