Skip to content

Commit

Permalink
feat: Modify Issuer verification for all possible scenarios
Browse files Browse the repository at this point in the history
The js sdk [1] supports overriding the expected issuer by allowing the
sdk user to set a couple of options.

This makes the issuer verification possible for a couple of non-standard
ways, namely when the customer's fapi sits behind a proxy or when the
domain is not the primary one.

[1] https://github.com/clerkinc/javascript/blob/c7c6912f34874467bc74104690fe9f95491cc10d/packages/backend/src/tokens/interstitialRule.ts#L158-L170

Fix USR-388
  • Loading branch information
fragoulis committed Oct 9, 2023
1 parent 4f8531e commit d9e4be0
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 8 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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"),
)
```

Expand Down
4 changes: 2 additions & 2 deletions clerk/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
}
13 changes: 7 additions & 6 deletions clerk/tokens.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package clerk

import (
"fmt"
"strings"
"time"

"github.com/go-jose/go-jose/v3"
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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")
}
38 changes: 38 additions & 0 deletions clerk/tokens_issuer.go
Original file line number Diff line number Diff line change
@@ -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")
}
14 changes: 14 additions & 0 deletions clerk/tokens_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
22 changes: 22 additions & 0 deletions clerk/tokens_options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
48 changes: 48 additions & 0 deletions clerk/tokens_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down

0 comments on commit d9e4be0

Please sign in to comment.