diff --git a/CHANGELOG.md b/CHANGELOG.md index dd52ad7e..b3883481 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +## [0.18.0] - 2024-04-30 + +### Changes +- `session.CreateNewSession` now defaults to the value of the `st-auth-mode` header (if available) if the configured `config.GetTokenTransferMethod` returns `any`. +- Enable smooth switching between `useDynamicAccessTokenSigningKey` settings by allowing refresh calls to change the signing key type of a session. + +### Breaking changes +- Make session required during signout. ## Breaking change diff --git a/recipe/emailpassword/authFlow_test.go b/recipe/emailpassword/authFlow_test.go index 3dfe088d..b6eb6186 100644 --- a/recipe/emailpassword/authFlow_test.go +++ b/recipe/emailpassword/authFlow_test.go @@ -1495,7 +1495,7 @@ func TestDefaultSignoutRouteRevokesSession(t *testing.T) { assert.Equal(t, "", cookieData1["refreshTokenDomain"]) } -func TestCallingTheAPIwithoutSessionShouldReturnOk(t *testing.T) { +func TestCallingTheAPIwithoutSessionShouldReturnUnauthorized(t *testing.T) { configValue := supertokens.TypeInput{ Supertokens: &supertokens.ConnectionInfo{ ConnectionURI: "http://localhost:8080", @@ -1550,8 +1550,8 @@ func TestCallingTheAPIwithoutSessionShouldReturnOk(t *testing.T) { t.Error(err.Error()) } - assert.Equal(t, 200, res.StatusCode) - assert.Equal(t, "OK", data["status"]) + assert.Equal(t, http.StatusUnauthorized, res.StatusCode) + assert.Empty(t, data["status"]) assert.Nil(t, req.Header["Cookie"]) } diff --git a/recipe/emailpassword/authMode_test.go b/recipe/emailpassword/authMode_test.go index 17146498..10a7c4ea 100644 --- a/recipe/emailpassword/authMode_test.go +++ b/recipe/emailpassword/authMode_test.go @@ -224,15 +224,43 @@ func TestWithGetTokenTransferMethodProvidedCreateNewSessionWithShouldUseHeaderIf defer testServer.Close() setupRoutesForTest(t, mux) - resp := createNewSession(t, testServer.URL, nil, nil, nil, nil) + t.Run("no st-auth-mode", func(t *testing.T) { + resp := createNewSession(t, testServer.URL, nil, nil, nil, nil) + + assert.Equal(t, resp["sAccessToken"], "-not-present-") + assert.Equal(t, resp["sRefreshToken"], "-not-present-") + assert.Equal(t, resp["antiCsrf"], "-not-present-") + assert.NotEmpty(t, resp["accessTokenFromHeader"]) + assert.NotEqual(t, resp["accessTokenFromHeader"], "-not-present-") + assert.NotEmpty(t, resp["refreshTokenFromHeader"]) + assert.NotEqual(t, resp["refreshTokenFromHeader"], "-not-present-") + }) - assert.Equal(t, resp["sAccessToken"], "-not-present-") - assert.Equal(t, resp["sRefreshToken"], "-not-present-") - assert.Equal(t, resp["antiCsrf"], "-not-present-") - assert.NotEmpty(t, resp["accessTokenFromHeader"]) - assert.NotEqual(t, resp["accessTokenFromHeader"], "-not-present-") - assert.NotEmpty(t, resp["refreshTokenFromHeader"]) - assert.NotEqual(t, resp["refreshTokenFromHeader"], "-not-present-") + t.Run("st-auth-mode is cookie", func(t *testing.T) { + authMode := string(sessmodels.CookieTransferMethod) + resp := createNewSession(t, testServer.URL, &authMode, nil, nil, nil) + + assert.NotEqual(t, resp["sAccessToken"], "-not-present-") + assert.NotEqual(t, resp["sRefreshToken"], "-not-present-") + assert.NotEqual(t, resp["antiCsrf"], "-not-present-") + assert.NotEmpty(t, resp["accessTokenFromHeader"]) + assert.Equal(t, resp["accessTokenFromHeader"], "-not-present-") + assert.NotEmpty(t, resp["refreshTokenFromHeader"]) + assert.Equal(t, resp["refreshTokenFromHeader"], "-not-present-") + }) + + t.Run("st-auth-mode is header", func(t *testing.T) { + authMode := string(sessmodels.HeaderTransferMethod) + resp := createNewSession(t, testServer.URL, &authMode, nil, nil, nil) + + assert.Equal(t, resp["sAccessToken"], "-not-present-") + assert.Equal(t, resp["sRefreshToken"], "-not-present-") + assert.Equal(t, resp["antiCsrf"], "-not-present-") + assert.NotEmpty(t, resp["accessTokenFromHeader"]) + assert.NotEqual(t, resp["accessTokenFromHeader"], "-not-present-") + assert.NotEmpty(t, resp["refreshTokenFromHeader"]) + assert.NotEqual(t, resp["refreshTokenFromHeader"], "-not-present-") + }) } func TestWithGetTokenTransferMethodProvidedCreateNewSessionWithShouldUseHeaderIfMethodReturnsHeader(t *testing.T) { diff --git a/recipe/session/recipeImplementation.go b/recipe/session/recipeImplementation.go index ca8b9b0e..c6b2a464 100644 --- a/recipe/session/recipeImplementation.go +++ b/recipe/session/recipeImplementation.go @@ -294,7 +294,7 @@ func MakeRecipeImplementation(querier supertokens.Querier, config sessmodels.Typ supertokens.LogDebugMessage("refreshSession: Started") - response, err := refreshSessionHelper(config, querier, refreshToken, antiCsrfToken, disableAntiCsrf, userContext) + response, err := refreshSessionHelper(config, querier, refreshToken, antiCsrfToken, disableAntiCsrf, config.UseDynamicAccessTokenSigningKey, userContext) if err != nil { return nil, err } diff --git a/recipe/session/sessionFunctions.go b/recipe/session/sessionFunctions.go index 43b0f858..fb7be614 100644 --- a/recipe/session/sessionFunctions.go +++ b/recipe/session/sessionFunctions.go @@ -224,10 +224,11 @@ func getSessionInformationHelper(querier supertokens.Querier, sessionHandle stri return nil, nil } -func refreshSessionHelper(config sessmodels.TypeNormalisedInput, querier supertokens.Querier, refreshToken string, antiCsrfToken *string, disableAntiCsrf bool, userContext supertokens.UserContext) (sessmodels.CreateOrRefreshAPIResponse, error) { +func refreshSessionHelper(config sessmodels.TypeNormalisedInput, querier supertokens.Querier, refreshToken string, antiCsrfToken *string, disableAntiCsrf bool, useDynamicAccessTokenSigningKey bool, userContext supertokens.UserContext) (sessmodels.CreateOrRefreshAPIResponse, error) { requestBody := map[string]interface{}{ - "refreshToken": refreshToken, - "enableAntiCsrf": !disableAntiCsrf && config.AntiCsrfFunctionOrString.StrValue == AntiCSRF_VIA_TOKEN, + "refreshToken": refreshToken, + "enableAntiCsrf": !disableAntiCsrf && config.AntiCsrfFunctionOrString.StrValue == AntiCSRF_VIA_TOKEN, + "useDynamicSigningKey": useDynamicAccessTokenSigningKey, } if antiCsrfToken != nil { requestBody["antiCsrfToken"] = *antiCsrfToken diff --git a/recipe/session/sessionHandlingFuncsWithoutReq_test.go b/recipe/session/sessionHandlingFuncsWithoutReq_test.go index e5085137..e27cfb67 100644 --- a/recipe/session/sessionHandlingFuncsWithoutReq_test.go +++ b/recipe/session/sessionHandlingFuncsWithoutReq_test.go @@ -2,6 +2,7 @@ package session import ( "errors" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -472,3 +473,87 @@ func TestRefreshShouldReturnErrorForNonTokens(t *testing.T) { assert.NotNil(t, err2) assert.True(t, errors.As(err2, &sessionError.UnauthorizedError{})) } + +func TestUseDynamicAccessTokenSigningKey(t *testing.T) { + useDynamicAccessTokenSigningKey := true + configValue := supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ + ConnectionURI: "http://localhost:8080", + }, + AppInfo: supertokens.AppInfo{ + AppName: "SuperTokens", + WebsiteDomain: "supertokens.io", + APIDomain: "api.supertokens.io", + }, + RecipeList: []supertokens.Recipe{ + Init(&sessmodels.TypeInput{ + UseDynamicAccessTokenSigningKey: &useDynamicAccessTokenSigningKey, + }), + }, + } + BeforeEach() + unittesting.StartUpST("localhost", "8080") + defer AfterEach() + + checkAccessTokenSigningKeyType := func(t *testing.T, tokens sessmodels.SessionTokens, isDynamic bool) { + t.Helper() + + info, err := ParseJWTWithoutSignatureVerification(tokens.AccessToken) + assert.NoError(t, err) + + if isDynamic { + assert.True(t, strings.HasPrefix(*info.KID, "d-")) + } else { + assert.True(t, strings.HasPrefix(*info.KID, "s-")) + } + } + + err := supertokens.Init(configValue) + assert.NoError(t, err) + + res, err := CreateNewSessionWithoutRequestResponse("public", "test-user-id", map[string]interface{}{ + "tokenProp": true, + }, map[string]interface{}{ + "dbProp": true, + }, nil) + + assert.NoError(t, err) + + tokens := res.GetAllSessionTokensDangerously() + checkAccessTokenSigningKeyType(t, tokens, true) + + resetAll() + + // here we change to false + useDynamicAccessTokenSigningKey = false + err = supertokens.Init(configValue) + assert.NoError(t, err) + + t.Run("should throw when verifying", func(t *testing.T) { + _, err = GetSessionWithoutRequestResponse(tokens.AccessToken, tokens.AntiCsrfToken, nil) + assert.Equal(t, err.Error(), "The access token doesn't match the useDynamicAccessTokenSigningKey setting") + }) + + t.Run("should work after refresh", func(t *testing.T) { + disableAntiCsrf := true + refreshedSession, err := RefreshSessionWithoutRequestResponse(*tokens.RefreshToken, &disableAntiCsrf, tokens.AntiCsrfToken) + assert.NoError(t, err) + + tokensAfterRefresh := refreshedSession.GetAllSessionTokensDangerously() + assert.True(t, tokensAfterRefresh.AccessAndFrontendTokenUpdated) + checkAccessTokenSigningKeyType(t, tokensAfterRefresh, false) + + verifiedSession, err := GetSessionWithoutRequestResponse(tokensAfterRefresh.AccessToken, tokensAfterRefresh.AntiCsrfToken, nil) + assert.NoError(t, err) + + tokensAfterVerify := verifiedSession.GetAllSessionTokensDangerously() + assert.True(t, tokensAfterVerify.AccessAndFrontendTokenUpdated) + checkAccessTokenSigningKeyType(t, tokensAfterVerify, false) + + verifiedSession2, err := GetSessionWithoutRequestResponse(tokensAfterVerify.AccessToken, tokensAfterVerify.AntiCsrfToken, nil) + assert.NoError(t, err) + + tokensAfterVerify2 := verifiedSession2.GetAllSessionTokensDangerously() + assert.False(t, tokensAfterVerify2.AccessAndFrontendTokenUpdated) + }) +} diff --git a/recipe/session/sessionRequestFunctions.go b/recipe/session/sessionRequestFunctions.go index c542a804..be796f27 100644 --- a/recipe/session/sessionRequestFunctions.go +++ b/recipe/session/sessionRequestFunctions.go @@ -60,7 +60,12 @@ func CreateNewSessionInRequest(req *http.Request, res http.ResponseWriter, tenan outputTokenTransferMethod := config.GetTokenTransferMethod(req, true, userContext) if outputTokenTransferMethod == sessmodels.AnyTransferMethod { - outputTokenTransferMethod = sessmodels.HeaderTransferMethod + authMode := GetAuthmodeFromHeader(req) + if authMode != nil && *authMode == sessmodels.CookieTransferMethod { + outputTokenTransferMethod = *authMode + } else { + outputTokenTransferMethod = sessmodels.HeaderTransferMethod + } } supertokens.LogDebugMessage(fmt.Sprintf("createNewSession: using transfer method %s", outputTokenTransferMethod)) diff --git a/recipe/session/signout.go b/recipe/session/signout.go index 88eefa4d..22178f90 100644 --- a/recipe/session/signout.go +++ b/recipe/session/signout.go @@ -27,9 +27,9 @@ func SignOutAPI(apiImplementation sessmodels.APIInterface, options sessmodels.AP return nil } - False := false + sessionRequired := true sessionContainer, err := GetSessionFromRequest(options.Req, options.Res, options.Config, &sessmodels.VerifySessionOptions{ - SessionRequired: &False, + SessionRequired: &sessionRequired, OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { return []claims.SessionClaimValidator{}, nil }, diff --git a/recipe/thirdparty/signoutFeature_test.go b/recipe/thirdparty/signoutFeature_test.go index f1355a3b..b14b6aba 100644 --- a/recipe/thirdparty/signoutFeature_test.go +++ b/recipe/thirdparty/signoutFeature_test.go @@ -35,7 +35,7 @@ import ( "gopkg.in/h2non/gock.v1" ) -func TestThatCallingTheAPIwithoutASessionShouldReturnOk(t *testing.T) { +func TestThatCallingTheAPIwithoutASessionShouldReturnUnauthorized(t *testing.T) { configValue := supertokens.TypeInput{ Supertokens: &supertokens.ConnectionInfo{ ConnectionURI: "http://localhost:8080", @@ -80,7 +80,7 @@ func TestThatCallingTheAPIwithoutASessionShouldReturnOk(t *testing.T) { if err != nil { t.Error(err.Error()) } - assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) dataInBytes, err := ioutil.ReadAll(resp.Body) if err != nil { @@ -94,7 +94,7 @@ func TestThatCallingTheAPIwithoutASessionShouldReturnOk(t *testing.T) { t.Error(err.Error()) } - assert.Equal(t, "OK", response["status"]) + assert.Empty(t, response["status"]) assert.Equal(t, 0, len(resp.Cookies())) } diff --git a/supertokens/constants.go b/supertokens/constants.go index 200d0d49..e3db5743 100644 --- a/supertokens/constants.go +++ b/supertokens/constants.go @@ -21,7 +21,7 @@ const ( ) // VERSION current version of the lib -const VERSION = "0.17.5" +const VERSION = "0.18.0" var ( cdiSupported = []string{"3.0"}