diff --git a/authutils/errors.go b/authutils/errors.go index 240f1e1..9824b35 100644 --- a/authutils/errors.go +++ b/authutils/errors.go @@ -38,8 +38,12 @@ func expired(timestamp int64) error { return errors.New(msg) } -func missingAudience(missingAud string, containsAuds []string) error { - containsString := strings.Join(containsAuds, ", ") - msg := fmt.Sprintf("token missing required audience: %s; contains: %s\n", missingAud, containsString) +func missingScope(missingScope string, containsScopes []string) error { + containsString := strings.Join(containsScopes, ", ") + msg := fmt.Sprintf("token missing required scope: %s; contains: %s\n", missingScope, containsString) return errors.New(msg) } + +func missingKey(keyID string) error { + return fmt.Errorf("no key exists with ID: %s", keyID) +} diff --git a/authutils/keys.go b/authutils/keys.go index 48a2867..762f122 100644 --- a/authutils/keys.go +++ b/authutils/keys.go @@ -2,7 +2,6 @@ package authutils import ( "encoding/json" - "errors" "fmt" "net/http" @@ -41,7 +40,7 @@ func (manager *KeysManager) Lookup(keyID string) (*jose.JSONWebKey, error) { jwk, exists = manager.KeyMap[keyID] // If still no key is found, return an error. if !exists { - return jwk, errors.New(fmt.Sprintf("no key exists with ID: %s", keyID)) + return nil, missingKey(keyID) } } return jwk, nil @@ -71,7 +70,7 @@ func (manager *KeysManager) Refresh() error { // Get the JSON response from the URL configured in the manager. resp, err := http.Get(manager.URL) if err != nil { - return err + return fmt.Errorf("couldn't get keys from %s: %s", manager.URL, err.Error()) } // Parse the response JSON into a jose.JSONWebKeySet. diff --git a/authutils/test_utils.go b/authutils/test_utils.go index f334b83..6e8efa6 100644 --- a/authutils/test_utils.go +++ b/authutils/test_utils.go @@ -78,10 +78,10 @@ func publicKeyToJWK(keyID string, publicKey *rsa.PublicKey) jose.JSONWebKey { func makeDefaultClaims() Claims { exp := int(time.Now().Unix() + 1000) exampleClaims := Claims{ - "aud": []string{"test"}, - "iss": "https://example-iss.net", - "exp": exp, - "pur": "access", + "scope": []string{"test"}, + "iss": "https://example-iss.net", + "exp": exp, + "pur": "access", } return exampleClaims @@ -89,10 +89,12 @@ func makeDefaultClaims() Claims { func makeDefaultExpected() Expected { purpose := "access" + now := time.Now().Unix() + exp := &now expected := Expected{ - Audiences: []string{"test"}, + Scopes: []string{"test"}, Issuers: []string{"https://example-iss.net"}, - Expiration: time.Now().Unix(), + Expiration: exp, Purpose: &purpose, } return expected diff --git a/authutils/validate.go b/authutils/validate.go index bb4a23b..bc06db8 100644 --- a/authutils/validate.go +++ b/authutils/validate.go @@ -91,6 +91,9 @@ func checkExpiration(claims *Claims, now int64) error { // checkIssuer validates the `iss` field in the claims. func checkIssuer(claims *Claims, allowed []string) error { + if allowed == nil { + return nil + } tokenIss, exists := (*claims)["iss"] if !exists { return missingField("iss") @@ -105,30 +108,34 @@ func checkIssuer(claims *Claims, allowed []string) error { return nil } -// checkAudience validates the `aud` field in the claims. -func checkAudience(claims *Claims, expected []string) error { - tokenAud, exists := (*claims)["aud"] +// checkScope validates the `scope` field in the claims. +func checkScope(claims *Claims, expected []string) error { + // if token has a scope field but no scopes are expected this is fine + if len(expected) == 0 { + return nil + } + tokenScope, exists := (*claims)["scope"] if !exists { - return missingField("aud") + return missingField("scope") } - var aud []string - switch a := tokenAud.(type) { + var scope []string + switch a := tokenScope.(type) { case []string: - aud = a + scope = a case []interface{}: for _, value := range a { valueString, casted := value.(string) if !casted { - return fieldTypeError("aud", tokenAud, "[]string") + return fieldTypeError("scope", tokenScope, "[]string") } - aud = append(aud, valueString) + scope = append(scope, valueString) } default: - return fieldTypeError("aud", tokenAud, "[]string") + return fieldTypeError("scope", tokenScope, "[]string") } - for _, expectedAud := range expected { - if !contains(expectedAud, aud) { - return missingAudience(expectedAud, aud) + for _, expectedScope := range expected { + if !contains(expectedScope, scope) { + return missingScope(expectedScope, scope) } } return nil @@ -154,26 +161,21 @@ func checkPurpose(claims *Claims, expected *string) error { // Expected represents some values which are used to validate the claims in a // token. type Expected struct { - // Audiences is a list of expected receivers or uses of the token. - Audiences []string + // Scopes is a list of expected uses of the token. + Scopes []string `json:"scope"` // Expiration is the Unix timestamp at which the token becomes expired. - Expiration int64 + Expiration *int64 `json:"exp"` // Issuers is a list of acceptable issuers to expect tokens to contain. - Issuers []string + Issuers []string `json:"iss"` // Purpose is an optional field indicating the type of the token (access, // refresh, etc.) - Purpose *string + Purpose *string `json:"pur"` } -// selfValidate ensures that the fields provided in Expected are valid. For -// example, to validate some Claims the validator must identify with at least -// one audience (`aud`) in the claims, so these may not be empty. +// See https://tools.ietf.org/html/rfc7519 for general information on JWTs and +// basic validation, and see https://tools.ietf.org/html/rfc7523 for +// considerations for validation specific to using JWTs for the OAuth2 flow. func (expected *Expected) selfValidate() error { - // Must expect at least one audience. - if len(expected.Audiences) == 0 { - return validationError("must validate against at least one audience") - } - if expected.Purpose != nil { // Must expect one of these given purposes. if !contains(*expected.Purpose, ALLOWED_PURPOSES) { @@ -194,14 +196,18 @@ func (expected *Expected) Validate(claims *Claims) error { return err } - now := time.Now().Unix() - if err := checkExpiration(claims, now); err != nil { + exp := expected.Expiration + if exp == nil { + now := time.Now().Unix() + exp = &now + } + if err := checkExpiration(claims, *exp); err != nil { return err } if err := checkIssuer(claims, expected.Issuers); err != nil { return err } - if err := checkAudience(claims, expected.Audiences); err != nil { + if err := checkScope(claims, expected.Scopes); err != nil { return err } if err := checkPurpose(claims, expected.Purpose); err != nil { diff --git a/authutils/validate_test.go b/authutils/validate_test.go index 0adfcce..e1638da 100644 --- a/authutils/validate_test.go +++ b/authutils/validate_test.go @@ -10,7 +10,6 @@ import ( // REQUIRED_CLAIMS lists the claims which absolutely must appear in a token, // whose absence will cause it not to validate. var REQUIRED_CLAIMS []string = []string{ - "aud", "exp", "iss", }