Skip to content

Commit 60df632

Browse files
Merge pull request #294 from supertokens/testing/add-additional-tests
fix: Fix access token verification logic to use latest version if the JWT header has no version in it
2 parents 5f5d148 + f06b08a commit 60df632

File tree

5 files changed

+263
-3
lines changed

5 files changed

+263
-3
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [unreleased]
99

10+
## [0.12.7] - 2023-06-05
11+
12+
### Fixes
13+
14+
- Fixes an issue where session verification would fail when using JWTs created by the JWT recipe (and not the session recipe)
15+
1016
## [0.12.6] - 2023-06-01
1117

1218
### Fixes

recipe/session/exposeAccessTokenToFrontendInCookieBasedAuth_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,50 @@ func TestShouldAttachTokensAfterRefreshWhenDisabled(t *testing.T) {
481481
checkResponse(t, res2, false)
482482
}
483483

484+
func TestThatRefreshTokenIsNotSentInHeadersWhenUsingCookies(t *testing.T) {
485+
configValue := supertokens.TypeInput{
486+
Supertokens: &supertokens.ConnectionInfo{
487+
ConnectionURI: "http://localhost:8080",
488+
},
489+
AppInfo: supertokens.AppInfo{
490+
AppName: "SuperTokens",
491+
WebsiteDomain: "supertokens.io",
492+
APIDomain: "api.supertokens.io",
493+
},
494+
RecipeList: []supertokens.Recipe{
495+
Init(&sessmodels.TypeInput{
496+
ExposeAccessTokenToFrontendInCookieBasedAuth: true,
497+
GetTokenTransferMethod: func(req *http.Request, forCreateNewSession bool, userContext supertokens.UserContext) sessmodels.TokenTransferMethod {
498+
return sessmodels.CookieTransferMethod
499+
},
500+
}),
501+
},
502+
}
503+
BeforeEach()
504+
unittesting.StartUpST("localhost", "8080")
505+
defer AfterEach()
506+
err := supertokens.Init(configValue)
507+
if err != nil {
508+
t.Error(err.Error())
509+
}
510+
511+
testServer := GetTestServer(t)
512+
defer func() {
513+
testServer.Close()
514+
}()
515+
516+
req, err := http.NewRequest(http.MethodGet, testServer.URL+"/create", nil)
517+
assert.NoError(t, err)
518+
res, err := http.DefaultClient.Do(req)
519+
assert.NoError(t, err)
520+
assert.Equal(t, 200, res.StatusCode)
521+
522+
refreshTokenInHeader := res.Header.Get(refreshTokenHeaderKey)
523+
accessTokenInHeader := res.Header.Get(accessTokenHeaderKey)
524+
assert.NotEqual(t, accessTokenInHeader, "")
525+
assert.Equal(t, refreshTokenInHeader, "")
526+
}
527+
484528
func checkResponse(t *testing.T, res *http.Response, exposed bool) {
485529
info := unittesting.ExtractInfoFromResponse(res)
486530

recipe/session/jwt.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"encoding/base64"
2020
"encoding/json"
2121
"errors"
22+
"fmt"
2223
"github.com/golang-jwt/jwt/v4"
2324
"github.com/supertokens/supertokens-golang/recipe/session/sessmodels"
2425
"reflect"
@@ -42,6 +43,7 @@ func checkHeader(header string) error {
4243

4344
func ParseJWTWithoutSignatureVerification(token string) (sessmodels.ParsedJWTInfo, error) {
4445
splittedInput := strings.Split(token, ".")
46+
latestAccessTokenVersion := 3
4547
var kid *string
4648
if len(splittedInput) != 3 {
4749
errors.New("Invalid JWT")
@@ -65,7 +67,7 @@ func ParseJWTWithoutSignatureVerification(token string) (sessmodels.ParsedJWTInf
6567
versionInHeader, ok := parsedHeader["version"]
6668

6769
if !ok {
68-
return sessmodels.ParsedJWTInfo{}, errors.New("JWT header mismatch")
70+
versionInHeader = fmt.Sprint(latestAccessTokenVersion)
6971
}
7072

7173
if reflect.TypeOf(versionInHeader).Kind() != reflect.String {
@@ -87,7 +89,7 @@ func ParseJWTWithoutSignatureVerification(token string) (sessmodels.ParsedJWTInf
8789
kidString := kidInHeader.(string)
8890
kid = &kidString
8991

90-
if parsedHeader["typ"].(string) != "JWT" || parseError != nil || versionNumber < 3 || parsedHeader["kid"] == nil {
92+
if parsedHeader["typ"].(string) != "JWT" || parseError != nil || versionNumber < latestAccessTokenVersion || parsedHeader["kid"] == nil {
9193
return sessmodels.ParsedJWTInfo{}, errors.New("JWT header mismatch")
9294
}
9395

recipe/session/session_test.go

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"net/http"
2222
"net/http/httptest"
2323
"strings"
24+
"sync"
2425
"testing"
2526
"time"
2627

@@ -1811,6 +1812,213 @@ func TestThatTheSDKFetchesJWKSFromAllCoreHostsUntilAValidResponse(t *testing.T)
18111812
assert.True(t, strings.Contains(urlsAttemptedForJWKSFetch[1], "http://localhost:8080"))
18121813
}
18131814

1815+
func TestSessionVerificationOfJWTBasedOnSessionPayload(t *testing.T) {
1816+
configValue := supertokens.TypeInput{
1817+
Supertokens: &supertokens.ConnectionInfo{
1818+
ConnectionURI: "http://localhost:8080",
1819+
},
1820+
AppInfo: supertokens.AppInfo{
1821+
APIDomain: "api.supertokens.io",
1822+
AppName: "SuperTokens",
1823+
WebsiteDomain: "supertokens.io",
1824+
},
1825+
RecipeList: []supertokens.Recipe{
1826+
Init(nil),
1827+
},
1828+
}
1829+
BeforeEach()
1830+
unittesting.StartUpST("localhost", "8080")
1831+
defer AfterEach()
1832+
err := supertokens.Init(configValue)
1833+
if err != nil {
1834+
t.Error(err.Error())
1835+
}
1836+
1837+
session, err := CreateNewSessionWithoutRequestResponse("testing", map[string]interface{}{}, map[string]interface{}{}, nil)
1838+
if err != nil {
1839+
t.Error(err.Error())
1840+
}
1841+
1842+
payload := session.GetAccessTokenPayload()
1843+
delete(payload, "iat")
1844+
delete(payload, "exp")
1845+
1846+
currentTimeInSeconds := time.Now()
1847+
jwtExpiry := uint64((currentTimeInSeconds.Add(10 * time.Second)).Unix())
1848+
False := false
1849+
jwt, err := CreateJWT(payload, &jwtExpiry, &False)
1850+
if err != nil {
1851+
t.Error(err.Error())
1852+
}
1853+
1854+
session, err = GetSessionWithoutRequestResponse(jwt.OK.Jwt, nil, nil)
1855+
if err != nil {
1856+
t.Error(err.Error())
1857+
}
1858+
1859+
assert.Equal(t, session.GetUserID(), "testing")
1860+
}
1861+
1862+
func TestSessionVerificationOfJWTBasedOnSessionPayloadWithCheckDatabase(t *testing.T) {
1863+
configValue := supertokens.TypeInput{
1864+
Supertokens: &supertokens.ConnectionInfo{
1865+
ConnectionURI: "http://localhost:8080",
1866+
},
1867+
AppInfo: supertokens.AppInfo{
1868+
APIDomain: "api.supertokens.io",
1869+
AppName: "SuperTokens",
1870+
WebsiteDomain: "supertokens.io",
1871+
},
1872+
RecipeList: []supertokens.Recipe{
1873+
Init(nil),
1874+
},
1875+
}
1876+
BeforeEach()
1877+
unittesting.StartUpST("localhost", "8080")
1878+
defer AfterEach()
1879+
err := supertokens.Init(configValue)
1880+
if err != nil {
1881+
t.Error(err.Error())
1882+
}
1883+
1884+
session, err := CreateNewSessionWithoutRequestResponse("testing", map[string]interface{}{}, map[string]interface{}{}, nil)
1885+
if err != nil {
1886+
t.Error(err.Error())
1887+
}
1888+
1889+
payload := session.GetAccessTokenPayload()
1890+
delete(payload, "iat")
1891+
delete(payload, "exp")
1892+
payload["tId"] = "public"
1893+
1894+
currentTimeInSeconds := time.Now()
1895+
jwtExpiry := uint64((currentTimeInSeconds.Add(10 * time.Second)).Unix())
1896+
False := false
1897+
jwt, err := CreateJWT(payload, &jwtExpiry, &False)
1898+
if err != nil {
1899+
t.Error(err.Error())
1900+
}
1901+
1902+
True := true
1903+
session, err = GetSessionWithoutRequestResponse(jwt.OK.Jwt, nil, &sessmodels.VerifySessionOptions{
1904+
CheckDatabase: &True,
1905+
})
1906+
if err != nil {
1907+
t.Error(err.Error())
1908+
}
1909+
1910+
assert.Equal(t, session.GetUserID(), "testing")
1911+
}
1912+
1913+
func TestThatLockingForJWKSCacheWorksFine(t *testing.T) {
1914+
originalRefreshlimit := JWKRefreshRateLimit
1915+
originalCacheAge := JWKCacheMaxAgeInMs
1916+
1917+
JWKRefreshRateLimit = 100
1918+
JWKCacheMaxAgeInMs = 2000
1919+
1920+
configValue := supertokens.TypeInput{
1921+
Supertokens: &supertokens.ConnectionInfo{
1922+
ConnectionURI: "http://localhost:8080",
1923+
},
1924+
AppInfo: supertokens.AppInfo{
1925+
APIDomain: "api.supertokens.io",
1926+
AppName: "SuperTokens",
1927+
WebsiteDomain: "supertokens.io",
1928+
},
1929+
RecipeList: []supertokens.Recipe{
1930+
Init(nil),
1931+
},
1932+
}
1933+
BeforeEach()
1934+
unittesting.SetKeyValueInConfig("access_token_dynamic_signing_key_update_interval", "0.0014")
1935+
unittesting.StartUpST("localhost", "8080")
1936+
defer AfterEach()
1937+
err := supertokens.Init(configValue)
1938+
if err != nil {
1939+
t.Error(err.Error())
1940+
}
1941+
1942+
differentKeyFoundCount := 0
1943+
notReturnFromCacheCount := 0
1944+
keys := []string{}
1945+
shouldStop := false
1946+
1947+
jwks, err := GetCombinedJWKS()
1948+
if err != nil {
1949+
t.Error(err.Error())
1950+
}
1951+
1952+
for _, k := range jwks.KIDs() {
1953+
keys = append(keys, k)
1954+
}
1955+
1956+
go func() {
1957+
time.Sleep(11 * time.Second)
1958+
shouldStop = true
1959+
}()
1960+
1961+
threadCount := 10
1962+
var wg sync.WaitGroup
1963+
wg.Add(threadCount)
1964+
1965+
for i := 0; i < threadCount; i++ {
1966+
go jwksLockTestRoutine(t, &shouldStop, i, &wg, func(_keys []string) {
1967+
if returnedFromCache == false {
1968+
notReturnFromCacheCount++
1969+
}
1970+
1971+
newKeys := []string{}
1972+
1973+
for _, _k2 := range _keys {
1974+
if !supertokens.DoesSliceContainString(_k2, keys) {
1975+
newKeys = append(newKeys, _k2)
1976+
}
1977+
}
1978+
1979+
if len(newKeys) != 0 {
1980+
differentKeyFoundCount++
1981+
keys = _keys
1982+
}
1983+
})
1984+
}
1985+
1986+
wg.Wait()
1987+
1988+
// We test for both
1989+
// - The keys changing
1990+
// - The number of times the result is not returned from cache
1991+
//
1992+
// Because even if the keys change only twice it could still mean that the SDK's cache locking
1993+
// does not work correctly and that it tried to query the core more times than it should have
1994+
//
1995+
// Checking for both the key change count and the cache miss count verifies the locking behaviour properly
1996+
//
1997+
// With the signing key interval as 5 seconds, and the test making requests for 11 seconds
1998+
// You expect the keys to change twice
1999+
assert.Equal(t, differentKeyFoundCount, 2)
2000+
// With cache lifetime as 2 seconds, you expect the cache to miss 5 times
2001+
assert.Equal(t, notReturnFromCacheCount, 5)
2002+
2003+
JWKRefreshRateLimit = originalRefreshlimit
2004+
JWKCacheMaxAgeInMs = originalCacheAge
2005+
}
2006+
2007+
func jwksLockTestRoutine(t *testing.T, shouldStop *bool, index int, group *sync.WaitGroup, doPost func([]string)) {
2008+
jwks, err := GetCombinedJWKS()
2009+
if err != nil {
2010+
t.Error(err.Error())
2011+
}
2012+
2013+
doPost(jwks.KIDs())
2014+
time.Sleep(100 * time.Millisecond)
2015+
if *shouldStop == false {
2016+
jwksLockTestRoutine(t, shouldStop, index, group, doPost)
2017+
} else {
2018+
group.Done()
2019+
}
2020+
}
2021+
18142022
type MockResponseWriter struct{}
18152023

18162024
func (mw MockResponseWriter) Header() http.Header {

supertokens/constants.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const (
2121
)
2222

2323
// VERSION current version of the lib
24-
const VERSION = "0.12.6"
24+
const VERSION = "0.12.7"
2525

2626
var (
2727
cdiSupported = []string{"2.21"}

0 commit comments

Comments
 (0)