Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: new implementation of sessionCookieStore #7

Merged
merged 7 commits into from
May 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 0 additions & 66 deletions src/session/app-session-cookie-store.ts

This file was deleted.

5 changes: 5 additions & 0 deletions src/session/authCookieName.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { AuthHandlerParams } from '../storyblok-auth-api'

const defaultCookieName = 'sb.auth'
export const authCookieName = (params: Pick<AuthHandlerParams, 'cookieName'>) =>
params.cookieName ?? defaultCookieName
27 changes: 27 additions & 0 deletions src/session/crud/getAllSessions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { AuthHandlerParams } from '../../storyblok-auth-api'
import { AppSession } from '../types'
import { getSignedCookie, GetCookie } from '../../utils'
import { authCookieName } from '../authCookieName'

export type AppSessionCookiePayload =
| {
sessions: AppSession[]
}
| undefined
export type GetAllSessionsParams = Pick<
AuthHandlerParams,
'clientSecret' | 'cookieName' | 'clientId'
>
export type GetAllSessions = (
params: GetAllSessionsParams,
getCookie: GetCookie,
) => AppSession[]
export const getAllSessions: GetAllSessions = (params, getCookie) => {
const signedCookie = getSignedCookie(
params.clientSecret,
getCookie,
authCookieName(params),
) as AppSessionCookiePayload
// TODO validate at runtime
return signedCookie?.sessions ?? []
}
24 changes: 24 additions & 0 deletions src/session/crud/getSession.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { AuthHandlerParams } from '../../storyblok-auth-api'
import { AppSession, AppSessionQuery } from '../types'
import { getAllSessions } from './getAllSessions'
import { keysEquals, keysFromQuery } from './utils'
import { GetCookie } from '../../utils'

export type GetSessionParams = Pick<
AuthHandlerParams,
'clientSecret' | 'cookieName' | 'clientId'
>

export type GetSession = (
params: GetSessionParams,
getCookie: GetCookie,
query: AppSessionQuery,
) => AppSession | undefined
export const getSession: GetSession = (params, getCookie, query) => {
const keys = {
...keysFromQuery(query),
appClientId: params.clientId,
}
const areSessionsEqual = keysEquals(keys)
return getAllSessions(params, getCookie).find(areSessionsEqual)
}
4 changes: 4 additions & 0 deletions src/session/crud/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './getAllSessions'
export * from './getSession'
export * from './putSession'
export * from './removeSession'
31 changes: 31 additions & 0 deletions src/session/crud/putSession.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { AppSession } from '../types'
import { AuthHandlerParams } from '../../storyblok-auth-api'
import { setAllSessions } from './setAllSessions'
import { getAllSessions } from './getAllSessions'
import { keysEquals } from './utils'
import { GetCookie, SetCookie } from '../../utils'

export type PutSessionParams = Pick<
AuthHandlerParams,
'clientSecret' | 'cookieName' | 'clientId'
>

export type PutSession = (
params: PutSessionParams,
getCookie: GetCookie,
setCookie: SetCookie,
session: AppSession,
) => AppSession

export const putSession: PutSession = (
params,
getCookie,
setCookie,
newSession,
) => {
const isNotEqual = (otherSession: AppSession) =>
!keysEquals(newSession)(otherSession)
const otherSessions = getAllSessions(params, getCookie).filter(isNotEqual)
setAllSessions(params, setCookie, [...otherSessions, newSession])
return newSession
}
35 changes: 35 additions & 0 deletions src/session/crud/removeSession.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { AppSession, AppSessionQuery } from '../types'
import { AuthHandlerParams } from '../../storyblok-auth-api'
import { setAllSessions } from './setAllSessions'
import { getAllSessions } from './getAllSessions'
import { keysEquals, keysFromQuery } from './utils'
import { SetCookie, GetCookie } from '../../utils'

export type RemoveSessionParams = Pick<
AuthHandlerParams,
'clientSecret' | 'cookieName' | 'clientId'
>

