Skip to content
Draft
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
2 changes: 1 addition & 1 deletion examples/actions/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const lists = {
actions: {
vote: {
access: ({ context }) => {
const ua = context.req?.headers['user-agent'] ?? ''
const ua = context.req?.headers.get('user-agent') ?? ''
// only allow voting from Chrome browsers
return ua.includes('Chrome')
},
Expand Down
23 changes: 6 additions & 17 deletions examples/auth-magic-link/keystone.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,12 @@
import { config } from '@keystone-6/core'
import { statelessSessions } from '@keystone-6/core/session'
import { createAuth } from '@keystone-6/auth'
import { lists, extendGraphqlSchema } from './schema'
import { lists, extendGraphqlSchema, sessionStrategy } from './schema'
import type { TypeInfo } from '.keystone/types'

// WARNING: this example is for demonstration purposes only
// as with each of our examples, it has not been vetted
// or tested for any particular usage

// WARNING: you need to change this
const sessionSecret = '-- DEV COOKIE SECRET; CHANGE ME --'

// statelessSessions uses cookies for session tracking
// these cookies have an expiry, in seconds
// we use an expiry of one hour for this example
const sessionMaxAge = 60 * 60

// withAuth is a function we can use to wrap our base configuration
const { withAuth } = createAuth({
// this is the list that contains our users
Expand All @@ -36,6 +27,11 @@ const { withAuth } = createAuth({
// the following fields are used by the "Create First User" form
fields: ['name', 'password'],
},
sessionStrategy,
getSession: ({ context, data }) =>
context.query.User.findOne({
where: { id: data.itemId },
}),
})

export default withAuth<TypeInfo>(
Expand All @@ -51,12 +47,5 @@ export default withAuth<TypeInfo>(
graphql: {
extendGraphqlSchema,
},
// you can find out more at https://keystonejs.com/docs/apis/session#session-api
session: statelessSessions({
// the maxAge option controls how long session cookies are valid for before they expire
maxAge: sessionMaxAge,
// the session secret is used to encrypt cookie data
secret: sessionSecret,
}),
})
)
27 changes: 19 additions & 8 deletions examples/auth-magic-link/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,44 @@ import { password, text, timestamp } from '@keystone-6/core/fields'
import type { Lists, Context, Session } from '.keystone/types'

import { randomBytes } from 'node:crypto'
import { statelessSessions } from '@keystone-6/auth'

const g = gWithContext<Context>()
type g<T> = gWithContext.infer<T>

declare module '.keystone/types' {
interface Session {
itemId: string
listKey: string
id: string
}
}

function hasSession({ session }: { session?: Session }) {
return Boolean(session)
}

// WARNING: you need to change this
const sessionSecret = '-- DEV COOKIE SECRET; CHANGE ME --'

// statelessSessions uses cookies for session tracking
// these cookies have an expiry, in seconds
// we use an expiry of one hour for this example
const sessionMaxAge = 60 * 60

export const sessionStrategy = statelessSessions({
// the maxAge option controls how long session cookies are valid for before they expire
maxAge: sessionMaxAge,
// the session secret is used to encrypt cookie data
secret: sessionSecret,
})

function isSameUserFilter({ session }: { session?: Session }) {
// you need to have a session to do this
if (!session) return false

// only yourself
return {
id: {
equals: session.itemId,
equals: session.id,
},
}
}
Expand Down Expand Up @@ -148,9 +163,6 @@ export const extendGraphqlSchema = g.extend(base => {
},

async resolve(args, { userId, token }, context) {
if (!context.sessionStrategy)
throw new Error('No session implementation available on context')

const kdf = (base.schema.getType('User') as any).getFields()?.password.extensions
?.keystoneSecretField
const sudoContext = context.sudo()
Expand All @@ -176,10 +188,9 @@ export const extendGraphqlSchema = g.extend(base => {
},
})

await context.sessionStrategy.start({
await sessionStrategy.start({
context,
data: {
listKey: 'User',
itemId: userId,
},
})
Expand Down
28 changes: 15 additions & 13 deletions examples/auth/keystone.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { config } from '@keystone-6/core'
import { statelessSessions } from '@keystone-6/core/session'
import { createAuth } from '@keystone-6/auth'
import { createAuth, statelessSessions } from '@keystone-6/auth'
import { lists } from './schema'
import type { TypeInfo } from '.keystone/types'

Expand Down Expand Up @@ -42,9 +41,19 @@ const { withAuth } = createAuth({
isAdmin: true,
},
},

// add isAdmin to the session data
sessionData: 'isAdmin',
sessionStrategy: statelessSessions({
// the maxAge option controls how long session cookies are valid for before they expire
maxAge: sessionMaxAge,
// the session secret is used to encrypt cookie data
secret: sessionSecret,
}),
async getSession({ context, data }) {
const user = await context.db.User.findOne({
where: { id: data.itemId },
})
if (!user) return
return { user }
},
})

export default withAuth<TypeInfo>(
Expand All @@ -60,15 +69,8 @@ export default withAuth<TypeInfo>(
ui: {
// only admins can view the AdminUI
isAccessAllowed: context => {
return context.session?.data?.isAdmin ?? false
return context.session?.user.isAdmin ?? false
},
},
// you can find out more at https://keystonejs.com/docs/apis/session#session-api
session: statelessSessions({
// the maxAge option controls how long session cookies are valid for before they expire
maxAge: sessionMaxAge,
// the session secret is used to encrypt cookie data
secret: sessionSecret,
}),
})
)
15 changes: 6 additions & 9 deletions examples/auth/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,7 @@ import type { Lists, Session } from '.keystone/types'
// or tested for any particular usage
declare module '.keystone/types' {
interface Session {
itemId: string
data: {
isAdmin: boolean
}
user: Lists.User.Item
}
}

Expand All @@ -24,26 +21,26 @@ function isAdminOrSameUser({ session, item }: { session?: Session; item: Lists.U
if (!session) return false

// admins can do anything
if (session.data.isAdmin) return true
if (session.user.isAdmin) return true

// no item? then no
if (!item) return false

// the authenticated user needs to be equal to the user we are updating
return session.itemId === item.id
return session.user.id === item.id
}

function isAdminOrSameUserFilter({ session }: { session?: Session }) {
// you need to have a session to do this
if (!session) return false

// admins can see everything
if (session.data?.isAdmin) return {}
if (session.user.isAdmin) return {}

// only yourself
return {
id: {
equals: session.itemId,
equals: session.user.id,
},
}
}
Expand All @@ -53,7 +50,7 @@ function isAdmin({ session }: { session?: Session }) {
if (!session) return false

// admins can do anything
if (session.data.isAdmin) return true
if (session.user.isAdmin) return true

// otherwise, no
return false
Expand Down
96 changes: 49 additions & 47 deletions examples/custom-session-invalidation/keystone.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { config } from '@keystone-6/core'
import { statelessSessions } from '@keystone-6/core/session'
import { createAuth } from '@keystone-6/auth'
import { createAuth, SessionStrategy, statelessSessions } from '@keystone-6/auth'
import { lists } from './schema'
import type { Config, Context, TypeInfo, Session } from '.keystone/types'
import type { TypeInfo, Lists } from '.keystone/types'
import type { BaseKeystoneTypeInfo } from '@keystone-6/core/types'

// WARNING: this example is for demonstration purposes only
// as with each of our examples, it has not been vetted
// or tested for any particular usage

// withAuth is a function we can use to wrap our base configuration
const { withAuth } = createAuth({
const { withAuth } = createAuth<Lists.User.TypeInfo, { itemId: string; startedAt: number }>({
// this is the list that contains our users
listKey: 'User',

Expand All @@ -29,55 +29,57 @@ const { withAuth } = createAuth({
fields: ['name', 'password'],
},

sessionData: 'passwordChangedAt',
sessionStrategy: withSessionStartedAt(statelessSessions()),
async getSession({ context, data }) {
const user = await context.db.User.findOne({
where: { id: data.itemId },
})
if (!user) return
if (user.passwordChangedAt && user.passwordChangedAt > new Date(data.startedAt)) {
return
}
return { user }
},
})

function withSessionInvalidation(config: Config): Config {
const existingSessionStrategy = config.session!

function withSessionStartedAt<T, TypeInfo extends BaseKeystoneTypeInfo>(
existingSessionStrategy: SessionStrategy<
T & { startedAt: number },
T & { startedAt: number },
TypeInfo
>
): SessionStrategy<T, T & { startedAt: number }, TypeInfo> {
return {
...config,
session: {
...existingSessionStrategy,
async get({ context }: { context: Context }): Promise<Session | undefined> {
const session = await existingSessionStrategy.get({ context })
if (!session) return

// has the password changed since the session started?
if (new Date(session.data.passwordChangedAt) > new Date(session.startedAt)) {
// invalidate the session if password changed
await existingSessionStrategy.end({ context })
return
}

return session
},
async start({ context, data }: { context: Context; data: Session }) {
return await existingSessionStrategy.start({
context,
data: {
...data,
startedAt: Date.now(),
},
})
},
async start({ context, data }) {
await existingSessionStrategy.start({
context,
data: { ...data, startedAt: Date.now() },
})
},
async get({ context }) {
const session = await existingSessionStrategy.get({ context })
if (
!session ||
typeof session !== 'object' ||
!('startedAt' in session) ||
typeof session.startedAt !== 'number'
)
return
return { ...session, startedAt: session.startedAt }
},
end: existingSessionStrategy.end,
}
}

export default withSessionInvalidation(
withAuth(
config<TypeInfo>({
db: {
provider: 'sqlite',
url: process.env.DATABASE_URL || 'file:./keystone-example.db',
export default withAuth(
config<TypeInfo>({
db: {
provider: 'sqlite',
url: process.env.DATABASE_URL || 'file:./keystone-example.db',

// WARNING: this is only needed for our monorepo examples, dont do this
prismaClientPath: 'node_modules/myprisma',
},
lists,
// you can find out more at https://keystonejs.com/docs/apis/session#session-api
session: statelessSessions<Session>(),
})
)
// WARNING: this is only needed for our monorepo examples, dont do this
prismaClientPath: 'node_modules/myprisma',
},
lists,
})
)
9 changes: 2 additions & 7 deletions examples/custom-session-invalidation/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,7 @@ import type { Lists, Session } from '.keystone/types'
// needs to be compatible with withAuth
declare module '.keystone/types' {
interface Session {
listKey: string
itemId: string
data: {
passwordChangedAt: string
}
startedAt: number
user: Lists.User.Item
}
}

Expand All @@ -30,7 +25,7 @@ function isSameUserFilter({ session }: { session?: Session }) {
// the authenticated user can only see themselves
return {
id: {
equals: session.itemId,
equals: session.user.id,
},
}
}
Expand Down
Loading
Loading