-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(user-tokens): implement user tokens (#203)
* user token start * tests * refactor auth * caching * api * fix test * comment * expiry * batching * remove user token implementation * Revert "remove user token implementation" This reverts commit 1e1dd78. * refact auth * remove file * fix * refact client auth function * refact function * refact * fix * remove file * bring changes * remove user token api * revert * remove correct user token api * bring changes * extract constant * verifyToken * fix tests * disable server if no expiremental socket enabled * fix test * remove dead code
- Loading branch information
1 parent
877d448
commit c27fb4d
Showing
8 changed files
with
326 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
import { uuid } from '@botpress/messaging-base' | ||
import crypto from 'crypto' | ||
import { v4 as uuidv4 } from 'uuid' | ||
import yn from 'yn' | ||
import { Service } from '../base/service' | ||
import { Batcher } from '../batching/batcher' | ||
import { BatchingService } from '../batching/service' | ||
import { ServerCache } from '../caching/cache' | ||
import { CachingService } from '../caching/service' | ||
import { CryptoService } from '../crypto/service' | ||
import { DatabaseService } from '../database/service' | ||
import { UserService } from '../users/service' | ||
import { UserTokenTable } from './table' | ||
import { UserToken } from './types' | ||
|
||
export const USER_TOKEN_LENGTH = 66 | ||
|
||
export class UserTokenService extends Service { | ||
public batcher!: Batcher<UserToken> | ||
|
||
private table: UserTokenTable | ||
private cacheById!: ServerCache<uuid, UserToken> | ||
private cacheTokens!: ServerCache<uuid, string> | ||
|
||
constructor( | ||
private db: DatabaseService, | ||
private cryptoService: CryptoService, | ||
private cachingService: CachingService, | ||
private batchingService: BatchingService, | ||
private userService: UserService | ||
) { | ||
super() | ||
this.table = new UserTokenTable() | ||
} | ||
|
||
async setup() { | ||
if (!yn(process.env.ENABLE_EXPERIMENTAL_SOCKETS)) { | ||
// let's not create a table we don't need for now | ||
return | ||
} | ||
|
||
this.batcher = await this.batchingService.newBatcher( | ||
'batcher_user_tokens', | ||
[this.userService.batcher], | ||
this.handleBatchFlush.bind(this) | ||
) | ||
|
||
this.cacheById = await this.cachingService.newServerCache('cache_user_token_by_id') | ||
this.cacheTokens = await this.cachingService.newServerCache('cache_user_token_raw') | ||
|
||
await this.db.registerTable(this.table) | ||
} | ||
|
||
private async handleBatchFlush(batch: UserToken[]) { | ||
const rows = batch.map((x) => this.serialize(x)) | ||
await this.query().insert(rows) | ||
} | ||
|
||
async generateToken(): Promise<string> { | ||
return crypto.randomBytes(USER_TOKEN_LENGTH).toString('base64') | ||
} | ||
|
||
async create(userId: uuid, token: string, expiry: Date | undefined): Promise<UserToken> { | ||
const userToken = { | ||
id: uuidv4(), | ||
userId, | ||
token: await this.cryptoService.hash(token), | ||
expiry | ||
} | ||
|
||
await this.batcher.push(userToken) | ||
this.cacheById.set(userToken.id, userToken) | ||
|
||
return userToken | ||
} | ||
|
||
async getById(id: uuid): Promise<UserToken | undefined> { | ||
const cached = this.cacheById.get(id) | ||
if (cached) { | ||
return cached | ||
} | ||
|
||
await this.batcher.flush() | ||
const rows = await this.query().where({ id }) | ||
|
||
if (rows?.length) { | ||
const userToken = this.deserialize(rows[0]) | ||
this.cacheById.set(id, userToken) | ||
return userToken | ||
} else { | ||
return undefined | ||
} | ||
} | ||
|
||
async verifyToken(id: string, token: string): Promise<UserToken | undefined> { | ||
const userToken = await this.getById(id) | ||
if (!userToken) { | ||
return undefined | ||
} | ||
|
||
if (userToken.expiry && Date.now() > userToken.expiry.getTime()) { | ||
return undefined | ||
} | ||
|
||
const cachedToken = this.cacheTokens.get(id) | ||
if (cachedToken) { | ||
if (token === cachedToken) { | ||
return userToken | ||
} else { | ||
return undefined | ||
} | ||
} | ||
|
||
if (await this.cryptoService.compareHash(userToken.token, token)) { | ||
this.cacheTokens.set(id, token) | ||
return userToken | ||
} else { | ||
return undefined | ||
} | ||
} | ||
|
||
public query() { | ||
return this.db.knex(this.table.id) | ||
} | ||
|
||
private serialize(userToken: Partial<UserToken>) { | ||
return { | ||
...userToken, | ||
expiry: this.db.setDate(userToken.expiry) | ||
} | ||
} | ||
|
||
private deserialize(userToken: any): UserToken { | ||
return { | ||
...userToken, | ||
expiry: userToken.expiry ? this.db.getDate(userToken.expiry) : undefined | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { Knex } from 'knex' | ||
import { Table } from '../base/table' | ||
|
||
export class UserTokenTable extends Table { | ||
get id() { | ||
return 'msg_user_tokens' | ||
} | ||
|
||
create(table: Knex.CreateTableBuilder) { | ||
table.uuid('id').primary() | ||
table.uuid('userId').references('id').inTable('msg_users') | ||
table.string('token').notNullable() | ||
table.timestamp('expiry').nullable() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { uuid } from '@botpress/messaging-base' | ||
|
||
export interface UserToken { | ||
id: uuid | ||
userId: uuid | ||
token: string | ||
expiry: Date | undefined | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.