Skip to content

Commit

Permalink
feat(auth): add cookie sessions authentication (#86)
Browse files Browse the repository at this point in the history
add new cookie-sessions package for HttpOnly Cookies auth, pending csrf protection
  • Loading branch information
bahdcoder authored Mar 7, 2021
1 parent 87eff00 commit 00f4a11
Show file tree
Hide file tree
Showing 8 changed files with 352 additions and 46 deletions.
4 changes: 3 additions & 1 deletion examples/blog/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ tensei()
welcome(),
mailgun('transactions').domain(process.env.MAILGUN_DOMAIN).plugin(),
cms().plugin(),
auth().rolesAndPermissions().plugin(),
auth().rolesAndPermissions()
.cookieSessions()
.plugin(),
media().plugin(),
rest().plugin(),
graphql().plugin()
Expand Down
1 change: 1 addition & 0 deletions packages/auth/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export interface AuthPluginConfig {
accessTokenExpiresIn: number
refreshTokenExpiresIn: number
}
httpOnlyCookiesAuth?: boolean
getUserPayloadFromProviderData?: (providerData: DataPayload) => DataPayload
separateSocialLoginAndRegister: boolean
beforeLogin: AuthHookFunction
Expand Down
101 changes: 77 additions & 24 deletions packages/auth/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class Auth {
enableRefreshTokens: false,
userResource: 'User',
roleResource: 'Role',
httpOnlyCookiesAuth: false,
permissionResource: 'Permission',
passwordResetResource: 'Password Reset',
fields: [],
Expand Down Expand Up @@ -101,6 +102,12 @@ class Auth {
return this
}

public cookieSessions() {
this.config.httpOnlyCookiesAuth = true

return this
}

public registered(registered: AuthPluginConfig['registered']) {
this.config.registered = registered

Expand Down Expand Up @@ -495,7 +502,7 @@ class Auth {
extendResources([this.resources.oauthIdentity])
}

if (this.socialAuthEnabled()) {
if (this.socialAuthEnabled() || this.config.httpOnlyCookiesAuth) {
databaseConfig.entities = [
...(databaseConfig.entities || []),
require('express-session-mikro-orm').generateSessionEntity(
Expand Down Expand Up @@ -556,6 +563,30 @@ class Auth {
permission: this.resources.permission.serialize()
})

if (this.config.httpOnlyCookiesAuth || this.socialAuthEnabled()) {
const ExpressSession = require('express-session')
const ExpressSessionMikroORMStore = require('express-session-mikro-orm').default

const Store = ExpressSessionMikroORMStore(ExpressSession, {
entityName: `${this.resources.user.data.pascalCaseName}Session`,
tableName: `${this.resources.user.data.snakeCaseNamePlural}_sessions`,
collection: `${this.resources.user.data.snakeCaseNamePlural}_sessions`
})

app.use(
ExpressSession({
store: new Store({
orm: config.orm
}) as any,
resave: false,
saveUninitialized: false,
cookie: this.config.cookieOptions,
secret:
process.env.SESSION_SECRET || '__sessions__secret__'
})
)
}

if (this.socialAuthEnabled()) {
const { register } = require('@tensei/social-auth')

Expand Down Expand Up @@ -773,10 +804,6 @@ class Auth {
private extendRoutes() {
const name = this.resources.user.data.slugSingular

const extend = {
tags: ['Auth']
}

return [
route(`Login ${name}`)
.group('Auth')
Expand Down Expand Up @@ -829,6 +856,7 @@ class Auth {
try {
return created(await this.register(request as any))
} catch (error) {
request.logger.error(error)
return unprocess(error)
}
}
Expand Down Expand Up @@ -1132,12 +1160,12 @@ class Auth {
[this.resources.user.data.snakeCaseName]: user
}
}),
graphQlQuery(`Logout ${name}`)
.path('logout')
.mutation()
.handle(async (_, args, ctx) => {
return true
}),
...(this.config.httpOnlyCookiesAuth ? [graphQlQuery(`Logout ${name}`)
.path('logout')
.mutation()
.handle(async (_, args, ctx) => {
return true
})] : []),
graphQlQuery(`Register ${name}`)
.path('register')
.mutation()
Expand Down Expand Up @@ -1374,6 +1402,10 @@ class Auth {
return undefined
}

if (this.config.httpOnlyCookiesAuth) {
return undefined
}

const { body } = ctx
const userField = this.resources.user.data.snakeCaseName
const tokenName = this.resources.token.data.pascalCaseName
Expand Down Expand Up @@ -1458,34 +1490,47 @@ class Auth {
[this.resources.user.data.snakeCaseName]: ctx.user
}

userPayload.access_token = this.generateJwt({
id: ctx.user.id
})
if (! this.config.httpOnlyCookiesAuth) {
userPayload.access_token = this.generateJwt({
id: ctx.user.id
})

userPayload.expires_in = this.config.tokensConfig.accessTokenExpiresIn
}

if (this.config.enableRefreshTokens) {
userPayload.refresh_token = refreshToken
}

userPayload.expires_in = this.config.tokensConfig.accessTokenExpiresIn
if (this.config.httpOnlyCookiesAuth) {
ctx.request.session.user = ctx.user
}

return userPayload
}

private extendGraphQLTypeDefs(gql: any) {
const snakeCaseName = this.resources.user.data.snakeCaseName

const cookies = this.config.httpOnlyCookiesAuth

return gql`
type register_response {
${cookies ? '' : `
access_token: String!
${this.config.enableRefreshTokens ? 'refresh_token: String!' : ''}
expires_in: Int!
`}
${snakeCaseName}: ${snakeCaseName}!
}
type login_response {
${cookies ? '' : `
access_token: String!
${this.config.enableRefreshTokens ? 'refresh_token: String!' : ''}
expires_in: Int!
`}
${snakeCaseName}: ${snakeCaseName}!
}
Expand Down Expand Up @@ -1577,7 +1622,9 @@ class Auth {
extend type Mutation {
login(object: login_input!): login_response!
${cookies ? `
logout: Boolean!
`: ''}
register(object: insert_${snakeCaseName}_input!): register_response!
request_password_reset(object: request_password_reset_input!): Boolean!
reset_password(object: reset_password_input!): Boolean!
Expand Down Expand Up @@ -1995,20 +2042,24 @@ class Auth {
}

private populateContextFromToken = async (
token: string,
token: string|undefined,
ctx: ApiContext
) => {
const { manager } = ctx

try {
let id

const payload = Jwt.verify(
token,
this.config.tokensConfig.secretKey
) as JwtPayload

id = payload.id
if (! this.config.httpOnlyCookiesAuth) {
const payload = Jwt.verify(
token!,
this.config.tokensConfig.secretKey
) as JwtPayload

id = payload.id
} else {
id = ctx.req.session?.user?.id
}

if (!id) {
return
Expand Down Expand Up @@ -2058,8 +2109,6 @@ class Auth {
const { headers } = req
const [, token] = (headers['authorization'] || '').split('Bearer ')

if (!token) return

return this.populateContextFromToken(token, ctx)
}

Expand Down Expand Up @@ -2225,6 +2274,10 @@ class Auth {
return undefined
}

if (this.config.httpOnlyCookiesAuth) {
return undefined
}

const plainTextToken = this.generateRandomToken(64)

// Expire all existing refresh tokens for this user.
Expand Down
Loading

0 comments on commit 00f4a11

Please sign in to comment.