Skip to content

Commit

Permalink
feat(guard): allow the client to pass the jwt as a cookie
Browse files Browse the repository at this point in the history
  • Loading branch information
MaximeMRF committed May 12, 2024
1 parent 53fb06a commit dbd913a
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 15 deletions.
7 changes: 6 additions & 1 deletion src/define_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@ import { Secret } from '@adonisjs/core/helpers'
export function jwtGuard<UserProvider extends JwtUserProviderContract<unknown>>(config: {
provider: UserProvider
tokenExpiresIn: number | string
useCookies?: boolean
}): GuardConfigProvider<(ctx: HttpContext) => JwtGuard<UserProvider>> {
return {
async resolver(_, app) {
const appKey = (app.config.get('app.appKey') as Secret<string>).release()
const options = { secret: appKey, expiresIn: config.tokenExpiresIn }
const options = {
secret: appKey,
expiresIn: config.tokenExpiresIn,
useCookies: config.useCookies,
}
return (ctx) => new JwtGuard(ctx, config.provider, options)
},
}
Expand Down
48 changes: 35 additions & 13 deletions src/jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export interface JwtUserProviderContract<RealUser> {
export type JwtGuardOptions = {
secret: string
expiresIn?: number | string
useCookies?: boolean
}

export class JwtGuard<UserProvider extends JwtUserProviderContract<unknown>>
Expand Down Expand Up @@ -102,6 +103,12 @@ export class JwtGuard<UserProvider extends JwtUserProviderContract<unknown>>
: {}
)

if (this.#options.useCookies) {
return this.#ctx.response.cookie('token', token, {
httpOnly: true,
})
}

return {
type: 'bearer',
token: token,
Expand All @@ -124,24 +131,39 @@ export class JwtGuard<UserProvider extends JwtUserProviderContract<unknown>>
}
this.authenticationAttempted = true

const cookieHeader = this.#ctx.request.request.headers.cookie
let token

/**
* Ensure the auth header exists
* If cookies are enabled, then read the token from the cookies
*/
const authHeader = this.#ctx.request.header('authorization')
if (!authHeader) {
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
guardDriverName: this.driverName,
})
if (cookieHeader) {
;[, token] = cookieHeader!.split('=')
}

/**
* Split the header value and read the token from it
* If token is missing on cookies, then try to read it from the header authorization
*/
const [, token] = authHeader.split('Bearer ')
if (!token) {
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
guardDriverName: this.driverName,
})
/**
* Ensure the auth header exists
*/
const authHeader = this.#ctx.request.header('authorization')
if (!authHeader) {
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
guardDriverName: this.driverName,
})
}

/**
* Split the header value and read the token from it
*/
;[, token] = authHeader!.split('Bearer ')
if (!token) {
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
guardDriverName: this.driverName,
})
}
}

/**
Expand Down Expand Up @@ -210,10 +232,10 @@ export class JwtGuard<UserProvider extends JwtUserProviderContract<unknown>>
async authenticateAsClient(
user: UserProvider[typeof symbols.PROVIDER_REAL_USER]
): Promise<AuthClientResponse> {
const token = await this.generate(user)
const token: any = await this.generate(user)
return {
headers: {
authorization: `Bearer ${token.token}`,
authorization: `Bearer ${this.#options.useCookies ? token : token.token}`,
},
}
}
Expand Down
85 changes: 84 additions & 1 deletion tests/guard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,90 @@ test.group('Jwt guard | authenticate', () => {
assert.deepEqual(guard.getUserOrFail(), authenticatedUser)
})

test('throw error when authorization header is missing', async ({ assert }) => {
test('it should return a cookie when user is authenticated', async ({ assert }) => {
const ctx = new HttpContextFactory().create()
const userProvider = new JwtFakeUserProvider()

const guard = new JwtGuard(ctx, userProvider, { secret: 'thisisasecret', useCookies: true })
ctx.request.request.headers.cookie = 'token=' + jwt.sign({ userId: 1 }, 'thisisasecret')

const authenticatedUser = await guard.authenticate()

assert.isTrue(guard.isAuthenticated)
assert.isTrue(guard.authenticationAttempted)

assert.equal(guard.user, authenticatedUser)
assert.deepEqual(guard.getUserOrFail(), authenticatedUser)
})

test('throw error when cookie header is invalid', async ({ assert }) => {
const ctx = new HttpContextFactory().create()
const userProvider = new JwtFakeUserProvider()

const guard = new JwtGuard(ctx, userProvider, { secret: 'thisisasecret' })
ctx.request.request.headers.cookie = 'foo bar'
const [result] = await Promise.allSettled([guard.authenticate()])

assert.equal(result!.status, 'rejected')
if (result!.status === 'rejected') {
assert.instanceOf(result!.reason, errors.E_UNAUTHORIZED_ACCESS)
}

assert.isUndefined(guard.user)
assert.throws(() => guard.getUserOrFail(), 'Unauthorized access')

assert.isFalse(guard.isAuthenticated)
assert.isTrue(guard.authenticationAttempted)
})

test('throw error when cookie token is empty', async ({ assert }) => {
const ctx = new HttpContextFactory().create()
const userProvider = new JwtFakeUserProvider()

const guard = new JwtGuard(ctx, userProvider, { secret: 'thisisasecret' })
ctx.request.request.headers.cookie = 'token='
const [result] = await Promise.allSettled([guard.authenticate()])

assert.equal(result!.status, 'rejected')
if (result!.status === 'rejected') {
assert.instanceOf(result!.reason, errors.E_UNAUTHORIZED_ACCESS)
}

assert.isUndefined(guard.user)
assert.throws(() => guard.getUserOrFail(), 'Unauthorized access')

assert.isFalse(guard.isAuthenticated)
assert.isTrue(guard.authenticationAttempted)
})

test('throw error when cookie token has been expired', async ({ assert }) => {
const ctx = new HttpContextFactory().create()
const userProvider = new JwtFakeUserProvider()
const user = await userProvider.findById(1)
const token = await userProvider.createToken(user!.getOriginal(), 'thisisasecret', {
expiresIn: '1h',
})

timeTravel(61 * 60)

const guard = new JwtGuard(ctx, userProvider, { secret: 'thisisasecret' })
ctx.request.request.headers.cookie = `token=${token}`
const [result] = await Promise.allSettled([guard.authenticate()])

assert.equal(result!.status, 'rejected')
if (result!.status === 'rejected') {
assert.instanceOf(result!.reason, errors.E_UNAUTHORIZED_ACCESS)
}

assert.isUndefined(guard.user)
assert.throws(() => guard.getUserOrFail(), 'Unauthorized access')
assert.isFalse(guard.isAuthenticated)
assert.isTrue(guard.authenticationAttempted)
})

test('throw error when authorization header and cookie header are missing', async ({
assert,
}) => {
const ctx = new HttpContextFactory().create()
const userProvider = new JwtFakeUserProvider()

Expand Down

0 comments on commit dbd913a

Please sign in to comment.