Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Accept list of strings in audience claim in token auth #3742

Merged
merged 3 commits into from
Apr 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion contrib/token-server/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ func (issuer *TokenIssuer) CreateJWT(subject string, audience string, grantedAcc
claimSet := token.ClaimSet{
Issuer: issuer.Issuer,
Subject: subject,
Audience: audience,
Audience: []string{audience},
Expiration: now.Add(exp).Unix(),
NotBefore: now.Unix(),
IssuedAt: now.Unix(),
Expand Down
34 changes: 18 additions & 16 deletions registry/auth/token/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,13 @@ type ResourceActions struct {
// ClaimSet describes the main section of a JSON Web Token.
type ClaimSet struct {
// Public claims
Issuer string `json:"iss"`
Subject string `json:"sub"`
Audience string `json:"aud"`
Expiration int64 `json:"exp"`
NotBefore int64 `json:"nbf"`
IssuedAt int64 `json:"iat"`
JWTID string `json:"jti"`
Issuer string `json:"iss"`
Subject string `json:"sub"`
Audience AudienceList `json:"aud"`
Expiration int64 `json:"exp"`
NotBefore int64 `json:"nbf"`
IssuedAt int64 `json:"iat"`
JWTID string `json:"jti"`

// Private claims
Access []*ResourceActions `json:"access"`
Expand Down Expand Up @@ -141,8 +141,8 @@ func (t *Token) Verify(verifyOpts VerifyOptions) error {
}

// Verify that the Audience claim is allowed.
if !contains(verifyOpts.AcceptedAudiences, t.Claims.Audience) {
log.Infof("token intended for another audience: %q", t.Claims.Audience)
if !containsAny(verifyOpts.AcceptedAudiences, t.Claims.Audience) {
log.Infof("token intended for another audience: %v", t.Claims.Audience)
return ErrInvalidToken
}

Expand Down Expand Up @@ -185,13 +185,15 @@ func (t *Token) Verify(verifyOpts VerifyOptions) error {

// VerifySigningKey attempts to get the key which was used to sign this token.
// The token header should contain either of these 3 fields:
// `x5c` - The x509 certificate chain for the signing key. Needs to be
// verified.
// `jwk` - The JSON Web Key representation of the signing key.
// May contain its own `x5c` field which needs to be verified.
// `kid` - The unique identifier for the key. This library interprets it
// as a libtrust fingerprint. The key itself can be looked up in
// the trustedKeys field of the given verify options.
//
// `x5c` - The x509 certificate chain for the signing key. Needs to be
// verified.
// `jwk` - The JSON Web Key representation of the signing key.
// May contain its own `x5c` field which needs to be verified.
// `kid` - The unique identifier for the key. This library interprets it
// as a libtrust fingerprint. The key itself can be looked up in
// the trustedKeys field of the given verify options.
//
// Each of these methods are tried in that order of preference until the
// signing key is found or an error is returned.
func (t *Token) VerifySigningKey(verifyOpts VerifyOptions) (signingKey libtrust.PublicKey, err error) {
Expand Down
10 changes: 5 additions & 5 deletions registry/auth/token/token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ func makeTestToken(issuer, audience string, access []*ResourceActions, rootKey l
claimSet := &ClaimSet{
Issuer: issuer,
Subject: "foo",
Audience: audience,
Audience: []string{audience},
Expiration: exp.Unix(),
NotBefore: now.Unix(),
IssuedAt: now.Unix(),
Expand Down Expand Up @@ -307,10 +307,10 @@ func writeTempRootCerts(rootKeys []libtrust.PrivateKey) (filename string, err er
// TestAccessController tests complete integration of the token auth package.
// It starts by mocking the options for a token auth accessController which
// it creates. It then tries a few mock requests:
// - don't supply a token; should error with challenge
// - supply an invalid token; should error with challenge
// - supply a token with insufficient access; should error with challenge
// - supply a valid token; should not error
// - don't supply a token; should error with challenge
// - supply an invalid token; should error with challenge
// - supply a token with insufficient access; should error with challenge
// - supply a valid token; should not error
func TestAccessController(t *testing.T) {
// Make 2 keys; only the first is to be a trusted root key.
rootKeys, err := makeRootKeys(2)
Expand Down
55 changes: 55 additions & 0 deletions registry/auth/token/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package token

import (
"encoding/json"
"reflect"
)

// AudienceList is a slice of strings that can be deserialized from either a single string value or a list of strings.
type AudienceList []string

func (s *AudienceList) UnmarshalJSON(data []byte) (err error) {
var value interface{}

if err = json.Unmarshal(data, &value); err != nil {
return err
}

switch v := value.(type) {
case string:
*s = []string{v}

case []string:
*s = v

case []interface{}:
var ss []string

for _, vv := range v {
vs, ok := vv.(string)
if !ok {
return &json.UnsupportedTypeError{
Type: reflect.TypeOf(vv),
}
}

ss = append(ss, vs)
}

*s = ss

case nil:
return nil

default:
return &json.UnsupportedTypeError{
Type: reflect.TypeOf(v),
}
}

return
}

func (s AudienceList) MarshalJSON() (b []byte, err error) {
return json.Marshal([]string(s))
}
85 changes: 85 additions & 0 deletions registry/auth/token/types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package token

import (
"encoding/json"
"testing"
)

func TestAudienceList_Unmarshal(t *testing.T) {
t.Run("OK", func(t *testing.T) {
testCases := []struct {
value string
expected AudienceList
}{
{
value: `"audience"`,
expected: AudienceList{"audience"},
},
{
value: `["audience1", "audience2"]`,
expected: AudienceList{"audience1", "audience2"},
},
{
value: `null`,
expected: nil,
},
}

for _, testCase := range testCases {
testCase := testCase

t.Run("", func(t *testing.T) {
var actual AudienceList

err := json.Unmarshal([]byte(testCase.value), &actual)
if err != nil {
t.Fatal(err)
}

assertStringListEqual(t, testCase.expected, actual)
})
}
})

t.Run("Error", func(t *testing.T) {
var actual AudienceList

err := json.Unmarshal([]byte("1234"), &actual)
if err == nil {
t.Fatal("expected unmarshal to fail")
}
})
}

func TestAudienceList_Marshal(t *testing.T) {
value := AudienceList{"audience"}

expected := `["audience"]`

actual, err := json.Marshal(value)
if err != nil {
t.Fatal(err)
}

if expected != string(actual) {
t.Errorf("expected marshaled list to be %v, got %v", expected, actual)
}
}

func assertStringListEqual(t *testing.T, expected []string, actual []string) {
t.Helper()

if len(expected) != len(actual) {
t.Errorf("length mismatch: expected %d long slice, got %d", len(expected), len(actual))

return
}

for i, v := range expected {
if v != actual[i] {
t.Errorf("expected %d. item to be %q, got %q", i, v, actual[i])
}

return
}
}
11 changes: 11 additions & 0 deletions registry/auth/token/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,14 @@ func contains(ss []string, q string) bool {

return false
}

// containsAny returns true if any of q is found in ss.
func containsAny(ss []string, q []string) bool {
for _, s := range ss {
if contains(q, s) {
return true
}
}

return false
}