diff --git a/README.md b/README.md index 947b4361..9b7911a0 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,8 @@ The new middlewares (`clerk.WithSessionV2()` & `clerk.RequireSessionV2()`) also - clerk.WithLeeway() to set a custom leeway that gives some extra time to the token to accommodate for clock skew - clerk.WithJWTVerificationKey() to set the JWK to use for verifying tokens without the need to fetch or cache any JWKs at runtime - clerk.WithCustomClaims() to pass a type (e.g. struct), which will be populated with the token claims based on json tags. +- clerk.WithSatelliteDomain() to skip the JWT token's "iss" claim verification. +- clerk.WithProxyURL() to verify the JWT token's "iss" claim against the proxy url. For example @@ -110,6 +112,8 @@ clerk.WithSessionV2( clerk.WithAuthorizedParty("my-authorized-party"), clerk.WithLeeway(5 * time.Second), clerk.WithCustomClaims(&customClaims), + clerk.WithSatelliteDomain(true), + clerk.WithProxyURL("https://example.com/__clerk"), ) ``` diff --git a/clerk/middleware.go b/clerk/middleware.go index 2bb8f074..8bedb2ee 100644 --- a/clerk/middleware.go +++ b/clerk/middleware.go @@ -51,7 +51,7 @@ func isAuthV2Request(r *http.Request, client Client) (string, bool) { claims, err := client.DecodeToken(headerToken) if err == nil { - return headerToken, isValidIssuer(claims.Issuer) + return headerToken, newIssuer(claims.Issuer).IsValid() } // Verification from header token failed, try with token from cookie @@ -65,5 +65,5 @@ func isAuthV2Request(r *http.Request, client Client) (string, bool) { return "", false } - return cookieSession.Value, isValidIssuer(claims.Issuer) + return cookieSession.Value, newIssuer(claims.Issuer).IsValid() } diff --git a/clerk/tokens.go b/clerk/tokens.go index 7f238aa4..4ac2dbad 100644 --- a/clerk/tokens.go +++ b/clerk/tokens.go @@ -2,7 +2,6 @@ package clerk import ( "fmt" - "strings" "time" "github.com/go-jose/go-jose/v3" @@ -52,6 +51,8 @@ type verifyTokenOptions struct { leeway time.Duration jwk *jose.JSONWebKey customClaims interface{} + isSatellite bool + proxyURL string } // VerifyToken verifies the session jwt token. @@ -99,7 +100,11 @@ func (c *client) VerifyToken(token string, opts ...VerifyTokenOption) (*SessionC return nil, err } - if !isValidIssuer(claims.Issuer) { + issuer := newIssuer(claims.Issuer). + WithSatelliteDomain(options.isSatellite). + WithProxyURL(options.proxyURL) + + if !issuer.IsValid() { return nil, fmt.Errorf("invalid issuer %s", claims.Issuer) } @@ -131,7 +136,3 @@ func verifyTokenParseClaims(parsedToken *jwt.JSONWebToken, key interface{}, sess } return parsedToken.Claims(key, sessionClaims, options.customClaims) } - -func isValidIssuer(issuer string) bool { - return strings.HasPrefix(issuer, "https://clerk.") || strings.Contains(issuer, ".clerk.accounts") -} diff --git a/clerk/tokens_issuer.go b/clerk/tokens_issuer.go new file mode 100644 index 00000000..a6a96c3e --- /dev/null +++ b/clerk/tokens_issuer.go @@ -0,0 +1,38 @@ +package clerk + +import "strings" + +type issuer struct { + iss string + isSatellite bool + proxyURL string +} + +func newIssuer(iss string) *issuer { + return &issuer{ + iss: iss, + } +} + +func (iss *issuer) WithSatelliteDomain(isSatellite bool) *issuer { + iss.isSatellite = isSatellite + return iss +} + +func (iss *issuer) WithProxyURL(proxyURL string) *issuer { + iss.proxyURL = proxyURL + return iss +} + +func (iss *issuer) IsValid() bool { + if iss.isSatellite { + return true + } + + if iss.proxyURL != "" { + return iss.iss == iss.proxyURL + } + + return strings.HasPrefix(iss.iss, "https://clerk.") || + strings.Contains(iss.iss, ".clerk.accounts") +} diff --git a/clerk/tokens_options.go b/clerk/tokens_options.go index 136bb07e..c2692f6e 100644 --- a/clerk/tokens_options.go +++ b/clerk/tokens_options.go @@ -63,6 +63,20 @@ func WithCustomClaims(customClaims interface{}) VerifyTokenOption { } } +func WithSatelliteDomain(isSatellite bool) VerifyTokenOption { + return func(o *verifyTokenOptions) error { + o.isSatellite = isSatellite + return nil + } +} + +func WithProxyURL(proxyURL string) VerifyTokenOption { + return func(o *verifyTokenOptions) error { + o.proxyURL = proxyURL + return nil + } +} + func pemToJWK(key string) (*jose.JSONWebKey, error) { block, _ := pem.Decode([]byte(key)) if block == nil { diff --git a/clerk/tokens_options_test.go b/clerk/tokens_options_test.go index fb4bd2c6..66d91472 100644 --- a/clerk/tokens_options_test.go +++ b/clerk/tokens_options_test.go @@ -76,6 +76,28 @@ BQIDAQAB } } +func TestWithSatelliteDomain(t *testing.T) { + isSatellite := true + + opts := &verifyTokenOptions{} + err := WithSatelliteDomain(isSatellite)(opts) + + if assert.NoError(t, err) { + assert.Equal(t, isSatellite, opts.isSatellite) + } +} + +func TestWithProxyURL(t *testing.T) { + proxyURL := "url" + + opts := &verifyTokenOptions{} + err := WithProxyURL(proxyURL)(opts) + + if assert.NoError(t, err) { + assert.Equal(t, proxyURL, opts.proxyURL) + } +} + func arrayToMap(t *testing.T, input []string) map[string]struct{} { t.Helper() diff --git a/clerk/tokens_test.go b/clerk/tokens_test.go index d1005ddf..763529fa 100644 --- a/clerk/tokens_test.go +++ b/clerk/tokens_test.go @@ -152,6 +152,54 @@ func TestClient_VerifyToken_InvalidIssuer(t *testing.T) { } } +func TestClient_VerifyToken_IssuerSatelliteDomain(t *testing.T) { + c, _ := NewClient("token") + + token, pubKey := testGenerateTokenJWT(t, dummySessionClaims, "kid") + + client := c.(*client) + client.jwksCache.set(testBuildJWKS(t, pubKey, jose.RS256, "kid")) + + got, _ := c.VerifyToken(token, WithSatelliteDomain(true)) + if !reflect.DeepEqual(got, &dummySessionClaims) { + t.Errorf("Expected %+v, but got %+v", dummySessionClaims, got) + } +} + +func TestClient_VerifyToken_InvalidIssuerProxyURL(t *testing.T) { + c, _ := NewClient("token") + + claims := dummySessionClaims + claims.Issuer = "invalid" + + token, pubKey := testGenerateTokenJWT(t, claims, "kid") + + client := c.(*client) + client.jwksCache.set(testBuildJWKS(t, pubKey, jose.RS256, "kid")) + + _, err := c.VerifyToken(token, WithProxyURL("issuer")) + if err == nil { + t.Errorf("Expected error to be returned") + } +} + +func TestClient_VerifyToken_ValidIssuerProxyURL(t *testing.T) { + c, _ := NewClient("token") + + claims := dummySessionClaims + claims.Issuer = "issuer" + + token, pubKey := testGenerateTokenJWT(t, claims, "kid") + + client := c.(*client) + client.jwksCache.set(testBuildJWKS(t, pubKey, jose.RS256, "kid")) + + got, _ := c.VerifyToken(token, WithProxyURL("issuer")) + if !reflect.DeepEqual(got, &claims) { + t.Errorf("Expected %+v, but got %+v", claims, got) + } +} + func TestClient_VerifyToken_ExpiredToken(t *testing.T) { c, _ := NewClient("token")