mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-18 20:49:56 +00:00
## Describe your changes `isValidAccessToken` only decodes the JWT payload and checks the audience claim, but its name suggested full token validation. Rename it to `validateTokenAudience` and document what it does: a client-side audience/shape check on a token just obtained from the IdP over TLS. Token authenticity is enforced server-side by the management server, which verifies the signature against the IdP's JWKS (`shared/auth/jwt/validator.go`) on every request. Also harden the parser: a non-empty token lacking the three-part JWT structure caused an index-out-of-range panic (`strings.Split(token, ".")[1]`); the shape is now validated first. `parseEmailFromIDToken` is documented as best-effort UX data (login hint/display), never used for authorization. Added tests for audience matching, malformed tokens, and the panic regression. Changes: - Rename `isValidAccessToken` → `validateTokenAudience`; document that it does not verify the signature and that authenticity is enforced server-side. - Fix an index-out-of-range panic on a non-empty token lacking JWT structure (`strings.Split(token, ".")[1]`) by validating the three-part shape first. - Document `parseEmailFromIDToken` as best-effort/unverified, used only for the login-hint/display UX, never for an authorization decision. - Add `util_test.go` covering audience matching (string and array), missing audience, malformed payloads, and the panic regression. ## Issue ticket number and link Internal cleanup: rename a misleadingly-named client-side helper and harden JWT parsing against malformed input (`client/internal/auth/util.go`). ## Stack - `0.74.7-branch` - ⚠️ No PR associated with branch <!-- branch-stack --> - \#6806 :point\_left: ### Checklist - [x] Is it a bug fix - [ ] Is a typo/documentation fix - [ ] Is a feature enhancement - [ ] It is a refactor - [x] Created tests that fail without the change (if possible) > By submitting this pull request, you confirm that you have read and agree to the terms of the [Contributor License Agreement](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT.md). ## Documentation Select exactly one: - [ ] I added/updated documentation for this change - [x] Documentation is **not needed** for this change (explain why) Internal client-side helper rename plus a panic hardening fix. No public API, gRPC, CLI/service flag, or configuration change; token authenticity enforcement (server-side JWKS verification) is unchanged. ### Docs PR URL (required if "docs added" is checked) Paste the PR link from <https://github.com/netbirdio/docs> here: N/A <!-- codesmith:footer --> *** <a href="https://app.blacksmith.sh/netbirdio/codesmith/netbird/pr/6806"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"><source media="(prefers-color-scheme: light)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-light-v2.svg"><img alt="View with Codesmith" src="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"></picture></a> <a href="https://backend.blacksmith.sh/track/enable-autofix?expires=1786811124&installation_id=146802194&pr_number=6806&repository=netbirdio%2Fnetbird&return_to=https%3A%2F%2Fgithub.com%2Fnetbirdio%2Fnetbird%2Fpull%2F6806&signature=fe45ce9f6df46a609594f84037aaab2a21893621f29d2be48d7f8b347c3c8776"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-light.svg"><img alt="Autofix with Codesmith" src="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"></picture></a> <sup>Need help on this PR? Tag <code>/codesmith</code> with what you need. Autofix is disabled.</sup> <!-- codesmith:autofix:disabled --> <!-- /codesmith:footer --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Bug Fixes** - Improved access-token audience validation during device and PKCE authentication flows. - Malformed tokens now return clear validation errors instead of risking runtime failures. - Added support for validating both string and array audience claims. - **Tests** - Added coverage for malformed tokens, invalid claims, missing audiences, and panic prevention. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
109 lines
2.8 KiB
Go
109 lines
2.8 KiB
Go
package auth
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"testing"
|
|
)
|
|
|
|
// makeJWT builds an unsigned JWT-shaped string (header.payload.signature) with
|
|
// the given claims payload. The signature part is arbitrary because
|
|
// validateTokenAudience intentionally does not verify it.
|
|
func makeJWT(t *testing.T, claims map[string]interface{}) string {
|
|
t.Helper()
|
|
header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"RS256","typ":"JWT"}`))
|
|
payloadBytes, err := json.Marshal(claims)
|
|
if err != nil {
|
|
t.Fatalf("marshal claims: %v", err)
|
|
}
|
|
payload := base64.RawURLEncoding.EncodeToString(payloadBytes)
|
|
return header + "." + payload + ".unverified-signature"
|
|
}
|
|
|
|
func TestValidateTokenAudience(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
token string
|
|
audience string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "empty token",
|
|
token: "",
|
|
audience: "netbird",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "not a JWT - no dots",
|
|
token: "notajwt",
|
|
audience: "netbird",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "not a JWT - two parts only",
|
|
token: "header.payload",
|
|
audience: "netbird",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "matching string audience",
|
|
token: makeJWT(t, map[string]interface{}{"aud": "netbird"}),
|
|
audience: "netbird",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "mismatching string audience",
|
|
token: makeJWT(t, map[string]interface{}{"aud": "other"}),
|
|
audience: "netbird",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "matching audience in array",
|
|
token: makeJWT(t, map[string]interface{}{"aud": []interface{}{"other", "netbird"}}),
|
|
audience: "netbird",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "mismatching audience array",
|
|
token: makeJWT(t, map[string]interface{}{"aud": []interface{}{"a", "b"}}),
|
|
audience: "netbird",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "missing audience claim",
|
|
token: makeJWT(t, map[string]interface{}{"sub": "user"}),
|
|
audience: "netbird",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "invalid base64 payload",
|
|
token: "header.!!!not-base64!!!.sig",
|
|
audience: "netbird",
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := validateTokenAudience(tc.token, tc.audience)
|
|
if tc.wantErr && err == nil {
|
|
t.Fatalf("expected error, got nil")
|
|
}
|
|
if !tc.wantErr && err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestValidateTokenAudienceNoPanic guards the regression where a non-empty
|
|
// token without the JWT dot structure caused an index-out-of-range panic.
|
|
func TestValidateTokenAudienceNoPanic(t *testing.T) {
|
|
inputs := []string{"a", ".", "a.", "aaaa", "no-dots-here"}
|
|
for _, in := range inputs {
|
|
if err := validateTokenAudience(in, "netbird"); err == nil {
|
|
t.Fatalf("expected error for malformed token %q, got nil", in)
|
|
}
|
|
}
|
|
}
|