Skip to content

Commit dbd913a

Browse files
committed
feat(guard): allow the client to pass the jwt as a cookie
1 parent 53fb06a commit dbd913a

File tree

3 files changed

+125
-15
lines changed

3 files changed

+125
-15
lines changed

src/define_config.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,16 @@ import { Secret } from '@adonisjs/core/helpers'
66
export function jwtGuard<UserProvider extends JwtUserProviderContract<unknown>>(config: {
77
provider: UserProvider
88
tokenExpiresIn: number | string
9+
useCookies?: boolean
910
}): GuardConfigProvider<(ctx: HttpContext) => JwtGuard<UserProvider>> {
1011
return {
1112
async resolver(_, app) {
1213
const appKey = (app.config.get('app.appKey') as Secret<string>).release()
13-
const options = { secret: appKey, expiresIn: config.tokenExpiresIn }
14+
const options = {
15+
secret: appKey,
16+
expiresIn: config.tokenExpiresIn,
17+
useCookies: config.useCookies,
18+
}
1419
return (ctx) => new JwtGuard(ctx, config.provider, options)
1520
},
1621
}

src/jwt.ts

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export interface JwtUserProviderContract<RealUser> {
4545
export type JwtGuardOptions = {
4646
secret: string
4747
expiresIn?: number | string
48+
useCookies?: boolean
4849
}
4950

5051
export class JwtGuard<UserProvider extends JwtUserProviderContract<unknown>>
@@ -102,6 +103,12 @@ export class JwtGuard<UserProvider extends JwtUserProviderContract<unknown>>
102103
: {}
103104
)
104105

106+
if (this.#options.useCookies) {
107+
return this.#ctx.response.cookie('token', token, {
108+
httpOnly: true,
109+
})
110+
}
111+
105112
return {
106113
type: 'bearer',
107114
token: token,
@@ -124,24 +131,39 @@ export class JwtGuard<UserProvider extends JwtUserProviderContract<unknown>>
124131
}
125132
this.authenticationAttempted = true
126133

134+
const cookieHeader = this.#ctx.request.request.headers.cookie
135+
let token
136+
127137
/**
128-
* Ensure the auth header exists
138+
* If cookies are enabled, then read the token from the cookies
129139
*/
130-
const authHeader = this.#ctx.request.header('authorization')
131-
if (!authHeader) {
132-
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
133-
guardDriverName: this.driverName,
134-
})
140+
if (cookieHeader) {
141+
;[, token] = cookieHeader!.split('=')
135142
}
136143

137144
/**
138-
* Split the header value and read the token from it
145+
* If token is missing on cookies, then try to read it from the header authorization
139146
*/
140-
const [, token] = authHeader.split('Bearer ')
141147
if (!token) {
142-
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
143-
guardDriverName: this.driverName,
144-
})
148+
/**
149+
* Ensure the auth header exists
150+
*/
151+
const authHeader = this.#ctx.request.header('authorization')
152+
if (!authHeader) {
153+
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
154+
guardDriverName: this.driverName,
155+
})
156+
}
157+
158+
/**
159+
* Split the header value and read the token from it
160+
*/
161+
;[, token] = authHeader!.split('Bearer ')
162+
if (!token) {
163+
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
164+
guardDriverName: this.driverName,
165+
})
166+
}
145167
}
146168

