31 changed files with 736 additions and 52 deletions
@ -0,0 +1,30 @@ |
|||
import {Field, FieldType, Model} from '../../../orm' |
|||
import {OAuth2Token} from '../types' |
|||
|
|||
export class OAuth2TokenModel extends Model<OAuth2TokenModel> implements OAuth2Token { |
|||
public static table = 'oauth2_tokens' |
|||
|
|||
public static key = 'oauth2_token_id' |
|||
|
|||
@Field(FieldType.serial, 'oauth2_token_id') |
|||
protected oauth2TokenId!: number |
|||
|
|||
public get id(): string { |
|||
return String(this.oauth2TokenId) |
|||
} |
|||
|
|||
@Field(FieldType.varchar, 'user_id') |
|||
public userId!: string |
|||
|
|||
@Field(FieldType.varchar, 'client_id') |
|||
public clientId!: string |
|||
|
|||
@Field(FieldType.timestamp) |
|||
public issued!: Date |
|||
|
|||
@Field(FieldType.timestamp) |
|||
public expires!: Date |
|||
|
|||
@Field(FieldType.varchar) |
|||
public scope?: string |
|||
} |
@ -0,0 +1,33 @@ |
|||
import {isOAuth2RedemptionCode, OAuth2Client, OAuth2RedemptionCode, RedemptionCodeRepository} from '../types' |
|||
import {Inject, Injectable} from '../../../di' |
|||
import {Cache, Maybe, uuid4} from '../../../util' |
|||
import {Authenticatable} from '../../types' |
|||
|
|||
@Injectable() |
|||
export class CacheRedemptionCodeRepository extends RedemptionCodeRepository { |
|||
@Inject() |
|||
protected readonly cache!: Cache |
|||
|
|||
async find(codeString: string): Promise<Maybe<OAuth2RedemptionCode>> { |
|||
const cacheKey = `@extollo:oauth2:redemption:${codeString}` |
|||
if ( await this.cache.has(cacheKey) ) { |
|||
const code = await this.cache.safe(cacheKey).then(x => x.json()) |
|||
if ( isOAuth2RedemptionCode(code) ) { |
|||
return code |
|||
} |
|||
} |
|||
} |
|||
|
|||
async issue(user: Authenticatable, client: OAuth2Client, scope?: string): Promise<OAuth2RedemptionCode> { |
|||
const code = { |
|||
scope, |
|||
clientId: client.id, |
|||
userId: user.getUniqueIdentifier(), |
|||
code: uuid4(), |
|||
} |
|||
|
|||
const cacheKey = `@extollo:oauth2:redemption:${code.code}` |
|||
await this.cache.put(cacheKey, JSON.stringify(code)) |
|||
return code |
|||
} |
|||
} |
@ -0,0 +1,88 @@ |
|||
import {isOAuth2Token, OAuth2Client, OAuth2Token, oauth2TokenString, OAuth2TokenString, TokenRepository} from '../types' |
|||
import {Inject, Injectable} from '../../../di' |
|||
import {Maybe} from '../../../util' |
|||
import {OAuth2TokenModel} from '../models/OAuth2TokenModel' |
|||
import {Config} from '../../../service/Config' |
|||
import * as jwt from 'jsonwebtoken' |
|||
import {Authenticatable} from '../../types' |
|||
|
|||
@Injectable() |
|||
export class ORMTokenRepository extends TokenRepository { |
|||
@Inject() |
|||
protected readonly config!: Config |
|||
|
|||
async find(id: string): Promise<Maybe<OAuth2Token>> { |
|||
const idNum = parseInt(id, 10) |
|||
if ( !isNaN(idNum) ) { |
|||
return OAuth2TokenModel.query<OAuth2TokenModel>() |
|||
.whereKey(idNum) |
|||
.first() |
|||
} |
|||
} |
|||
|
|||
async issue(user: Authenticatable, client: OAuth2Client, scope?: string): Promise<OAuth2Token> { |
|||
const expiration = this.config.safe('outh2.token.lifetimeSeconds') |
|||
.or(60 * 60 * 6) |
|||
.integer() * 1000 |
|||
|
|||
const token = new OAuth2TokenModel() |
|||
token.scope = scope |
|||
token.userId = String(user.getUniqueIdentifier()) |
|||
token.clientId = client.id |
|||
token.issued = new Date() |
|||
token.expires = new Date(Math.floor(Date.now() + expiration)) |
|||
await token.save() |
|||
|
|||
return token |
|||
} |
|||
|
|||
async encode(token: OAuth2Token): Promise<OAuth2TokenString> { |
|||
const secret = this.config.safe('oauth2.secret').string() |
|||
const payload = { |
|||
id: token.id, |
|||
userId: token.userId, |
|||
clientId: token.clientId, |
|||
iat: Math.floor(token.issued.valueOf() / 1000), |
|||
exp: Math.floor(token.expires.valueOf() / 1000), |
|||
...(token.scope ? { scope: token.scope } : {}), |
|||
} |
|||
|
|||
const generated = await new Promise<string>((res, rej) => { |
|||
jwt.sign(payload, secret, {}, (err, gen) => { |
|||
if (err || err === null || !gen) { |
|||
rej(err || new Error('Unable to encode JWT.')) |
|||
} else { |
|||
res(gen) |
|||
} |
|||
}) |
|||
}) |
|||
|
|||
return oauth2TokenString(generated) |
|||
} |
|||
|
|||
async decode(token: OAuth2TokenString): Promise<Maybe<OAuth2Token>> { |
|||
const secret = this.config.safe('oauth2.secret').string() |
|||
const decoded = await new Promise<any>((res, rej) => { |
|||
jwt.verify(token, secret, {}, (err, payload) => { |
|||
if ( err ) { |
|||
rej(err) |
|||
} else { |
|||
res(payload) |
|||
} |
|||
}) |
|||
}) |
|||
|
|||
const value = { |
|||
id: decoded.id, |
|||
userId: decoded.userId, |
|||
clientId: decoded.clientId, |
|||
issued: new Date(decoded.iat * 1000), |
|||
expires: new Date(decoded.exp * 1000), |
|||
...(decoded.scope ? { scope: decoded.scope } : {}), |
|||
} |
|||
|
|||
if ( isOAuth2Token(value) ) { |
|||
return value |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,74 @@ |
|||
import { |
|||
AbstractFactory, |
|||
Container, |
|||
DependencyRequirement, |
|||
PropertyDependency, |
|||
isInstantiable, |
|||
DEPENDENCY_KEYS_METADATA_KEY, |
|||
DEPENDENCY_KEYS_PROPERTY_METADATA_KEY, Instantiable, FactoryProducer, |
|||
} from '../../../di' |
|||
import {Collection, ErrorWithContext} from '../../../util' |
|||
import {Config} from '../../../service/Config' |
|||
import {RedemptionCodeRepository} from '../types' |
|||
import {CacheRedemptionCodeRepository} from './CacheRedemptionCodeRepository' |
|||
|
|||
/** |
|||
* A dependency injection factory that matches the abstract RedemptionCodeRepository class |
|||
* and produces an instance of the configured repository driver implementation. |
|||
*/ |
|||
@FactoryProducer() |
|||
export class RedemptionCodeRepositoryFactory extends AbstractFactory<RedemptionCodeRepository> { |
|||
protected get config(): Config { |
|||
return Container.getContainer().make<Config>(Config) |
|||
} |
|||
|
|||
produce(): RedemptionCodeRepository { |
|||
return new (this.getRedemptionCodeRepositoryClass())() |
|||
} |
|||
|
|||
match(something: unknown): boolean { |
|||
return something === RedemptionCodeRepository |
|||
} |
|||
|
|||
getDependencyKeys(): Collection<DependencyRequirement> { |
|||
const meta = Reflect.getMetadata(DEPENDENCY_KEYS_METADATA_KEY, this.getRedemptionCodeRepositoryClass()) |
|||
if ( meta ) { |
|||
return meta |
|||
} |
|||
return new Collection<DependencyRequirement>() |
|||
} |
|||
|
|||
getInjectedProperties(): Collection<PropertyDependency> { |
|||
const meta = new Collection<PropertyDependency>() |
|||
let currentToken = this.getRedemptionCodeRepositoryClass() |
|||
|
|||
do { |
|||
const loadedMeta = Reflect.getMetadata(DEPENDENCY_KEYS_PROPERTY_METADATA_KEY, currentToken) |
|||
if ( loadedMeta ) { |
|||
meta.concat(loadedMeta) |
|||
} |
|||
currentToken = Object.getPrototypeOf(currentToken) |
|||
} while (Object.getPrototypeOf(currentToken) !== Function.prototype && Object.getPrototypeOf(currentToken) !== Object.prototype) |
|||
|
|||
return meta |
|||
} |
|||
|
|||
/** |
|||
* Return the instantiable class of the configured client repository backend. |
|||
* @protected |
|||
* @return Instantiable<RedemptionCodeRepository> |
|||
*/ |
|||
protected getRedemptionCodeRepositoryClass(): Instantiable<RedemptionCodeRepository> { |
|||
const RedemptionCodeRepositoryClass = this.config.get('oauth2.repository.client', CacheRedemptionCodeRepository) |
|||
|
|||
if ( !isInstantiable(RedemptionCodeRepositoryClass) || !(RedemptionCodeRepositoryClass.prototype instanceof RedemptionCodeRepository) ) { |
|||
const e = new ErrorWithContext('Provided client repository class does not extend from @extollo/lib.RedemptionCodeRepository') |
|||
e.context = { |
|||
configKey: 'oauth2.repository.client', |
|||
class: RedemptionCodeRepositoryClass.toString(), |
|||
} |
|||
} |
|||
|
|||
return RedemptionCodeRepositoryClass |
|||
} |
|||
} |
@ -0,0 +1,74 @@ |
|||
import { |
|||
AbstractFactory, |
|||
Container, |
|||
DependencyRequirement, |
|||
PropertyDependency, |
|||
isInstantiable, |
|||
DEPENDENCY_KEYS_METADATA_KEY, |
|||
DEPENDENCY_KEYS_PROPERTY_METADATA_KEY, Instantiable, FactoryProducer, |
|||
} from '../../../di' |
|||
import {Collection, ErrorWithContext} from '../../../util' |
|||
import {Config} from '../../../service/Config' |
|||
import {TokenRepository} from '../types' |
|||
import {ORMTokenRepository} from './ORMTokenRepository' |
|||
|
|||
/** |
|||
* A dependency injection factory that matches the abstract TokenRepository class |
|||
* and produces an instance of the configured repository driver implementation. |
|||
*/ |
|||
@FactoryProducer() |
|||
export class TokenRepositoryFactory extends AbstractFactory<TokenRepository> { |
|||
protected get config(): Config { |
|||
return Container.getContainer().make<Config>(Config) |
|||
} |
|||
|
|||
produce(): TokenRepository { |
|||
return new (this.getTokenRepositoryClass())() |
|||
} |
|||
|
|||
match(something: unknown): boolean { |
|||
return something === TokenRepository |
|||
} |
|||
|
|||
getDependencyKeys(): Collection<DependencyRequirement> { |
|||
const meta = Reflect.getMetadata(DEPENDENCY_KEYS_METADATA_KEY, this.getTokenRepositoryClass()) |
|||
if ( meta ) { |
|||
return meta |
|||
} |
|||
return new Collection<DependencyRequirement>() |
|||
} |
|||
|
|||
getInjectedProperties(): Collection<PropertyDependency> { |
|||
const meta = new Collection<PropertyDependency>() |
|||
let currentToken = this.getTokenRepositoryClass() |
|||
|
|||
do { |
|||
const loadedMeta = Reflect.getMetadata(DEPENDENCY_KEYS_PROPERTY_METADATA_KEY, currentToken) |
|||
if ( loadedMeta ) { |
|||
meta.concat(loadedMeta) |
|||
} |
|||
currentToken = Object.getPrototypeOf(currentToken) |
|||
} while (Object.getPrototypeOf(currentToken) !== Function.prototype && Object.getPrototypeOf(currentToken) !== Object.prototype) |
|||
|
|||
return meta |
|||
} |
|||
|
|||
/** |
|||
* Return the instantiable class of the configured token repository backend. |
|||
* @protected |
|||
* @return Instantiable<TokenRepository> |
|||
*/ |
|||
protected getTokenRepositoryClass(): Instantiable<TokenRepository> { |
|||
const TokenRepositoryClass = this.config.get('oauth2.repository.token', ORMTokenRepository) |
|||
|
|||
if ( !isInstantiable(TokenRepositoryClass) || !(TokenRepositoryClass.prototype instanceof TokenRepository) ) { |
|||
const e = new ErrorWithContext('Provided token repository class does not extend from @extollo/lib.TokenRepository') |
|||
e.context = { |
|||
configKey: 'oauth2.repository.client', |
|||
class: TokenRepositoryClass.toString(), |
|||
} |
|||
} |
|||
|
|||
return TokenRepositoryClass |
|||
} |
|||
} |
Loading…
Reference in new issue