export type RemoveSession = (
params: RemoveSessionParams,
getCookie: GetCookie,
setCookie: SetCookie,
query: AppSessionQuery,
) => AppSession | undefined
export const removeSession: RemoveSession = (
params,
getCookie,
setCookie,
query,
) => {
const sessions = getAllSessions(params, getCookie)
const keys = {
...keysFromQuery(query),
appClientId: params.clientId,
}
const isEqual = keysEquals(keys)
const toRemove = sessions.find(isEqual)
const allOtherSessions = sessions.filter((s) => s !== toRemove)
johannes-lindgren marked this conversation as resolved.
Show resolved Hide resolved
setAllSessions(params, setCookie, allOtherSessions)
return toRemove
}
19 changes: 19 additions & 0 deletions src/session/crud/setAllSessions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { AppSession } from '../types'
import { setSignedCookie, SetCookie } from '../../utils'
import { authCookieName } from '../authCookieName'
import { AuthHandlerParams } from '../../storyblok-auth-api'

export type SetAllSessionsParams = Pick<
AuthHandlerParams,
'clientSecret' | 'cookieName' | 'clientId'
>
export type SetAllSessions = (
params: SetAllSessionsParams,
setCookie: SetCookie,
sessions: AppSession[],
) => void
export const setAllSessions: SetAllSessions = (params, setCookie, sessions) => {
setSignedCookie(params.clientSecret, setCookie, authCookieName(params), {
sessions,
})
}
2 changes: 2 additions & 0 deletions src/session/crud/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './keysFromQuery'
export * from './keysEquals'
8 changes: 8 additions & 0 deletions src/session/crud/utils/keysEquals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { AppSession } from '../../types'

export const keysEquals =
(a: Pick<AppSession, 'appClientId' | 'spaceId' | 'userId'>) =>
(b: Pick<AppSession, 'appClientId' | 'spaceId' | 'userId'>) =>
a.appClientId === b.appClientId &&
a.spaceId === b.spaceId &&
a.userId === b.userId
9 changes: 9 additions & 0 deletions src/session/crud/utils/keysFromQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { AppSessionKeys, AppSessionQuery } from '../../types'

export const keysFromQuery = (keys: AppSessionQuery): AppSessionKeys => {
const { spaceId, userId } = keys
return {
spaceId: typeof spaceId === 'number' ? spaceId : parseInt(spaceId, 10),
userId: typeof userId === 'number' ? userId : parseInt(userId, 10),
}
}
6 changes: 4 additions & 2 deletions src/session/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from './sessionCookieStore'
export * from './isAppSessionQuery/isAppSessionQuery'
export * from './isAppSessionQuery'
export * from './crud'
export * from './types'
export * from './refreshStoredAppSession'
export * from './sessionCookieStore'
54 changes: 54 additions & 0 deletions src/session/refreshStoredAppSession.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { AppSession } from './types'
import { shouldRefresh } from './shouldRefresh/shouldRefresh'
import { refreshAppSession } from './refreshAppSession/refreshAppSession'
import { refreshToken } from '../storyblok-auth-api/refreshToken'
import { putSession, removeSession } from './crud'
import { AuthHandlerParams } from '../storyblok-auth-api'
import { GetCookie, SetCookie } from '../utils'

export type RefreshParams = Pick<
AuthHandlerParams,
'clientSecret' | 'clientId' | 'baseUrl' | 'endpointPrefix'
>
export type Refresh = (
params: RefreshParams,
getCookie: GetCookie,
setCookie: SetCookie,
appSession: AppSession | undefined,
) => Promise<AppSession | undefined>

