diff --git a/CHANGELOG.md b/CHANGELOG.md index 54cafab..a40a411 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,3 +5,11 @@ ### Updates - Only `alg` and `kid` claims in a JWT header are considered during verification. + +## v1.1.3 + +### Updates + +- Fixed edge cause with `aud` claim that would not find Auth0 being JWTs valid (thank you @awrenn). +- Updated readme with testing notes. +- Ran `gofumpt` on code for clean up. \ No newline at end of file diff --git a/README.md b/README.md index da088ac..46a6f43 100644 --- a/README.md +++ b/README.md @@ -81,3 +81,23 @@ verifier.SetLeeway("2m") //String instance of time that will be parsed by `time. ``` [Okta Developer Forum]: https://devforum.okta.com/ + +## Testing + +If you create a PR from a fork of okta/okta-jwt-verifier-golang the build for +the PR will fail. Don't worry, we'll bring your commits into a review branch in +okta/okta-jwt-verifier-golang and get a green build. + +jwtverifier_test.go expects environment variables for `ISSUER`, `CLIENT_ID`, +`USERNAME`, and `PASSWORD` to be present. Take note if you use zshell as +`USERSNAME` is a special environment variable and is not settable. Therefore +tests shouldn't be run in zshell. + +`USERNAME` and `PASSWORD` are for a user with access to the test app associated +with `CLIENT_ID`. The test app should not have 2FA enabled and allow password +login. The General Settings for the test app should have Application Grant type +with Implicit (hybrid) enabled. + +``` +go test -test.v +``` \ No newline at end of file diff --git a/adaptors/lestrratGoJwx/lestrratGoJwx.go b/adaptors/lestrratGoJwx/lestrratGoJwx.go index c76da30..0ca3824 100644 --- a/adaptors/lestrratGoJwx/lestrratGoJwx.go +++ b/adaptors/lestrratGoJwx/lestrratGoJwx.go @@ -28,8 +28,10 @@ import ( "github.com/patrickmn/go-cache" ) -var jwkSetCache *cache.Cache = cache.New(5*time.Minute, 10*time.Minute) -var jwkSetMu = &sync.Mutex{} +var ( + jwkSetCache *cache.Cache = cache.New(5*time.Minute, 10*time.Minute) + jwkSetMu = &sync.Mutex{} +) func getJwkSet(jwkUri string) (jwk.Set, error) { jwkSetMu.Lock() @@ -39,7 +41,6 @@ func getJwkSet(jwkUri string) (jwk.Set, error) { return x.(jwk.Set), nil } jwkSet, err := jwk.Fetch(context.Background(), jwkUri) - if err != nil { return nil, err } @@ -58,17 +59,14 @@ func (lgj LestrratGoJwx) New() adaptors.Adaptor { } func (lgj LestrratGoJwx) GetKey(jwkUri string) { - return } func (lgj LestrratGoJwx) Decode(jwt string, jwkUri string) (interface{}, error) { jwkSet, err := getJwkSet(jwkUri) - if err != nil { return nil, err } token, err := jws.VerifySet([]byte(jwt), jwkSet) - if err != nil { return nil, err } @@ -78,5 +76,4 @@ func (lgj LestrratGoJwx) Decode(jwt string, jwkUri string) (interface{}, error) json.Unmarshal(token, &claims) return claims, nil - } diff --git a/discovery/oidc/oidc.go b/discovery/oidc/oidc.go index bde0f58..0d09dfa 100644 --- a/discovery/oidc/oidc.go +++ b/discovery/oidc/oidc.go @@ -18,7 +18,7 @@ package oidc import "github.com/okta/okta-jwt-verifier-golang/discovery" -type Oidc struct{ +type Oidc struct { wellKnownUrl string } diff --git a/jwtverifier.go b/jwtverifier.go index 68aab19..d778f35 100644 --- a/jwtverifier.go +++ b/jwtverifier.go @@ -34,9 +34,11 @@ import ( "github.com/patrickmn/go-cache" ) -var metaDataCache *cache.Cache = cache.New(5*time.Minute, 10*time.Minute) -var metaDataMu = &sync.Mutex{} -var regx = regexp.MustCompile(`[a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+\.?([a-zA-Z0-9-_]+)[/a-zA-Z0-9-_]+?$`) +var ( + metaDataCache *cache.Cache = cache.New(5*time.Minute, 10*time.Minute) + metaDataMu = &sync.Mutex{} + regx = regexp.MustCompile(`[a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+\.?([a-zA-Z0-9-_]+)[/a-zA-Z0-9-_]+?$`) +) type JwtVerifier struct { Issuer string @@ -80,7 +82,7 @@ func (j *JwtVerifier) SetLeeway(duration string) { func (j *JwtVerifier) VerifyAccessToken(jwt string) (*Jwt, error) { validJwt, err := j.isValidJwt(jwt) - if validJwt == false { + if !validJwt { return nil, fmt.Errorf("token is not valid: %s", err.Error()) } @@ -133,7 +135,6 @@ func (j *JwtVerifier) decodeJwt(jwt string) (interface{}, error) { return nil, fmt.Errorf("failed to decode JWT: missing 'jwks_uri' from metadata") } resp, err := j.Adaptor.Decode(jwt, jwksURI) - if err != nil { return nil, fmt.Errorf("could not decode token: %s", err.Error()) } @@ -143,7 +144,7 @@ func (j *JwtVerifier) decodeJwt(jwt string) (interface{}, error) { func (j *JwtVerifier) VerifyIdToken(jwt string) (*Jwt, error) { validJwt, err := j.isValidJwt(jwt) - if validJwt == false { + if !validJwt { return nil, fmt.Errorf("token is not valid: %s", err.Error()) } @@ -206,7 +207,6 @@ func (j *JwtVerifier) validateNonce(nonce interface{}) error { } func (j *JwtVerifier) validateAudience(audience interface{}) error { - switch v := audience.(type) { case string: if v != j.ClaimsToValidate["aud"] { @@ -219,8 +219,19 @@ func (j *JwtVerifier) validateAudience(audience interface{}) error { } } return fmt.Errorf("aud: %s does not match %s", v, j.ClaimsToValidate["aud"]) + case []interface{}: + for _, e := range v { + element, ok := e.(string) + if !ok { + return fmt.Errorf("unknown type for audience validation") + } + if element == j.ClaimsToValidate["aud"] { + return nil + } + } + return fmt.Errorf("aud: %s does not match %s", v, j.ClaimsToValidate["aud"]) default: - return fmt.Errorf("Unknown type for audience validation") + return fmt.Errorf("unknown type for audience validation") } return nil @@ -229,7 +240,6 @@ func (j *JwtVerifier) validateAudience(audience interface{}) error { func (j *JwtVerifier) validateClientId(clientId interface{}) error { // Client Id can be optional, it will be validated if it is present in the ClaimsToValidate array if cid, exists := j.ClaimsToValidate["cid"]; exists && clientId != cid { - switch v := clientId.(type) { case string: if v != cid { @@ -243,9 +253,8 @@ func (j *JwtVerifier) validateClientId(clientId interface{}) error { } return fmt.Errorf("aud: %s does not match %s", v, cid) default: - return fmt.Errorf("Unknown type for clientId validation") + return fmt.Errorf("unknown type for clientId validation") } - } return nil } @@ -290,7 +299,6 @@ func (j *JwtVerifier) getMetaData() (map[string]interface{}, error) { } resp, err := http.Get(metaDataUrl) - if err != nil { return nil, fmt.Errorf("request for metadata was not successful: %s", err.Error()) } @@ -311,7 +319,7 @@ func (j *JwtVerifier) isValidJwt(jwt string) (bool, error) { } // Verify that the JWT Follows correct JWT encoding. - var jwtRegex = regx.MatchString + jwtRegex := regx.MatchString if !jwtRegex(jwt) { return false, fmt.Errorf("token must contain at least 1 period ('.') and only characters 'a-Z 0-9 _'") } @@ -320,14 +328,13 @@ func (j *JwtVerifier) isValidJwt(jwt string) (bool, error) { header := parts[0] header = padHeader(header) headerDecoded, err := base64.StdEncoding.DecodeString(header) - if err != nil { return false, fmt.Errorf("the tokens header does not appear to be a base64 encoded string") } var jsonObject map[string]interface{} isHeaderJson := json.Unmarshal([]byte(headerDecoded), &jsonObject) == nil - if isHeaderJson == false { + if !isHeaderJson { return false, fmt.Errorf("the tokens header is not a json object") } @@ -348,6 +355,7 @@ func (j *JwtVerifier) isValidJwt(jwt string) (bool, error) { return true, nil } + func padHeader(header string) string { if i := len(header) % 4; i != 0 { header += strings.Repeat("=", 4-i) diff --git a/jwtverifier_test.go b/jwtverifier_test.go index 9008d01..6817d83 100644 --- a/jwtverifier_test.go +++ b/jwtverifier_test.go @@ -350,12 +350,11 @@ func Test_a_successful_authentication_can_have_its_tokens_parsed(t *testing.T) { postValues := map[string]string{"username": os.Getenv("USERNAME"), "password": os.Getenv("PASSWORD")} postJsonValues, _ := json.Marshal(postValues) resp, err := http.Post(requestUri, "application/json", bytes.NewReader(postJsonValues)) - if err != nil { t.Errorf("could not submit authentication endpoint") } defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) + body, _ := ioutil.ReadAll(resp.Body) var authn AuthnResponse err = json.Unmarshal(body, &authn) @@ -406,7 +405,6 @@ func Test_a_successful_authentication_can_have_its_tokens_parsed(t *testing.T) { } claims, err := jv.New().VerifyIdToken(idToken) - if err != nil { t.Errorf("could not verify id_token: %s", err.Error()) } diff --git a/utils/nonce.go b/utils/nonce.go index 03e43de..625e208 100644 --- a/utils/nonce.go +++ b/utils/nonce.go @@ -1,9 +1,9 @@ package utils import ( - "fmt" - "encoding/base64" "crypto/rand" + "encoding/base64" + "fmt" ) func GenerateNonce() (string, error) { diff --git a/utils/parseEnv.go b/utils/parseEnv.go index 85c1b83..1637b9d 100644 --- a/utils/parseEnv.go +++ b/utils/parseEnv.go @@ -24,10 +24,8 @@ import ( ) func ParseEnvironment() { - //useGlobalEnv := true if _, err := os.Stat(".env"); os.IsNotExist(err) { log.Printf("Environment Variable file (.env) is not present. Relying on Global Environment Variables") - //useGlobalEnv = false } setEnvVariable("CLIENT_ID", os.Getenv("CLIENT_ID")) @@ -43,8 +41,8 @@ func ParseEnvironment() { log.Printf("Could not resolve a ISSUER environment variable.") os.Exit(1) } - } + func setEnvVariable(env string, current string) { if current != "" { return