Skip to content

Commit

Permalink
feat: add support to define custom connection for tokens provider
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Mar 24, 2021
1 parent 6eaaf36 commit ea71af5
Show file tree
Hide file tree
Showing 9 changed files with 191 additions and 9 deletions.
6 changes: 6 additions & 0 deletions adonis-typings/auth.ts
Expand Up @@ -137,6 +137,11 @@ declare module '@ioc:Adonis/Addons/Auth' {
* providers, given they only store a single token.
*/
export interface TokenProviderContract {
/**
* Define a custom connection for the driver in use
*/
setConnection(connection: any): this

/**
* Saves the token to some persistance storage and returns an lookup
* id. We introduced the concept of lookup ids, since lookups by
Expand Down Expand Up @@ -616,6 +621,7 @@ declare module '@ioc:Adonis/Addons/Auth' {
Name extends keyof GuardsList
> extends GuardContract<Provider, Name> {
token?: ProviderTokenContract
tokenProvider: TokenProviderContract

/**
* Attempt to verify user credentials and perform login
Expand Down
2 changes: 1 addition & 1 deletion src/Guards/Oat/index.ts
Expand Up @@ -41,7 +41,7 @@ export class OATGuard extends BaseGuard<any> implements OATGuardContract<any, an
private emitter: EmitterContract,
provider: UserProviderContract<any>,
private ctx: HttpContextContract,
private tokenProvider: TokenProviderContract
public tokenProvider: TokenProviderContract
) {
super(name, config, provider)
}
Expand Down
25 changes: 21 additions & 4 deletions src/TokenProviders/Database/index.ts
Expand Up @@ -9,7 +9,7 @@

import { DateTime } from 'luxon'
import { safeEqual } from '@poppinss/utils/build/helpers'
import { DatabaseContract } from '@ioc:Adonis/Lucid/Database'
import { DatabaseContract, QueryClientContract } from '@ioc:Adonis/Lucid/Database'
import {
TokenProviderContract,
ProviderTokenContract,
Expand All @@ -24,13 +24,22 @@ import { ProviderToken } from '../../Tokens/ProviderToken'
export class TokenDatabaseProvider implements TokenProviderContract {
constructor(private config: DatabaseTokenProviderConfig, private db: DatabaseContract) {}

/**
* Custom connection or query client
*/
private connection?: string | QueryClientContract

/**
* Returns the query client for database queries
*/
private getQueryClient() {
return this.config.connection
? this.db.connection(this.config.connection)
: this.db.connection()
if (!this.connection) {
return this.db.connection(this.config.connection)
}

return typeof this.connection === 'string'
? this.db.connection(this.connection)
: this.connection
}

/**
Expand All @@ -48,6 +57,14 @@ export class TokenDatabaseProvider implements TokenProviderContract {
.where('type', tokenType)
}

/**
* Define custom connection
*/
public setConnection(connection: string | QueryClientContract): this {
this.connection = connection
return this
}

/**
* Reads the token using the lookup token id
*/
Expand Down
27 changes: 27 additions & 0 deletions src/TokenProviders/Redis/index.ts
Expand Up @@ -40,10 +40,27 @@ type PersistedToken = {
export class TokenRedisProvider implements TokenProviderContract {
constructor(private config: RedisTokenProviderConfig, private redis: RedisManagerContract) {}

/**
* Custom connection or query client
*/
private connection?: string | RedisConnectionContract | RedisClusterConnectionContract

/**
* Returns the singleton instance of the redis connection
*/
private getRedisConnection(): RedisConnectionContract | RedisClusterConnectionContract {
/**
* Use custom connection if defined
*/
if (this.connection) {
return typeof this.connection === 'string'
? this.redis.connection(this.connection)
: this.connection
}

/**
* Config must have a connection defined
*/
if (!this.config.redisConnection) {
throw new Exception(
'Missing "redisConnection" property for auth redis provider inside "config/auth" file',
Expand Down Expand Up @@ -80,6 +97,16 @@ export class TokenRedisProvider implements TokenProviderContract {
}
}

/**
* Define custom connection
*/
public setConnection(
connection: string | RedisConnectionContract | RedisClusterConnectionContract
): this {
this.connection = connection
return this
}

/**
* Reads the token using the lookup token id
*/
Expand Down
5 changes: 3 additions & 2 deletions src/UserProviders/Database/index.ts
Expand Up @@ -54,6 +54,7 @@ export class DatabaseProvider implements DatabaseProviderContract<DatabaseProvid
if (!this.connection) {
return this.db.connection(this.config.connection)
}

return typeof this.connection === 'string'
? this.db.connection(this.connection)
: this.connection
Expand Down Expand Up @@ -119,15 +120,15 @@ export class DatabaseProvider implements DatabaseProviderContract<DatabaseProvid
/**
* Define before hooks. Check interface for exact type information
*/
public before(event: string, callback: (query: any) => Promise<void>): this {
public before(event: 'findUser', callback: (query: any) => Promise<void>): this {
this.hooks.add('before', event, callback)
return this
}

/**
* Define after hooks. Check interface for exact type information
*/
public after(event: string, callback: (...args: any[]) => Promise<void>): this {
public after(event: 'findUser', callback: (...args: any[]) => Promise<void>): this {
this.hooks.add('after', event, callback)
return this
}
Expand Down
2 changes: 1 addition & 1 deletion src/UserProviders/Lucid/index.ts
Expand Up @@ -116,7 +116,7 @@ export class LucidProvider implements LucidProviderContract<LucidProviderModel>
/**
* Define after hooks. Check interface for exact type information
*/
public after(event: string, callback: (...args: any[]) => Promise<void>): this {
public after(event: 'findUser', callback: (...args: any[]) => Promise<void>): this {
this.hooks.add('after', event, callback)
return this
}
Expand Down
5 changes: 4 additions & 1 deletion test-helpers/index.ts
Expand Up @@ -133,7 +133,10 @@ export async function setupApplication(
`const redisConfig = {
connection: 'local',
connections: {
local: {}
local: {},
localDb1: {
db: '2'
}
}
}
export default redisConfig`
Expand Down
62 changes: 62 additions & 0 deletions test/token-providers/database.spec.ts
Expand Up @@ -48,6 +48,28 @@ test.group('Database Token Provider', (group) => {
assert.exists(tokenId)
})

test('use custom connection for persistance', async (assert) => {
const token = string.generateRandom(40)
const db = app.container.use('Adonis/Lucid/Database')
const provider = getTokensDbProvider(db)
provider.setConnection(db.connection('secondary'))

const tokenId = await provider.write({
name: 'Auth token',
tokenHash: token,
userId: '1',
type: 'api_token',
expiresAt: DateTime.local(),
})

assert.exists(tokenId)
const secondaryConnectionTokens = await db.connection('secondary').from('api_tokens')
const primaryConnectionTokens = await db.connection().from('api_tokens')

assert.lengthOf(secondaryConnectionTokens, 1)
assert.lengthOf(primaryConnectionTokens, 0)
})

test('read token from the database', async (assert) => {
const token = string.generateRandom(40)
const db = app.container.use('Adonis/Lucid/Database')
Expand All @@ -68,6 +90,27 @@ test.group('Database Token Provider', (group) => {
assert.exists(tokenRow!.expiresAt)
})

test('read token from a custom database connection', async (assert) => {
const token = string.generateRandom(40)
const db = app.container.use('Adonis/Lucid/Database')
const provider = getTokensDbProvider(db)
provider.setConnection(db.connection('secondary'))

const tokenId = await provider.write({
name: 'Auth token',
tokenHash: token,
userId: '1',
type: 'api_token',
expiresAt: DateTime.local().plus({ minutes: 30 }),
})

const tokenRow = await provider.read(tokenId, token, 'api_token')
assert.equal(tokenRow!.name, 'Auth token')
assert.equal(tokenRow!.tokenHash, token)
assert.equal(tokenRow!.type, 'api_token')
assert.exists(tokenRow!.expiresAt)
})

test('return null when there is a token hash mis-match', async (assert) => {
const token = string.generateRandom(40)
const db = app.container.use('Adonis/Lucid/Database')
Expand Down Expand Up @@ -150,4 +193,23 @@ test.group('Database Token Provider', (group) => {
const tokens = await db.from('api_tokens').select('*')
assert.lengthOf(tokens, 0)
})

test('delete token from a custom database connection', async (assert) => {
const token = string.generateRandom(40)
const db = app.container.use('Adonis/Lucid/Database')
const provider = getTokensDbProvider(db)
provider.setConnection(db.connection('secondary'))

const tokenId = await provider.write({
name: 'Auth token',
tokenHash: token,
userId: '1',
type: 'api_token',
expiresAt: DateTime.local(),
})

await provider.destroy(tokenId, 'api_token')
const tokens = await db.connection('secondary').from('api_tokens').select('*')
assert.lengthOf(tokens, 0)
})
})
66 changes: 66 additions & 0 deletions test/token-providers/redis.spec.ts
Expand Up @@ -58,6 +58,32 @@ test.group('Redis Token Provider', (group) => {
assert.isBelow(expiry, 2 * 24 * 3600 + 1)
})

test('save token to the database using a custom connection', async (assert) => {
const token = string.generateRandom(40)
const redis = app.container.use('Adonis/Addons/Redis')
const provider = getTokensRedisProvider(redis)
provider.setConnection(redis.connection('localDb1'))

const tokenId = await provider.write({
name: 'Auth token',
tokenHash: token,
userId: '1',
type: 'api_token',
expiresAt: DateTime.local().plus({ days: 2 }),
})

assert.exists(tokenId)
const tokenRow = JSON.parse((await redis.connection('localDb1').get(`api_token:${tokenId}`))!)
assert.deepEqual(tokenRow, {
user_id: '1',
name: 'Auth token',
token,
})

let expiry = await redis.connection('localDb1').ttl(tokenId)
assert.isBelow(expiry, 2 * 24 * 3600 + 1)
})

test('read token from the database', async (assert) => {
const token = string.generateRandom(40)
const redis = app.container.use('Adonis/Addons/Redis')
Expand All @@ -78,6 +104,27 @@ test.group('Redis Token Provider', (group) => {
assert.equal(tokenRow!.type, 'api_token')
})

test('read token from a custom database connection', async (assert) => {
const token = string.generateRandom(40)
const redis = app.container.use('Adonis/Addons/Redis')
const provider = getTokensRedisProvider(redis)
provider.setConnection(redis.connection('localDb1'))

const tokenId = await provider.write({
name: 'Auth token',
tokenHash: token,
userId: '1',
type: 'api_token',
expiresAt: DateTime.local().plus({ days: 2 }),
})

assert.exists(tokenId)
const tokenRow = await provider.read(tokenId, token, 'api_token')
assert.equal(tokenRow!.name, 'Auth token')
assert.equal(tokenRow!.tokenHash, token)
assert.equal(tokenRow!.type, 'api_token')
})

test('return null when there is a token hash mis-match', async (assert) => {
const token = string.generateRandom(40)
const redis = app.container.use('Adonis/Addons/Redis')
Expand Down Expand Up @@ -163,4 +210,23 @@ test.group('Redis Token Provider', (group) => {
const tokenRow = await redis.get(tokenId)
assert.isNull(tokenRow)
})

test('delete token using a custom connection', async (assert) => {
const token = string.generateRandom(40)
const redis = app.container.use('Adonis/Addons/Redis')
const provider = getTokensRedisProvider(redis)
provider.setConnection(redis.connection('localDb1'))

const tokenId = await provider.write({
name: 'Auth token',
tokenHash: token,
userId: '1',
type: 'api_token',
expiresAt: DateTime.local().plus({ seconds: 1 }),
})

await provider.destroy(tokenId, 'api_token')
const tokenRow = await redis.connection('localDb1').get(tokenId)
assert.isNull(tokenRow)
})
})

0 comments on commit ea71af5

Please sign in to comment.