/**
* Given a stored session and getters and setters for mutating the storage, refreshes the session
* @param params
* @param getCookie
* @param setCookie
* @param currentAppSession
*/
export const refreshStoredAppSession: Refresh = async (
params,
getCookie,
setCookie,
currentAppSession,
) => {
if (!currentAppSession) {
// passed undefined
return undefined
}
if (!shouldRefresh(currentAppSession)) {
// does not need refresh
return currentAppSession
}

// should refresh
const newAppSession = await refreshAppSession(refreshToken(fetch)(params))(
currentAppSession,
)
Comment on lines +43 to +45
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this always called from the browser? Or is there a possibility that this can be run on the server side?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Always called from the server

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah okay. then don't we need to pass fetch from node-fetch package?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gosh, you're right 🤯

In the very beginning, the library used to have code running in the client as well as on the server. It seems like I've forgotten to purge the tsconfig compilerOptions.lib "dom" option.

The library has only been used by us in a Next which uses the isomorphic-fetch, so we never noticed.

I've opened a new ticket for it here: https://storyblok.atlassian.net/browse/EXT-1531

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Does the ticket include the replacement of fetch with node-fetch? Or how do you want to approach that part?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure yet... there are a few options:

  • use node-fetch
  • require node version >= 18 (where fetch was added)
  • use openid-client to make the refresh request. When I'm replacing grant, I'll use the openid-client library for making request to the Storyblok API.

Probably the last option here


if (!newAppSession) {
// Refresh failed -> user becomes unauthenticated
removeSession(params, getCookie, setCookie, currentAppSession)
return undefined
}

return putSession(params, getCookie, setCookie, newAppSession)
}
55 changes: 23 additions & 32 deletions src/session/sessionCookieStore.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,30 @@
import { simpleSessionCookieStore } from './app-session-cookie-store'
import { shouldRefresh } from './shouldRefresh/shouldRefresh'
import { refreshAppSession } from './refreshAppSession/refreshAppSession'
import { refreshToken } from '../storyblok-auth-api/refreshToken'
import { AppSessionCookieStoreFactory } from './types'
import { AppSessionStore } from './types'
import { AppSessionCookieStoreFactory, AppSessionStore } from './types'
import { getAllSessions, getSession, putSession, removeSession } from './crud'
import { refreshStoredAppSession } from './refreshStoredAppSession'
import {
GetCookie,
getCookie as getNodeCookie,
SetCookie,
setCookie as setNodeCookie,
} from '../utils'

export const sessionCookieStore: AppSessionCookieStoreFactory =
(params) =>
(requestParams): AppSessionStore => {
const store = simpleSessionCookieStore(params)(requestParams)
const getCookie: GetCookie = (name) =>
getNodeCookie(requestParams.req, name)
const setCookie: SetCookie = (name, value) =>
setNodeCookie(requestParams.res, name, value)
Comment on lines +14 to +17
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So far, from my understanding this sessionCookieStore is still relying on the Node.js request and response object because getNodeCookie and setNodeCookie expects them. And your plan to going to make this replaceable? Is it the changes you told me you're thinking of?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes exactly.

It also makes the other functions easier to test, because we don't need to mock request and response objects; just the getter and setter functions.

return {
...store,
get: async (keys, options) => {
const currentSession = await store.get(keys)
if (options?.autoRefresh === false) {
return currentSession
}
if (!currentSession) {
return undefined
}
if (shouldRefresh(currentSession)) {
const newSession = await refreshAppSession(
refreshToken(fetch)(params),
)(currentSession)

if (!newSession) {
// Refresh failed -> user becomes unauthenticated
await store.remove(currentSession)
return undefined
}

await store.put(newSession)
return newSession
}
return currentSession
},
get: async (keys) =>
refreshStoredAppSession(
params,
getCookie,
setCookie,
getSession(params, getCookie, keys),
),
getAll: async () => getAllSessions(params, getCookie),
put: async (session) => putSession(params, getCookie, setCookie, session),
remove: async (keys) => removeSession(params, getCookie, setCookie, keys),
}
}
1 change: 1 addition & 0 deletions src/utils/GetCookie.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type GetCookie = (name: string) => string | undefined
1 change: 1 addition & 0 deletions src/utils/SetCookie.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type SetCookie = (name: string, value: string) => void
Loading