CVE-2022-31145: Insufficient Session Expiration (CWE-613)
value: FlyteAdmin is the control plane for Flyte responsible for managing entities and administering workflow executions. In versions 1.1.30 and prior, authenticated users using an external identity provider can continue to use Access Tokens and ID Tokens even after they expire. Users who use FlyteAdmin as the OAuth2 Authorization Server are unaffected by this issue. A patch is available on the `master` branch of the repository. As a workaround, rotating signing keys immediately will invalidate all open sessions and force all users to attempt to obtain new tokens. Those who use this workaround should continue to rotate keys until FlyteAdmin has been upgraded and hide FlyteAdmin deployment ingress URL from the internet.
CWE description: According to WASC, "Insufficient Session Expiration is when a web site permits an attacker to reuse old session credentials or session IDs for authorization."
CVSS v3 base score: 6.5
Published: 2022-07-13T21:15Z
Repository: https://github.com/flyteorg/flyteadmin
Language: Go
Files changed: auth/authzserver/authorize_test.go, auth/authzserver/resource_server.go, auth/authzserver/resource_server_test.go
Commit date: 2022-07-13 10:37:06 -0700
Commit message: Merge pull request from GHSA-qwrj-9hmp-gpxh

* Fix claims verification for access tokens in external IdP setup

Signed-off-by: Haytham Abuelfutuh <haytham@afutuh.com>

* Add another test case for no signature

Signed-off-by: Haytham Abuelfutuh <haytham@afutuh.com>
Diff: diff --git a/auth/authzserver/authorize_test.go b/auth/authzserver/authorize_test.go
index d481283..99a01d5 100644
--- a/auth/authzserver/authorize_test.go
+++ b/auth/authzserver/authorize_test.go
@@ -63,13 +63,10 @@ func TestAuthEndpoint(t *testing.T) {
 		authEndpoint(authCtx, w, req)
 		assert.Equal(t, http.StatusForbidden, w.Code)
 	})
 }
 
-// #nosec
-const sampleIDToken = `eyJraWQiOiJaNmRtWl9UWGhkdXctalVCWjZ1RUV6dm5oLWpoTk8wWWhlbUI3cWFfTE9jIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiIwMHVra2k0OHBzSDhMaWtZVjVkNiIsIm5hbWUiOiJIYXl0aGFtIEFidWVsZnV0dWgiLCJ2ZXIiOjEsImlzcyI6Imh0dHBzOi8vZGV2LTE0MTg2NDIyLm9rdGEuY29tL29hdXRoMi9hdXNrbmdubjd1QlZpUXE2YjVkNiIsImF1ZCI6IjBvYWtraGV0ZU5qQ01FUnN0NWQ2IiwiaWF0IjoxNjE4NDUzNjc5LCJleHAiOjE2MTg0NTcyNzksImp0aSI6IklELmE0YXpLdUphVFM2YzNTeHdpWWdTMHhPbTM2bVFnVlVVN0I4V2dEdk80dFkiLCJhbXIiOlsicHdkIl0sImlkcCI6IjBvYWtrbTFjaTFVZVBwTlUwNWQ2IiwicHJlZmVycmVkX3VzZXJuYW1lIjoiaGF5dGhhbUB1bmlvbi5haSIsImF1dGhfdGltZSI6MTYxODQ0NjI0NywiYXRfaGFzaCI6Ikg5Q0FweWlrQkpGYXJ4d1FUbnB6ZFEifQ.SJ3BTD_MFcrYvTnql181Ddeb_mOm81z_S7ZKQ6P8mMgWqn94LZ2nG8k8-_odaaNAAT-M1nAFKWqZAQGvliwS1_TsD8_j0cen5zYnGcz2Uu5fFlvoHwuPgy5JYYNOXkXYgPnIb3kNkgXKbkdjS9hdbMfvnPd9rr8v0yzqf0AQBnUe-cPrzY-ZJjvh80IWDZgSjoP244tTYppPkx8UtedJLJZ4tzB7aXlEyoRV-DpmOLfJkAmblRm4OsO1qjwmx3HSIy_T-0PANn-g4AS07rpoMYHRcqncdgcAsVfGxjyWiOg3kbymLqpGlkIZgzmev-TmpoDp0QkUVPOntuiB57GZ6g`
-
 //func TestAuthCallbackEndpoint(t *testing.T) {
 //	originalURL := "http://localhost:8088/oauth2/authorize?client_id=my-client&redirect_uri=http%3A%2F%2Flocalhost%3A3846%2Fcallback&response_type=code&scope=photos+openid+offline&state=some-random-state-foobar&nonce=some-random-nonce&code_challenge=p0v_UR0KrXl4--BpxM2BQa7qIW5k3k4WauBhjmkVQw8&code_challenge_method=S256"
 //	req := httptest.NewRequest(http.MethodGet, originalURL, nil)
 //	w := httptest.NewRecorder()
 //
