From 8fc0802111a165f0905f8bd8c53e023759c9b267 Mon Sep 17 00:00:00 2001 From: Thomas Conner Date: Mon, 17 Jun 2024 11:09:08 -0400 Subject: [PATCH 1/7] fix: make refresh token optional for a session --- src/GoTrueClient.ts | 42 ++++++++++++++++++++++++++++++------------ src/lib/types.ts | 2 +- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/GoTrueClient.ts b/src/GoTrueClient.ts index 00958fd65..b18ddeeb4 100644 --- a/src/GoTrueClient.ts +++ b/src/GoTrueClient.ts @@ -1111,7 +1111,16 @@ export default class GoTrueClient { return { data: { session: currentSession }, error: null } } + if (!currentSession.refresh_token) { + this._debug( + '#__loadSession()', + 'session has no refresh token to refresh session' + ) + return { data: { session: null }, error: null }; + } + const { session, error } = await this._callRefreshToken(currentSession.refresh_token) + if (error) { return { data: { session: null }, error } } @@ -1258,13 +1267,14 @@ export default class GoTrueClient { } /** - * Sets the session data from the current session. If the current session is expired, setSession will take care of refreshing it to obtain a new session. + * Sets the session data from the current session. If the current session is expired, + * setSession will take care of refreshing it to obtain a new session when a refresh token is provided and autoRefreshToken is not disabled. * If the refresh token or access token in the current session is invalid, an error will be thrown. - * @param currentSession The current session that minimally contains an access token and refresh token. + * @param currentSession The current session that minimally contains an access token. */ async setSession(currentSession: { access_token: string - refresh_token: string + refresh_token?: string }): Promise { await this.initializePromise @@ -1275,10 +1285,10 @@ export default class GoTrueClient { protected async _setSession(currentSession: { access_token: string - refresh_token: string + refresh_token?: string }): Promise { try { - if (!currentSession.access_token || !currentSession.refresh_token) { + if (!currentSession.access_token) { throw new AuthSessionMissingError() } @@ -1293,6 +1303,10 @@ export default class GoTrueClient { } if (hasExpired) { + if (!this.autoRefreshToken || !currentSession.refresh_token) { + return { data: { user: null, session: null }, error: null } + } + const { session: refreshedSession, error } = await this._callRefreshToken( currentSession.refresh_token ) @@ -1303,12 +1317,15 @@ export default class GoTrueClient { if (!refreshedSession) { return { data: { user: null, session: null }, error: null } } + session = refreshedSession } else { const { data, error } = await this._getUser(currentSession.access_token) + if (error) { throw error } + session = { access_token: currentSession.access_token, refresh_token: currentSession.refresh_token, @@ -1337,11 +1354,11 @@ export default class GoTrueClient { * If the current session's refresh token is invalid, an error will be thrown. * @param currentSession The current session. If passed in, it must contain a refresh token. */ - async refreshSession(currentSession?: { refresh_token: string }): Promise { + async refreshSession(session?: { refresh_token: string }): Promise { await this.initializePromise return await this._acquireLock(-1, async () => { - return await this._refreshSession(currentSession) + return await this._refreshSession(session) }) } @@ -1350,20 +1367,22 @@ export default class GoTrueClient { }): Promise { try { return await this._useSession(async (result) => { - if (!currentSession) { + let oldSession: Pick | null = currentSession ? { ...currentSession } : null; + + if (!oldSession) { const { data, error } = result if (error) { throw error } - currentSession = data.session ?? undefined + oldSession = data.session; } - if (!currentSession?.refresh_token) { + if (!oldSession?.refresh_token) { throw new AuthSessionMissingError() } - const { session, error } = await this._callRefreshToken(currentSession.refresh_token) + const { session, error } = await this._callRefreshToken(oldSession.refresh_token) if (error) { return { data: { user: null, session: null }, error: error } } @@ -1822,7 +1841,6 @@ export default class GoTrueClient { typeof maybeSession === 'object' && maybeSession !== null && 'access_token' in maybeSession && - 'refresh_token' in maybeSession && 'expires_at' in maybeSession return isValidSession diff --git a/src/lib/types.ts b/src/lib/types.ts index 708a5de92..0acaac86b 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -241,7 +241,7 @@ export interface Session { /** * A one-time used refresh token that never expires. */ - refresh_token: string + refresh_token?: string /** * The number of seconds until the token expires (since it was issued). Returned when a login is confirmed. */ From 804fcbaf12eb3f26042df43790db74105477eb2b Mon Sep 17 00:00:00 2001 From: Thomas Conner Date: Mon, 17 Jun 2024 11:09:30 -0400 Subject: [PATCH 2/7] fix: update tests to check for no refresh token provided to setSession() --- test/GoTrueClient.test.ts | 48 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/test/GoTrueClient.test.ts b/test/GoTrueClient.test.ts index 6378ae1c4..21ea0c47d 100644 --- a/test/GoTrueClient.test.ts +++ b/test/GoTrueClient.test.ts @@ -139,6 +139,50 @@ describe('GoTrueClient', () => { expect(user?.user_metadata).toMatchObject({ hello: 'world' }) }) + test('setSession should return no error without a passed-in refresh token', async () => { + const { email, password } = mockUserCredentials() + + const { error, data } = await authWithSession.signUp({ + email, + password, + }) + expect(error).toBeNull() + expect(data.session).not.toBeNull() + + const { + data: { session }, + error: setSessionError, + } = await authWithSession.setSession({ + // @ts-expect-error 'data.session should not be null because of the assertion above' + access_token: data.session.access_token + }) + expect(setSessionError).toBeNull() + expect(session).not.toBeNull() + expect(session!.user).not.toBeNull() + expect(session!.expires_in).not.toBeNull() + expect(session!.expires_at).not.toBeNull() + expect(session!.access_token).not.toBeNull() + expect(session!.refresh_token).not.toBeNull() + expect(session!.token_type).toStrictEqual('bearer') + + /** + * getSession has been added to verify setSession is also saving + * the session, not just returning it. + */ + const { data: getSessionData, error: getSessionError } = await authWithSession.getSession() + expect(getSessionError).toBeNull() + expect(getSessionData).not.toBeNull() + + const { + data: { user }, + error: updateError, + } = await authWithSession.updateUser({ data: { hello: 'world' } }) + + expect(updateError).toBeNull() + expect(user).not.toBeNull() + expect(user?.user_metadata).toMatchObject({ hello: 'world' }) + }) + test('getSession() should return the currentUser session', async () => { const { email, password } = mockUserCredentials() @@ -265,7 +309,7 @@ describe('GoTrueClient', () => { // verify the deferred has been reset and successive calls can be made // @ts-expect-error 'Allow access to private _callRefreshToken()' const { session: session3, error: error3 } = await authWithSession._callRefreshToken( - data.session!.refresh_token + data.session!.refresh_token! ) expect(error3).toBeNull() @@ -307,7 +351,7 @@ describe('GoTrueClient', () => { // vreify the deferred has been reset and successive calls can be made // @ts-expect-error 'Allow access to private _callRefreshToken()' const { session: session3, error: error3 } = await authWithSession._callRefreshToken( - data.session!.refresh_token + data.session!.refresh_token! ) expect(error3).toBeNull() From 119bc6699fdafb549701672f163288417e894f10 Mon Sep 17 00:00:00 2001 From: Thomas Conner Date: Tue, 18 Jun 2024 10:20:14 -0400 Subject: [PATCH 3/7] fix: make refresh_token required --- src/GoTrueClient.ts | 24 ++++++-------------- src/lib/types.ts | 2 +- test/GoTrueClient.test.ts | 48 ++------------------------------------- 3 files changed, 10 insertions(+), 64 deletions(-) diff --git a/src/GoTrueClient.ts b/src/GoTrueClient.ts index b18ddeeb4..802825cac 100644 --- a/src/GoTrueClient.ts +++ b/src/GoTrueClient.ts @@ -1111,14 +1111,6 @@ export default class GoTrueClient { return { data: { session: currentSession }, error: null } } - if (!currentSession.refresh_token) { - this._debug( - '#__loadSession()', - 'session has no refresh token to refresh session' - ) - return { data: { session: null }, error: null }; - } - const { session, error } = await this._callRefreshToken(currentSession.refresh_token) if (error) { @@ -1274,7 +1266,7 @@ export default class GoTrueClient { */ async setSession(currentSession: { access_token: string - refresh_token?: string + refresh_token: string }): Promise { await this.initializePromise @@ -1285,7 +1277,7 @@ export default class GoTrueClient { protected async _setSession(currentSession: { access_token: string - refresh_token?: string + refresh_token: string }): Promise { try { if (!currentSession.access_token) { @@ -1303,7 +1295,7 @@ export default class GoTrueClient { } if (hasExpired) { - if (!this.autoRefreshToken || !currentSession.refresh_token) { + if (!this.autoRefreshToken) { return { data: { user: null, session: null }, error: null } } @@ -1367,22 +1359,20 @@ export default class GoTrueClient { }): Promise { try { return await this._useSession(async (result) => { - let oldSession: Pick | null = currentSession ? { ...currentSession } : null; - - if (!oldSession) { + if (!currentSession) { const { data, error } = result if (error) { throw error } - oldSession = data.session; + currentSession = data.session ?? undefined } - if (!oldSession?.refresh_token) { + if (!currentSession?.refresh_token) { throw new AuthSessionMissingError() } - const { session, error } = await this._callRefreshToken(oldSession.refresh_token) + const { session, error } = await this._callRefreshToken(currentSession.refresh_token) if (error) { return { data: { user: null, session: null }, error: error } } diff --git a/src/lib/types.ts b/src/lib/types.ts index 0acaac86b..708a5de92 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -241,7 +241,7 @@ export interface Session { /** * A one-time used refresh token that never expires. */ - refresh_token?: string + refresh_token: string /** * The number of seconds until the token expires (since it was issued). Returned when a login is confirmed. */ diff --git a/test/GoTrueClient.test.ts b/test/GoTrueClient.test.ts index 21ea0c47d..6378ae1c4 100644 --- a/test/GoTrueClient.test.ts +++ b/test/GoTrueClient.test.ts @@ -139,50 +139,6 @@ describe('GoTrueClient', () => { expect(user?.user_metadata).toMatchObject({ hello: 'world' }) }) - test('setSession should return no error without a passed-in refresh token', async () => { - const { email, password } = mockUserCredentials() - - const { error, data } = await authWithSession.signUp({ - email, - password, - }) - expect(error).toBeNull() - expect(data.session).not.toBeNull() - - const { - data: { session }, - error: setSessionError, - } = await authWithSession.setSession({ - // @ts-expect-error 'data.session should not be null because of the assertion above' - access_token: data.session.access_token - }) - expect(setSessionError).toBeNull() - expect(session).not.toBeNull() - expect(session!.user).not.toBeNull() - expect(session!.expires_in).not.toBeNull() - expect(session!.expires_at).not.toBeNull() - expect(session!.access_token).not.toBeNull() - expect(session!.refresh_token).not.toBeNull() - expect(session!.token_type).toStrictEqual('bearer') - - /** - * getSession has been added to verify setSession is also saving - * the session, not just returning it. - */ - const { data: getSessionData, error: getSessionError } = await authWithSession.getSession() - expect(getSessionError).toBeNull() - expect(getSessionData).not.toBeNull() - - const { - data: { user }, - error: updateError, - } = await authWithSession.updateUser({ data: { hello: 'world' } }) - - expect(updateError).toBeNull() - expect(user).not.toBeNull() - expect(user?.user_metadata).toMatchObject({ hello: 'world' }) - }) - test('getSession() should return the currentUser session', async () => { const { email, password } = mockUserCredentials() @@ -309,7 +265,7 @@ describe('GoTrueClient', () => { // verify the deferred has been reset and successive calls can be made // @ts-expect-error 'Allow access to private _callRefreshToken()' const { session: session3, error: error3 } = await authWithSession._callRefreshToken( - data.session!.refresh_token! + data.session!.refresh_token ) expect(error3).toBeNull() @@ -351,7 +307,7 @@ describe('GoTrueClient', () => { // vreify the deferred has been reset and successive calls can be made // @ts-expect-error 'Allow access to private _callRefreshToken()' const { session: session3, error: error3 } = await authWithSession._callRefreshToken( - data.session!.refresh_token! + data.session!.refresh_token ) expect(error3).toBeNull() From 78bf103da81c738b323e5a9e11857564528717cc Mon Sep 17 00:00:00 2001 From: Thomas Conner Date: Tue, 18 Jun 2024 10:21:27 -0400 Subject: [PATCH 4/7] fix: check that refresh_token is provided --- src/GoTrueClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GoTrueClient.ts b/src/GoTrueClient.ts index 802825cac..b628bb782 100644 --- a/src/GoTrueClient.ts +++ b/src/GoTrueClient.ts @@ -1280,7 +1280,7 @@ export default class GoTrueClient { refresh_token: string }): Promise { try { - if (!currentSession.access_token) { + if (!currentSession.access_token || !currentSession.refresh_token) { throw new AuthSessionMissingError() } From ede3deafacb18468a2d96991cd1e98fa69cdf139 Mon Sep 17 00:00:00 2001 From: Thomas Conner Date: Tue, 18 Jun 2024 10:22:13 -0400 Subject: [PATCH 5/7] fix: restore comment --- src/GoTrueClient.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/GoTrueClient.ts b/src/GoTrueClient.ts index b628bb782..28a4e3a63 100644 --- a/src/GoTrueClient.ts +++ b/src/GoTrueClient.ts @@ -1259,10 +1259,9 @@ export default class GoTrueClient { } /** - * Sets the session data from the current session. If the current session is expired, - * setSession will take care of refreshing it to obtain a new session when a refresh token is provided and autoRefreshToken is not disabled. + * Sets the session data from the current session. If the current session is expired, setSession will take care of refreshing it to obtain a new session. * If the refresh token or access token in the current session is invalid, an error will be thrown. - * @param currentSession The current session that minimally contains an access token. + * @param currentSession The current session that minimally contains an access token and refresh token. */ async setSession(currentSession: { access_token: string From 5242b8f6a9472573f72266d5a0cc0b2f61811337 Mon Sep 17 00:00:00 2001 From: Thomas Conner Date: Tue, 18 Jun 2024 10:22:59 -0400 Subject: [PATCH 6/7] fix: rename param to currentSession --- src/GoTrueClient.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/GoTrueClient.ts b/src/GoTrueClient.ts index 28a4e3a63..96112bd84 100644 --- a/src/GoTrueClient.ts +++ b/src/GoTrueClient.ts @@ -1345,11 +1345,11 @@ export default class GoTrueClient { * If the current session's refresh token is invalid, an error will be thrown. * @param currentSession The current session. If passed in, it must contain a refresh token. */ - async refreshSession(session?: { refresh_token: string }): Promise { + async refreshSession(currentSession?: { refresh_token: string }): Promise { await this.initializePromise return await this._acquireLock(-1, async () => { - return await this._refreshSession(session) + return await this._refreshSession(currentSession) }) } From 5f79e53dcd277b91084965d0fd735471742ded09 Mon Sep 17 00:00:00 2001 From: Thomas Conner Date: Tue, 18 Jun 2024 10:23:45 -0400 Subject: [PATCH 7/7] fix: check that valid session has refresh_token --- src/GoTrueClient.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/GoTrueClient.ts b/src/GoTrueClient.ts index 96112bd84..be01fd94d 100644 --- a/src/GoTrueClient.ts +++ b/src/GoTrueClient.ts @@ -1830,6 +1830,7 @@ export default class GoTrueClient { typeof maybeSession === 'object' && maybeSession !== null && 'access_token' in maybeSession && + 'refresh_token' in maybeSession && 'expires_at' in maybeSession return isValidSession