Skip to content

Commit

Permalink
Merge pull request #1 from MaximeMRF/feat/pass-the-jwt-by-cookies
Browse files Browse the repository at this point in the history
feat(guard): allow the client to pass the jwt as a cookie
  • Loading branch information
MaximeMRF authored May 12, 2024
2 parents 53fb06a + 1030df8 commit b06816f
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 17 deletions.
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ const authConfig = defineConfig({
jwt: jwtGuard({
// tokenExpiresIn can be a string or a number, it can be optional
tokenExpiresIn: '1h',
// if you want to use cookies for the authentication instead of the bearer token (optional)
useCookies: true,
provider: sessionUserProvider({
model: () => import('#models/user'),
}),
Expand All @@ -49,7 +51,7 @@ const authConfig = defineConfig({
})
```

`tokenExpiresIn` is the time before the token expires it can be a string or a number, it can be optional.
`tokenExpiresIn` is the time before the token expires it can be a string or a number and it can be optional.

```typescript
// string
Expand All @@ -58,6 +60,14 @@ tokenExpiresIn: '1h'
tokenExpiresIn: 60 * 60
```

You can also use cookies for the authentication instead of the bearer token by setting `useCookies` to `true`.

```typescript
useCookies: true
```

If you just want to use jwt with the bearer token no need to set `useCookies` to `false` you can just remove it.

## Usage

To make a protected route, you have to use the `auth` middleware with the `jwt` guard.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@maximemrf/adonisjs-jwt",
"description": "",
"version": "0.0.14",
"version": "0.1.0",
"engines": {
"node": ">=20.6.0"
},
Expand Down
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
50 changes: 37 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,41 @@ 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 = this.#ctx.request.cookie('token')
? this.#ctx.request.cookie('token')
: (this.#ctx.request.request.headers.cookie!.match(/token=(.*?)(;|$)/) || [])[1]
}

/**
* 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 +234,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 b06816f

Please sign in to comment.