parent
a039b1ff25
commit
8f08b94f74
@ -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