diff --git a/auth/authzserver/resource_server.go b/auth/authzserver/resource_server.go
index 78e8952..19f5dbf 100644
--- a/auth/authzserver/resource_server.go
+++ b/auth/authzserver/resource_server.go
@@ -2,10 +2,11 @@ package authzserver
 
 import (
 	"context"
 	"encoding/json"
 	"fmt"
+	jwtgo "github.com/golang-jwt/jwt/v4"
 	"io/ioutil"
 	"mime"
 	"net/http"
 	"net/url"
 	"strings"
@@ -26,21 +27,25 @@ type ResourceServer struct {
 	signatureVerifier oidc.KeySet
 	allowedAudience   []string
 }
 
 func (r ResourceServer) ValidateAccessToken(ctx context.Context, expectedAudience, tokenStr string) (interfaces.IdentityContext, error) {
-	raw, err := r.signatureVerifier.VerifySignature(ctx, tokenStr)
+	_, err := r.signatureVerifier.VerifySignature(ctx, tokenStr)
 	if err != nil {
 		return nil, err
 	}
 
-	claimsRaw := map[string]interface{}{}
-	if err = json.Unmarshal(raw, &claimsRaw); err != nil {
-		return nil, fmt.Errorf("failed to unmarshal user info claim into UserInfo type. Error: %w", err)
+	t, _, err := jwtgo.NewParser().ParseUnverified(tokenStr, jwtgo.MapClaims{})
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse token: %v", err)
+	}
+
+	if err = t.Claims.Valid(); err != nil {
+		return nil, fmt.Errorf("failed to validate token: %v", err)
 	}
 
-	return verifyClaims(sets.NewString(append(r.allowedAudience, expectedAudience)...), claimsRaw)
+	return verifyClaims(sets.NewString(append(r.allowedAudience, expectedAudience)...), t.Claims.(jwtgo.MapClaims))
 }
 
 func doRequest(ctx context.Context, req *http.Request) (*http.Response, error) {
 	client := http.DefaultClient
 	if c, ok := ctx.Value(oauth2.HTTPClient).(*http.Client); ok {
diff --git a/auth/authzserver/resource_server_test.go b/auth/authzserver/resource_server_test.go
index 002924d..e300f08 100644
--- a/auth/authzserver/resource_server_test.go
+++ b/auth/authzserver/resource_server_test.go
@@ -1,17 +1,22 @@
 package authzserver
 
 import (
 	"context"
+	"crypto/rand"
+	"crypto/rsa"
 	"encoding/json"
 	"io"
 	"net/http"
 	"net/http/httptest"
 	"net/url"
 	"reflect"
 	"strings"
 	"testing"
+	"time"
+
+	"github.com/golang-jwt/jwt/v4"
 
 	"github.com/stretchr/testify/assert"
 
 	"github.com/lestrrat-go/jwx/jwk"
 
@@ -19,32 +24,40 @@ import (
 	"github.com/flyteorg/flyteadmin/auth/config"
 	authConfig "github.com/flyteorg/flyteadmin/auth/config"
 	stdlibConfig "github.com/flyteorg/flytestdlib/config"
 )
 
-func newMockResourceServer(t testing.TB) ResourceServer {
+func newMockResourceServer(t testing.TB, publicKey rsa.PublicKey) (resourceServer ResourceServer, closer func()) {
 	ctx := context.Background()
 	dummy := ""
 	serverURL := &dummy
 	hf := func(w http.ResponseWriter, r *http.Request) {
 		if r.URL.Path == "/.well-known/oauth-authorization-server" {
 			w.Header().Set("Content-Type", "application/json")
 			_, err := io.WriteString(w, strings.ReplaceAll(`{
-				"issuer": "https://dev-14186422.okta.com",
+				"issuer": "https://whatever.okta.com",
 				"authorization_endpoint": "https://example.com/auth",
 				"token_endpoint": "https://example.com/token",
-				"jwks_uri": "URL/keys",
+				"jwks_uri": "{URL}/keys",
 				"id_token_signing_alg_values_supported": ["RS256"]
-			}`, "URL", *serverURL))
+			}`, "{URL}", *serverURL))
 
 			if !assert.NoError(t, err) {
 				t.FailNow()
 			}
 
 			return
 		} else if r.URL.Path == "/keys" {
 			keys := jwk.NewSet()
+			key := jwk.NewRSAPublicKey()
+			err := key.FromRaw(&publicKey)
+			if err != nil {
+				http.Error(w, err.Error(), 400)
+				return
+			}
+
+			keys.Add(key)
 			raw, err := json.Marshal(keys)
 			if err != nil {
 				http.Error(w, err.Error(), 400)
 				return
 			}
@@ -53,40 +66,131 @@ func newMockResourceServer(t testing.TB) ResourceServer {
 			_, err = io.WriteString(w, string(raw))
 
 			if !assert.NoError(t, err) {
 				t.FailNow()
 			}
+
+			return
 		}
 
 		http.NotFound(w, r)
 	}
 
 	s := httptest.NewServer(http.HandlerFunc(hf))
-	defer s.Close()
-
 	*serverURL = s.URL
 
 	http.DefaultClient = s.Client()
 
 	r, err := NewOAuth2ResourceServer(ctx, authConfig.ExternalAuthorizationServer{
-		BaseURL: stdlibConfig.URL{URL: *config.MustParseURL(s.URL)},
+		BaseURL:         stdlibConfig.URL{URL: *config.MustParseURL(s.URL)},
+		AllowedAudience: []string{"https://localhost"},
 	}, stdlibConfig.URL{})
 	if !assert.NoError(t, err) {
 		t.FailNow()
 	}
 
-	return r
-}
-
-func TestNewOAuth2ResourceServer(t *testing.T) {
-	newMockResourceServer(t)
+	return r, func() {
+		s.Close()
+	}
 }
 
 func TestResourceServer_ValidateAccessToken(t *testing.T) {
-	r := newMockResourceServer(t)
-	_, err := r.ValidateAccessToken(context.Background(), "myserver", sampleIDToken)
-	assert.Error(t, err)
+	sampleRSAKey, err := rsa.GenerateKey(rand.Reader, 2048)
+	if !assert.NoError(t, err) {
+		t.FailNow()
+	}
+
+	r, closer := newMockResourceServer(t, sampleRSAKey.PublicKey)
+	defer closer()
+
+	t.Run("No signature", func(t *testing.T) {
+		sampleIDToken, err := jwt.NewWithClaims(jwt.SigningMethodRS512, jwt.RegisteredClaims{
+			Audience:  r.allowedAudience,
+			ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
+			IssuedAt:  jwt.NewNumericDate(time.Now()),
+			Issuer:    "localhost",
+			Subject:   "someone",
+		}).SignedString(sampleRSAKey)
+		if !assert.NoError(t, err) {
+			t.FailNow()
+		}
+
+		parts := strings.Split(sampleIDToken, ".")
+		sampleIDToken = strings.Join(parts[:len(parts)-1], ".") + "."
+
+		_, err = r.ValidateAccessToken(context.Background(), "myserver", sampleIDToken)
+		if !assert.Error(t, err) {
+			t.FailNow()
+		}
+
+		assert.Contains(t, err.Error(), "failed to verify id token signature")
+	})
+
+	t.Run("Invalid signature", func(t *testing.T) {
+		sampleRSAKey, err := rsa.GenerateKey(rand.Reader, 2048)
+		if !assert.NoError(t, err) {
+			t.FailNow()
+		}
+
+		sampleIDToken, err := jwt.NewWithClaims(jwt.SigningMethodRS512, jwt.RegisteredClaims{
+			Audience:  r.allowedAudience,
+			ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
+			IssuedAt:  jwt.NewNumericDate(time.Now()),
+			Issuer:    "localhost",
+			Subject:   "someone",
+		}).SignedString(sampleRSAKey)
+		if !assert.NoError(t, err) {
+			t.FailNow()
+		}
+
+		_, err = r.ValidateAccessToken(context.Background(), "myserver", sampleIDToken)
+		if !assert.Error(t, err) {
+			t.FailNow()
+		}
+
+		assert.Contains(t, err.Error(), "failed to verify id token signature")
+	})
+
+	t.Run("Invalid audience", func(t *testing.T) {
+		sampleIDToken, err := jwt.NewWithClaims(jwt.SigningMethodRS512, jwt.RegisteredClaims{
+			Audience:  []string{"https://hello world"},
+			ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
+			IssuedAt:  jwt.NewNumericDate(time.Now()),
+			Issuer:    "localhost",
+			Subject:   "someone",
+		}).SignedString(sampleRSAKey)
+		if !assert.NoError(t, err) {
+			t.FailNow()
+		}
+
+		_, err = r.ValidateAccessToken(context.Background(), "myserver", sampleIDToken)
+		if !assert.Error(t, err) {
+			t.FailNow()
+		}
+
+		assert.Contains(t, err.Error(), "invalid audience")
+	})
+
+	t.Run("Expired token", func(t *testing.T) {
+		sampleIDToken, err := jwt.NewWithClaims(jwt.SigningMethodRS512, jwt.StandardClaims{
+			Audience:  r.allowedAudience[0],
+			ExpiresAt: time.Now().Add(-time.Hour).Unix(),
+			IssuedAt:  time.Now().Add(-2 * time.Hour).Unix(),
+			Issuer:    "localhost",
+			Subject:   "someone",
+		}).SignedString(sampleRSAKey)
+		if !assert.NoError(t, err) {
+			t.FailNow()
+		}
+
+		_, err = r.ValidateAccessToken(context.Background(), "myserver", sampleIDToken)
+		if !assert.Error(t, err) {
+			t.FailNow()
+		}
+
+		assert.Contains(t, err.Error(), "failed to validate token: Token is expired")
+	})
 }
 
 func Test_doRequest(t *testing.T) {
 	type args struct {
 		ctx context.Context

Vulnerable code: // #nosec
const sampleIDToken = `eyJraWQiOiJaNmRtWl9UWGhkdXctalVCWjZ1RUV6dm5oLWpoTk8wWWhlbUI3cWFfTE9jIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiIwMHVra2k0OHBzSDhMaWtZVjVkNiIsIm5hbWUiOiJIYXl0aGFtIEFidWVsZnV0dWgiLCJ2ZXIiOjEsImlzcyI6Imh0dHBzOi8vZGV2LTE0MTg2NDIyLm9rdGEuY29tL29hdXRoMi9hdXNrbmdubjd1QlZpUXE2YjVkNiIsImF1ZCI6IjBvYWtraGV0ZU5qQ01FUnN0NWQ2IiwiaWF0IjoxNjE4NDUzNjc5LCJleHAiOjE2MTg0NTcyNzksImp0aSI6IklELmE0YXpLdUphVFM2YzNTeHdpWWdTMHhPbTM2bVFnVlVVN0I4V2dEdk80dFkiLCJhbXIiOlsicHdkIl0sImlkcCI6IjBvYWtrbTFjaTFVZVBwTlUwNWQ2IiwicHJlZmVycmVkX3VzZXJuYW1lIjoiaGF5dGhhbUB1bmlvbi5haSIsImF1dGhfdGltZSI6MTYxODQ0NjI0NywiYXRfaGFzaCI6Ikg5Q0FweWlrQkpGYXJ4d1FUbnB6ZFEifQ.SJ3BTD_MFcrYvTnql181Ddeb_mOm81z_S7ZKQ6P8mMgWqn94LZ2nG8k8-_odaaNAAT-M1nAFKWqZAQGvliwS1_TsD8_j0cen5zYnGcz2Uu5fFlvoHwuPgy5JYYNOXkXYgPnIb3kNkgXKbkdjS9hdbMfvnPd9rr8v0yzqf0AQBnUe-cPrzY-ZJjvh80IWDZgSjoP244tTYppPkx8UtedJLJZ4tzB7aXlEyoRV-DpmOLfJkAmblRm4OsO1qjwmx3HSIy_T-0PANn-g4AS07rpoMYHRcqncdgcAsVfGxjyWiOg3kbymLqpGlkIZgzmev-TmpoDp0QkUVPOntuiB57GZ6g`

	raw, err := r.signatureVerifier.VerifySignature(ctx, tokenStr)
	claimsRaw := map[string]interface{}{}
	if err = json.Unmarshal(raw, &claimsRaw); err != nil {
		return nil, fmt.Errorf("failed to unmarshal user info claim into UserInfo type. Error: %w", err)
	return verifyClaims(sets.NewString(append(r.allowedAudience, expectedAudience)...), claimsRaw)
func newMockResourceServer(t testing.TB) ResourceServer {
				"issuer": "https://dev-14186422.okta.com",
				"jwks_uri": "URL/keys",
			}`, "URL", *serverURL))
	defer s.Close()

		BaseURL: stdlibConfig.URL{URL: *config.MustParseURL(s.URL)},
	return r
}

func TestNewOAuth2ResourceServer(t *testing.T) {
	newMockResourceServer(t)
	r := newMockResourceServer(t)
	_, err := r.ValidateAccessToken(context.Background(), "myserver", sampleIDToken)
	assert.Error(t, err)
Fixed code: 	jwtgo "github.com/golang-jwt/jwt/v4"
	_, err := r.signatureVerifier.VerifySignature(ctx, tokenStr)
	t, _, err := jwtgo.NewParser().ParseUnverified(tokenStr, jwtgo.MapClaims{})
	if err != nil {
		return nil, fmt.Errorf("failed to parse token: %v", err)
	}

	if err = t.Claims.Valid(); err != nil {
		return nil, fmt.Errorf("failed to validate token: %v", err)
	return verifyClaims(sets.NewString(append(r.allowedAudience, expectedAudience)...), t.Claims.(jwtgo.MapClaims))
	"crypto/rand"
	"crypto/rsa"
	"time"

	"github.com/golang-jwt/jwt/v4"
func newMockResourceServer(t testing.TB, publicKey rsa.PublicKey) (resourceServer ResourceServer, closer func()) {
				"issuer": "https://whatever.okta.com",
				"jwks_uri": "{URL}/keys",
			}`, "{URL}", *serverURL))
			key := jwk.NewRSAPublicKey()
			err := key.FromRaw(&publicKey)
			if err != nil {
				http.Error(w, err.Error(), 400)
				return
			}

			keys.Add(key)

			return
		BaseURL:         stdlibConfig.URL{URL: *config.MustParseURL(s.URL)},
		AllowedAudience: []string{"https://localhost"},
	return r, func() {
		s.Close()
	}
	sampleRSAKey, err := rsa.GenerateKey(rand.Reader, 2048)
	if !assert.NoError(t, err) {
		t.FailNow()
	}

	r, closer := newMockResourceServer(t, sampleRSAKey.PublicKey)
	defer closer()

	t.Run("No signature", func(t *testing.T) {
		sampleIDToken, err := jwt.NewWithClaims(jwt.SigningMethodRS512, jwt.RegisteredClaims{
			Audience:  r.allowedAudience,
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
			IssuedAt:  jwt.NewNumericDate(time.Now()),
			Issuer:    "localhost",
			Subject:   "someone",
		}).SignedString(sampleRSAKey)
		if !assert.NoError(t, err) {
			t.FailNow()
		}

		parts := strings.Split(sampleIDToken, ".")
		sampleIDToken = strings.Join(parts[:len(parts)-1], ".") + "."

		_, err = r.ValidateAccessToken(context.Background(), "myserver", sampleIDToken)
		if !assert.Error(t, err) {
			t.FailNow()
		}

		assert.Contains(t, err.Error(), "failed to verify id token signature")
	})

	t.Run("Invalid signature", func(t *testing.T) {
		sampleRSAKey, err := rsa.GenerateKey(rand.Reader, 2048)
		if !assert.NoError(t, err) {
			t.FailNow()
		}

		sampleIDToken, err := jwt.NewWithClaims(jwt.SigningMethodRS512, jwt.RegisteredClaims{
			Audience:  r.allowedAudience,
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
			IssuedAt:  jwt.NewNumericDate(time.Now()),
			Issuer:    "localhost",
			Subject:   "someone",
		}).SignedString(sampleRSAKey)
		if !assert.NoError(t, err) {
			t.FailNow()
		}

		_, err = r.ValidateAccessToken(context.Background(), "myserver", sampleIDToken)
		if !assert.Error(t, err) {
			t.FailNow()
		}

		assert.Contains(t, err.Error(), "failed to verify id token signature")
	})

	t.Run("Invalid audience", func(t *testing.T) {
		sampleIDToken, err := jwt.NewWithClaims(jwt.SigningMethodRS512, jwt.RegisteredClaims{
			Audience:  []string{"https://hello world"},
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
			IssuedAt:  jwt.NewNumericDate(time.Now()),
			Issuer:    "localhost",
			Subject:   "someone",
		}).SignedString(sampleRSAKey)
		if !assert.NoError(t, err) {
			t.FailNow()
		}

		_, err = r.ValidateAccessToken(context.Background(), "myserver", sampleIDToken)
		if !assert.Error(t, err) {
			t.FailNow()
		}

		assert.Contains(t, err.Error(), "invalid audience")
	})

	t.Run("Expired token", func(t *testing.T) {
		sampleIDToken, err := jwt.NewWithClaims(jwt.SigningMethodRS512, jwt.StandardClaims{
			Audience:  r.allowedAudience[0],
			ExpiresAt: time.Now().Add(-time.Hour).Unix(),
			IssuedAt:  time.Now().Add(-2 * time.Hour).Unix(),
			Issuer:    "localhost",
			Subject:   "someone",
		}).SignedString(sampleRSAKey)
		if !assert.NoError(t, err) {
			t.FailNow()
		}

		_, err = r.ValidateAccessToken(context.Background(), "myserver", sampleIDToken)
		if !assert.Error(t, err) {
			t.FailNow()
		}

		assert.Contains(t, err.Error(), "failed to validate token: Token is expired")
	})