147169
/**
@@ -210,10 +232,10 @@ export class JwtGuard<UserProvider extends JwtUserProviderContract<unknown>>
210232
async authenticateAsClient(
211233
user: UserProvider[typeof symbols.PROVIDER_REAL_USER]
212234
): Promise<AuthClientResponse> {
213-
const token = await this.generate(user)
235+
const token: any = await this.generate(user)
214236
return {
215237
headers: {
216-
authorization: `Bearer ${token.token}`,
238+
authorization: `Bearer ${this.#options.useCookies ? token : token.token}`,
217239
},
218240
}
219241
}

tests/guard.spec.ts

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,90 @@ test.group('Jwt guard | authenticate', () => {
2323
assert.deepEqual(guard.getUserOrFail(), authenticatedUser)
2424
})
2525

26-
test('throw error when authorization header is missing', async ({ assert }) => {
26+
test('it should return a cookie when user is authenticated', async ({ assert }) => {
27+
const ctx = new HttpContextFactory().create()
28+
const userProvider = new JwtFakeUserProvider()
29+
30+
const guard = new JwtGuard(ctx, userProvider, { secret: 'thisisasecret', useCookies: true })
31+
ctx.request.request.headers.cookie = 'token=' + jwt.sign({ userId: 1 }, 'thisisasecret')
32+
33+
const authenticatedUser = await guard.authenticate()
34+
35+
assert.isTrue(guard.isAuthenticated)
36+
assert.isTrue(guard.authenticationAttempted)
37+
38+
assert.equal(guard.user, authenticatedUser)
39+
assert.deepEqual(guard.getUserOrFail(), authenticatedUser)
40+
})
41+
42+
test('throw error when cookie header is invalid', async ({ assert }) => {
43+
const ctx = new HttpContextFactory().create()
44+
const userProvider = new JwtFakeUserProvider()
45+
46+
const guard = new JwtGuard(ctx, userProvider, { secret: 'thisisasecret' })
47+
ctx.request.request.headers.cookie = 'foo bar'
48+
const [result] = await Promise.allSettled([guard.authenticate()])
49+
50+
assert.equal(result!.status, 'rejected')
51+
if (result!.status === 'rejected') {
52+
assert.instanceOf(result!.reason, errors.E_UNAUTHORIZED_ACCESS)
53+
}
54+
55+
assert.isUndefined(guard.user)
56+
assert.throws(() => guard.getUserOrFail(), 'Unauthorized access')
57+
58+
assert.isFalse(guard.isAuthenticated)
59+
assert.isTrue(guard.authenticationAttempted)
60+
})
61+
62+
test('throw error when cookie token is empty', async ({ assert }) => {
63+
const ctx = new HttpContextFactory().create()
64+
const userProvider = new JwtFakeUserProvider()
65+
66+
const guard = new JwtGuard(ctx, userProvider, { secret: 'thisisasecret' })
67+
ctx.request.request.headers.cookie = 'token='
68+
const [result] = await Promise.allSettled([guard.authenticate()])
69+
70+
assert.equal(result!.status, 'rejected')
71+
if (result!.status === 'rejected') {
72+
assert.instanceOf(result!.reason, errors.E_UNAUTHORIZED_ACCESS)
73+
}
74+
75+
assert.isUndefined(guard.user)
76+
assert.throws(() => guard.getUserOrFail(), 'Unauthorized access')
77+
78+
assert.isFalse(guard.isAuthenticated)
79+
assert.isTrue(guard.authenticationAttempted)
80+
})
81+
82+
test('throw error when cookie token has been expired', async ({ assert }) => {
83+
const ctx = new HttpContextFactory().create()
84+
const userProvider = new JwtFakeUserProvider()
85+
const user = await userProvider.findById(1)
86+
const token = await userProvider.createToken(user!.getOriginal(), 'thisisasecret', {
87+
expiresIn: '1h',
88+
})
89+
90+
timeTravel(61 * 60)
91+
92+
const guard = new JwtGuard(ctx, userProvider, { secret: 'thisisasecret' })
93+
ctx.request.request.headers.cookie = `token=${token}`
94+
const [result] = await Promise.allSettled([guard.authenticate()])
95+
96+
assert.equal(result!.status, 'rejected')
97+
if (result!.status === 'rejected') {
98+
assert.instanceOf(result!.reason, errors.E_UNAUTHORIZED_ACCESS)
99+
}
100+
101+
assert.isUndefined(guard.user)
102+
assert.throws(() => guard.getUserOrFail(), 'Unauthorized access')
103+
assert.isFalse(guard.isAuthenticated)
104+
assert.isTrue(guard.authenticationAttempted)
105+
})
106+
107+
test('throw error when authorization header and cookie header are missing', async ({
108+
assert,
109+
}) => {
27110
const ctx = new HttpContextFactory().create()
28111
const userProvider = new JwtFakeUserProvider()
29112

0 commit comments

Comments
 (0)