Implement /oauth2/token endpoint; token auth middleware
This commit is contained in:
parent
36647a013d
commit
940d50b89c
41
src/auth/context/TokenSecurityContext.ts
Normal file
41
src/auth/context/TokenSecurityContext.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import {SecurityContext} from './SecurityContext'
|
||||||
|
import {AuthenticatableRepository} from '../types'
|
||||||
|
import {Awaitable} from '../../util'
|
||||||
|
import {Inject} from '../../di'
|
||||||
|
import {Request} from '../../http/lifecycle/Request'
|
||||||
|
import {OAuth2Token, TokenRepository} from '../server/types'
|
||||||
|
import {UserAuthenticationResumedEvent} from '../event/UserAuthenticationResumedEvent'
|
||||||
|
|
||||||
|
export class TokenSecurityContext extends SecurityContext {
|
||||||
|
@Inject()
|
||||||
|
protected readonly request!: Request
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
protected readonly tokens!: TokenRepository
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public readonly repository: AuthenticatableRepository,
|
||||||
|
) {
|
||||||
|
super(repository, 'token')
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
persist(): Awaitable<void> {}
|
||||||
|
|
||||||
|
async resume(): Promise<void> {
|
||||||
|
if ( !this.request.hasInstance(OAuth2Token) ) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const token: OAuth2Token = this.request.getExistingInstance(OAuth2Token)
|
||||||
|
if ( !token.userId ) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await this.repository.getByIdentifier(token.userId)
|
||||||
|
if ( user ) {
|
||||||
|
this.authenticatedUser = user
|
||||||
|
await this.bus.push(new UserAuthenticationResumedEvent(user, this))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,9 +2,11 @@ export * from './types'
|
|||||||
export * from './AuthenticatableAlreadyExistsError'
|
export * from './AuthenticatableAlreadyExistsError'
|
||||||
export * from './NotAuthorizedError'
|
export * from './NotAuthorizedError'
|
||||||
export * from './Authentication'
|
export * from './Authentication'
|
||||||
|
export * from './repository/AuthenticatableRepositoryFactory'
|
||||||
|
|
||||||
export * from './context/SecurityContext'
|
export * from './context/SecurityContext'
|
||||||
export * from './context/SessionSecurityContext'
|
export * from './context/SessionSecurityContext'
|
||||||
|
export * from './context/TokenSecurityContext'
|
||||||
|
|
||||||
export * from './event/AuthenticationEvent'
|
export * from './event/AuthenticationEvent'
|
||||||
export * from './event/UserAuthenticatedEvent'
|
export * from './event/UserAuthenticatedEvent'
|
||||||
@ -14,6 +16,8 @@ export * from './event/UserFlushedEvent'
|
|||||||
export * from './middleware/AuthRequiredMiddleware'
|
export * from './middleware/AuthRequiredMiddleware'
|
||||||
export * from './middleware/GuestRequiredMiddleware'
|
export * from './middleware/GuestRequiredMiddleware'
|
||||||
export * from './middleware/SessionAuthMiddleware'
|
export * from './middleware/SessionAuthMiddleware'
|
||||||
|
export * from './middleware/TokenAuthMiddleware'
|
||||||
|
export * from './middleware/ScopeRequiredMiddleware'
|
||||||
|
|
||||||
export * from './provider/basic/BasicLoginAttempt'
|
export * from './provider/basic/BasicLoginAttempt'
|
||||||
export * from './provider/basic/BasicLoginProvider'
|
export * from './provider/basic/BasicLoginProvider'
|
||||||
|
33
src/auth/middleware/ScopeRequiredMiddleware.ts
Normal file
33
src/auth/middleware/ScopeRequiredMiddleware.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import {Middleware} from '../../http/routing/Middleware'
|
||||||
|
import {ResponseObject} from '../../http/routing/Route'
|
||||||
|
import {OAuth2Token} from '../server/types'
|
||||||
|
import {HTTPError} from '../../http/HTTPError'
|
||||||
|
import {HTTPStatus, Pipeline} from '../../util'
|
||||||
|
import {Request} from '../../http/lifecycle/Request'
|
||||||
|
import {Constructable, Container} from '../../di'
|
||||||
|
|
||||||
|
export class ScopeRequiredMiddleware extends Middleware {
|
||||||
|
constructor(
|
||||||
|
request: Request,
|
||||||
|
protected readonly scope: string,
|
||||||
|
) {
|
||||||
|
super(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(): ResponseObject {
|
||||||
|
if ( !this.request.hasInstance(OAuth2Token) ) {
|
||||||
|
throw new HTTPError(HTTPStatus.UNAUTHORIZED, 'Must specify an OAuth2 token.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const token: OAuth2Token = this.request.getExistingInstance(OAuth2Token)
|
||||||
|
if ( typeof token.scope !== 'undefined' && token.scope !== this.scope ) {
|
||||||
|
throw new HTTPError(HTTPStatus.UNAUTHORIZED, 'Insufficient token permissions (requires: ' + this.scope + ')')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const scope = (name: string): Constructable<ScopeRequiredMiddleware> => {
|
||||||
|
return new Pipeline<Container, ScopeRequiredMiddleware>(
|
||||||
|
container => container.make(ScopeRequiredMiddleware, name),
|
||||||
|
)
|
||||||
|
}
|
@ -1,10 +1,8 @@
|
|||||||
import {Middleware} from '../../http/routing/Middleware'
|
import {Middleware} from '../../http/routing/Middleware'
|
||||||
import {Inject, Injectable, Instantiable} from '../../di'
|
import {Inject, Injectable} from '../../di'
|
||||||
import {Config} from '../../service/Config'
|
import {Config} from '../../service/Config'
|
||||||
import {Logging} from '../../service/Logging'
|
import {Logging} from '../../service/Logging'
|
||||||
import {AuthenticatableRepository} from '../types'
|
import {AuthenticatableRepository} from '../types'
|
||||||
import {Maybe} from '../../util'
|
|
||||||
import {AuthenticationConfig, isAuthenticationConfig} from '../config'
|
|
||||||
import {ResponseObject} from '../../http/routing/Route'
|
import {ResponseObject} from '../../http/routing/Route'
|
||||||
import {SessionSecurityContext} from '../context/SessionSecurityContext'
|
import {SessionSecurityContext} from '../context/SessionSecurityContext'
|
||||||
import {SecurityContext} from '../context/SecurityContext'
|
import {SecurityContext} from '../context/SecurityContext'
|
||||||
@ -23,22 +21,9 @@ export class SessionAuthMiddleware extends Middleware {
|
|||||||
|
|
||||||
async apply(): Promise<ResponseObject> {
|
async apply(): Promise<ResponseObject> {
|
||||||
this.logging.debug('Applying session auth middleware.')
|
this.logging.debug('Applying session auth middleware.')
|
||||||
const context = <SessionSecurityContext> this.make(SessionSecurityContext, this.getRepository())
|
const repo = <AuthenticatableRepository> this.make(AuthenticatableRepository)
|
||||||
|
const context = <SessionSecurityContext> this.make(SessionSecurityContext, repo)
|
||||||
this.request.registerSingletonInstance(SecurityContext, context)
|
this.request.registerSingletonInstance(SecurityContext, context)
|
||||||
await context.resume()
|
await context.resume()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Build the correct AuthenticatableRepository based on the auth config.
|
|
||||||
* @protected
|
|
||||||
*/
|
|
||||||
protected getRepository(): AuthenticatableRepository {
|
|
||||||
const config: Maybe<AuthenticationConfig> = this.config.get('auth')
|
|
||||||
if ( !isAuthenticationConfig(config) ) {
|
|
||||||
throw new TypeError('Invalid authentication config.')
|
|
||||||
}
|
|
||||||
|
|
||||||
const repo: Instantiable<AuthenticatableRepository> = config.storage
|
|
||||||
return this.make(repo)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
45
src/auth/middleware/TokenAuthMiddleware.ts
Normal file
45
src/auth/middleware/TokenAuthMiddleware.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import {Middleware} from '../../http/routing/Middleware'
|
||||||
|
import {Inject, Injectable} from '../../di'
|
||||||
|
import {Config} from '../../service/Config'
|
||||||
|
import {Logging} from '../../service/Logging'
|
||||||
|
import {AuthenticatableRepository} from '../types'
|
||||||
|
import {ResponseObject} from '../../http/routing/Route'
|
||||||
|
import {SecurityContext} from '../context/SecurityContext'
|
||||||
|
import {TokenSecurityContext} from '../context/TokenSecurityContext'
|
||||||
|
import {OAuth2Token, oauth2TokenString, TokenRepository} from '../server/types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injects a TokenSecurityContext into the request and attempts to
|
||||||
|
* resume the user's authentication.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class TokenAuthMiddleware extends Middleware {
|
||||||
|
@Inject()
|
||||||
|
protected readonly config!: Config
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
protected readonly logging!: Logging
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
protected readonly tokens!: TokenRepository
|
||||||
|
|
||||||
|
async apply(): Promise<ResponseObject> {
|
||||||
|
this.logging.debug('Applying token auth middleware.')
|
||||||
|
let tokenString = this.request.getHeader('Authorization')
|
||||||
|
if ( Array.isArray(tokenString) ) {
|
||||||
|
tokenString = tokenString[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( tokenString ) {
|
||||||
|
const token = await this.tokens.decode(oauth2TokenString(tokenString))
|
||||||
|
if ( token ) {
|
||||||
|
this.request.registerSingletonInstance(OAuth2Token, token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const repo = <AuthenticatableRepository> this.make(AuthenticatableRepository)
|
||||||
|
const context = <TokenSecurityContext> this.make(TokenSecurityContext, repo)
|
||||||
|
this.request.registerSingletonInstance(SecurityContext, context)
|
||||||
|
await context.resume()
|
||||||
|
}
|
||||||
|
}
|
73
src/auth/repository/AuthenticatableRepositoryFactory.ts
Normal file
73
src/auth/repository/AuthenticatableRepositoryFactory.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
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 {AuthenticatableRepository} from '../types'
|
||||||
|
import {ORMUserRepository} from './orm/ORMUserRepository'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A dependency injection factory that matches the abstract ClientRepository class
|
||||||
|
* and produces an instance of the configured repository driver implementation.
|
||||||
|
*/
|
||||||
|
@FactoryProducer()
|
||||||
|
export class AuthenticatableRepositoryFactory extends AbstractFactory<AuthenticatableRepository> {
|
||||||
|
protected get config(): Config {
|
||||||
|
return Container.getContainer().make<Config>(Config)
|
||||||
|
}
|
||||||
|
|
||||||
|
produce(): AuthenticatableRepository {
|
||||||
|
return new (this.getAuthenticatableRepositoryClass())()
|
||||||
|
}
|
||||||
|
|
||||||
|
match(something: unknown): boolean {
|
||||||
|
return something === AuthenticatableRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
getDependencyKeys(): Collection<DependencyRequirement> {
|
||||||
|
const meta = Reflect.getMetadata(DEPENDENCY_KEYS_METADATA_KEY, this.getAuthenticatableRepositoryClass())
|
||||||
|
if ( meta ) {
|
||||||
|
return meta
|
||||||
|
}
|
||||||
|
return new Collection<DependencyRequirement>()
|
||||||
|
}
|
||||||
|
|
||||||
|
getInjectedProperties(): Collection<PropertyDependency> {
|
||||||
|
const meta = new Collection<PropertyDependency>()
|
||||||
|
let currentToken = this.getAuthenticatableRepositoryClass()
|
||||||
|
|
||||||
|
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 user repository backend.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected getAuthenticatableRepositoryClass(): Instantiable<AuthenticatableRepository> {
|
||||||
|
const AuthenticatableRepositoryClass = this.config.get('auth.storage', ORMUserRepository)
|
||||||
|
|
||||||
|
if ( !isInstantiable(AuthenticatableRepositoryClass) || !(AuthenticatableRepositoryClass.prototype instanceof AuthenticatableRepository) ) {
|
||||||
|
const e = new ErrorWithContext('Provided client repository class does not extend from @extollo/lib.AuthenticatableRepository')
|
||||||
|
e.context = {
|
||||||
|
configKey: 'auth.storage',
|
||||||
|
class: AuthenticatableRepositoryClass.toString(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AuthenticatableRepositoryClass
|
||||||
|
}
|
||||||
|
}
|
@ -10,13 +10,24 @@ import {
|
|||||||
OAuth2Scope,
|
OAuth2Scope,
|
||||||
RedemptionCodeRepository,
|
RedemptionCodeRepository,
|
||||||
ScopeRepository,
|
ScopeRepository,
|
||||||
|
TokenRepository,
|
||||||
} from './types'
|
} from './types'
|
||||||
import {HTTPError} from '../../http/HTTPError'
|
import {HTTPError} from '../../http/HTTPError'
|
||||||
import {HTTPStatus, Maybe} from '../../util'
|
import {Awaitable, HTTPStatus, left, Maybe} from '../../util'
|
||||||
import {view} from '../../http/response/ViewResponseFactory'
|
import {view} from '../../http/response/ViewResponseFactory'
|
||||||
import {SecurityContext} from '../context/SecurityContext'
|
import {SecurityContext} from '../context/SecurityContext'
|
||||||
import {redirect} from '../../http/response/RedirectResponseFactory'
|
import {redirect} from '../../http/response/RedirectResponseFactory'
|
||||||
import {AuthRequiredMiddleware} from '../middleware/AuthRequiredMiddleware'
|
import {AuthRequiredMiddleware} from '../middleware/AuthRequiredMiddleware'
|
||||||
|
import {one} from '../../http/response/api'
|
||||||
|
import {AuthenticatableRepository} from '../types'
|
||||||
|
|
||||||
|
export enum GrantType {
|
||||||
|
Client = 'client_credentials',
|
||||||
|
Password = 'password',
|
||||||
|
Code = 'authorization_code',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const grantTypes: GrantType[] = [GrantType.Client, GrantType.Code, GrantType.Password]
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OAuth2Server extends Controller {
|
export class OAuth2Server extends Controller {
|
||||||
@ -33,13 +44,78 @@ export class OAuth2Server extends Controller {
|
|||||||
.passingRequest()
|
.passingRequest()
|
||||||
.calls<OAuth2Server>(OAuth2Server, x => x.authorizeAndRedirect)
|
.calls<OAuth2Server>(OAuth2Server, x => x.authorizeAndRedirect)
|
||||||
|
|
||||||
Route.post('/oauth2/redeem')
|
Route.post('/oauth2/token')
|
||||||
.alias('@oauth2:authorize:redeem')
|
.alias('@oauth2:token')
|
||||||
|
.parameterMiddleware<OAuth2Client>(async req =>
|
||||||
|
left(req.make<OAuth2Server>(OAuth2Server).getClientFromRequest(req)))
|
||||||
.passingRequest()
|
.passingRequest()
|
||||||
.calls<OAuth2Server>(OAuth2Server, x => x.redeemToken)
|
.calls<OAuth2Server>(OAuth2Server, x => x.issue)
|
||||||
}
|
}
|
||||||
|
|
||||||
async redeemToken(request: Request): Promise<ResponseObject> {
|
issue(request: Request, client: OAuth2Client): Awaitable<ResponseObject> {
|
||||||
|
const grant = request.safe('grant_type')
|
||||||
|
.in(grantTypes)
|
||||||
|
|
||||||
|
if ( grant === GrantType.Client ) {
|
||||||
|
return this.issueFromClient(request, client)
|
||||||
|
} else if ( grant === GrantType.Code ) {
|
||||||
|
return this.issueFromCode(request, client)
|
||||||
|
} else if ( grant === GrantType.Password ) {
|
||||||
|
return this.issueFromCredential(request, client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async issueFromCredential(request: Request, client: OAuth2Client): Promise<ResponseObject> {
|
||||||
|
const scope = String(this.request.input('scope') ?? '') || undefined
|
||||||
|
const username = this.request.safe('username').string()
|
||||||
|
const password = this.request.safe('password').string()
|
||||||
|
|
||||||
|
const userRepo = <AuthenticatableRepository> request.make(AuthenticatableRepository)
|
||||||
|
const user = await userRepo.getByIdentifier(username)
|
||||||
|
if ( !user || !(await user.validateCredential(password)) ) {
|
||||||
|
throw new HTTPError(HTTPStatus.BAD_REQUEST)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenRepo = <TokenRepository> request.make(TokenRepository)
|
||||||
|
const token = await tokenRepo.issue(user, client, scope)
|
||||||
|
return one({
|
||||||
|
token: await tokenRepo.encode(token),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async issueFromCode(request: Request, client: OAuth2Client): Promise<ResponseObject> {
|
||||||
|
const scope = String(this.request.input('scope') ?? '') || undefined
|
||||||
|
const codeRepo = <RedemptionCodeRepository> request.make(RedemptionCodeRepository)
|
||||||
|
const codeString = request.safe('code').string()
|
||||||
|
const code = await codeRepo.find(codeString)
|
||||||
|
if ( !code ) {
|
||||||
|
throw new HTTPError(HTTPStatus.BAD_REQUEST)
|
||||||
|
}
|
||||||
|
|
||||||
|
const userRepo = <AuthenticatableRepository> request.make(AuthenticatableRepository)
|
||||||
|
const user = await userRepo.getByIdentifier(code.userId)
|
||||||
|
if ( !user ) {
|
||||||
|
throw new HTTPError(HTTPStatus.BAD_REQUEST)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenRepo = <TokenRepository> request.make(TokenRepository)
|
||||||
|
const token = await tokenRepo.issue(user, client, scope)
|
||||||
|
return one({
|
||||||
|
token: await tokenRepo.encode(token),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async issueFromClient(request: Request, client: OAuth2Client): Promise<ResponseObject> {
|
||||||
|
const scope = String(this.request.input('scope') ?? '') || undefined
|
||||||
|
|
||||||
|
const tokenRepo = <TokenRepository> request.make(TokenRepository)
|
||||||
|
const token = await tokenRepo.issue(undefined, client, scope)
|
||||||
|
return one({
|
||||||
|
token: await tokenRepo.encode(token),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async getClientFromRequest(request: Request): Promise<OAuth2Client> {
|
||||||
const authParts = String(request.getHeader('Authorization')).split(':')
|
const authParts = String(request.getHeader('Authorization')).split(':')
|
||||||
if ( authParts.length !== 2 ) {
|
if ( authParts.length !== 2 ) {
|
||||||
throw new HTTPError(HTTPStatus.BAD_REQUEST)
|
throw new HTTPError(HTTPStatus.BAD_REQUEST)
|
||||||
@ -52,22 +128,13 @@ export class OAuth2Server extends Controller {
|
|||||||
throw new HTTPError(HTTPStatus.UNAUTHORIZED)
|
throw new HTTPError(HTTPStatus.UNAUTHORIZED)
|
||||||
}
|
}
|
||||||
|
|
||||||
const codeRepo = <RedemptionCodeRepository> request.make(RedemptionCodeRepository)
|
return client
|
||||||
const codeString = request.safe('code').string()
|
|
||||||
const code = await codeRepo.find(codeString)
|
|
||||||
if ( !code ) {
|
|
||||||
throw new HTTPError(HTTPStatus.BAD_REQUEST)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async authorizeAndRedirect(request: Request): Promise<ResponseObject> {
|
async authorizeAndRedirect(request: Request): Promise<ResponseObject> {
|
||||||
// Look up the client in the client repo
|
// Look up the client in the client repo
|
||||||
const session = <Session> request.make(Session)
|
const session = <Session> request.make(Session)
|
||||||
const clientId = session.safe('oauth2.authorize.clientId').string()
|
const client = await this.getClientFromRequest(request)
|
||||||
const client = await this.getClient(request, clientId)
|
|
||||||
|
|
||||||
const flowType = session.safe('oauth2.authorize.flow').in(client.allowedFlows)
|
const flowType = session.safe('oauth2.authorize.flow').in(client.allowedFlows)
|
||||||
if ( flowType === OAuth2FlowType.code ) {
|
if ( flowType === OAuth2FlowType.code ) {
|
||||||
return this.authorizeCodeFlow(request, client)
|
return this.authorizeCodeFlow(request, client)
|
||||||
|
@ -20,17 +20,19 @@ export class ORMTokenRepository extends TokenRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async issue(user: Authenticatable, client: OAuth2Client, scope?: string): Promise<OAuth2Token> {
|
async issue(user: Authenticatable|undefined, client: OAuth2Client, scope?: string): Promise<OAuth2Token> {
|
||||||
const expiration = this.config.safe('outh2.token.lifetimeSeconds')
|
const expiration = this.config.safe('outh2.token.lifetimeSeconds')
|
||||||
.or(60 * 60 * 6)
|
.or(60 * 60 * 6)
|
||||||
.integer() * 1000
|
.integer() * 1000
|
||||||
|
|
||||||
const token = new OAuth2TokenModel()
|
const token = new OAuth2TokenModel()
|
||||||
token.scope = scope
|
token.scope = scope
|
||||||
token.userId = String(user.getUniqueIdentifier())
|
|
||||||
token.clientId = client.id
|
token.clientId = client.id
|
||||||
token.issued = new Date()
|
token.issued = new Date()
|
||||||
token.expires = new Date(Math.floor(Date.now() + expiration))
|
token.expires = new Date(Math.floor(Date.now() + expiration))
|
||||||
|
if ( user ) {
|
||||||
|
token.userId = String(user.getUniqueIdentifier())
|
||||||
|
}
|
||||||
await token.save()
|
await token.save()
|
||||||
|
|
||||||
return token
|
return token
|
||||||
@ -40,10 +42,10 @@ export class ORMTokenRepository extends TokenRepository {
|
|||||||
const secret = this.config.safe('oauth2.secret').string()
|
const secret = this.config.safe('oauth2.secret').string()
|
||||||
const payload = {
|
const payload = {
|
||||||
id: token.id,
|
id: token.id,
|
||||||
userId: token.userId,
|
|
||||||
clientId: token.clientId,
|
clientId: token.clientId,
|
||||||
iat: Math.floor(token.issued.valueOf() / 1000),
|
iat: Math.floor(token.issued.valueOf() / 1000),
|
||||||
exp: Math.floor(token.expires.valueOf() / 1000),
|
exp: Math.floor(token.expires.valueOf() / 1000),
|
||||||
|
...(token.userId ? { userId: token.userId } : {}),
|
||||||
...(token.scope ? { scope: token.scope } : {}),
|
...(token.scope ? { scope: token.scope } : {}),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,10 +76,10 @@ export class ORMTokenRepository extends TokenRepository {
|
|||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
id: decoded.id,
|
id: decoded.id,
|
||||||
userId: decoded.userId,
|
|
||||||
clientId: decoded.clientId,
|
clientId: decoded.clientId,
|
||||||
issued: new Date(decoded.iat * 1000),
|
issued: new Date(decoded.iat * 1000),
|
||||||
expires: new Date(decoded.exp * 1000),
|
expires: new Date(decoded.exp * 1000),
|
||||||
|
...(decoded.userId ? { userId: decoded.userId } : {}),
|
||||||
...(decoded.scope ? { scope: decoded.scope } : {}),
|
...(decoded.scope ? { scope: decoded.scope } : {}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,13 +83,19 @@ export abstract class ScopeRepository {
|
|||||||
abstract findByName(name: string): Awaitable<Maybe<OAuth2Scope>>
|
abstract findByName(name: string): Awaitable<Maybe<OAuth2Scope>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OAuth2Token {
|
export abstract class OAuth2Token {
|
||||||
id: string
|
abstract id: string
|
||||||
userId: AuthenticatableIdentifier
|
|
||||||
clientId: string
|
/** When undefined, these are client credentials. */
|
||||||
issued: Date
|
abstract userId?: AuthenticatableIdentifier
|
||||||
expires: Date
|
|
||||||
scope?: string
|
abstract clientId: string
|
||||||
|
|
||||||
|
abstract issued: Date
|
||||||
|
|
||||||
|
abstract expires: Date
|
||||||
|
|
||||||
|
abstract scope?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OAuth2TokenString = TypeTag<'@extollo/lib.OAuth2TokenString'> & string
|
export type OAuth2TokenString = TypeTag<'@extollo/lib.OAuth2TokenString'> & string
|
||||||
@ -105,7 +111,6 @@ export function isOAuth2Token(what: unknown): what is OAuth2Token {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!hasOwnProperty(what, 'id')
|
!hasOwnProperty(what, 'id')
|
||||||
|| !hasOwnProperty(what, 'userId')
|
|
||||||
|| !hasOwnProperty(what, 'clientId')
|
|| !hasOwnProperty(what, 'clientId')
|
||||||
|| !hasOwnProperty(what, 'issued')
|
|| !hasOwnProperty(what, 'issued')
|
||||||
|| !hasOwnProperty(what, 'expires')
|
|| !hasOwnProperty(what, 'expires')
|
||||||
@ -115,7 +120,7 @@ export function isOAuth2Token(what: unknown): what is OAuth2Token {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
typeof what.id !== 'string'
|
typeof what.id !== 'string'
|
||||||
|| !(typeof what.userId === 'string' || typeof what.userId === 'number')
|
|| (hasOwnProperty(what, 'userId') && !(typeof what.userId === 'string' || typeof what.userId === 'number'))
|
||||||
|| typeof what.clientId !== 'string'
|
|| typeof what.clientId !== 'string'
|
||||||
|| !(what.issued instanceof Date)
|
|| !(what.issued instanceof Date)
|
||||||
|| !(what.expires instanceof Date)
|
|| !(what.expires instanceof Date)
|
||||||
@ -129,7 +134,7 @@ export function isOAuth2Token(what: unknown): what is OAuth2Token {
|
|||||||
export abstract class TokenRepository {
|
export abstract class TokenRepository {
|
||||||
abstract find(id: string): Awaitable<Maybe<OAuth2Token>>
|
abstract find(id: string): Awaitable<Maybe<OAuth2Token>>
|
||||||
|
|
||||||
abstract issue(user: Authenticatable, client: OAuth2Client, scope?: string): Awaitable<OAuth2Token>
|
abstract issue(user: Authenticatable|undefined, client: OAuth2Client, scope?: string): Awaitable<OAuth2Token>
|
||||||
|
|
||||||
abstract decode(token: OAuth2TokenString): Awaitable<Maybe<OAuth2Token>>
|
abstract decode(token: OAuth2TokenString): Awaitable<Maybe<OAuth2Token>>
|
||||||
|
|
||||||
|
@ -109,6 +109,20 @@ export class Response {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a header from the response by name.
|
||||||
|
* @param name
|
||||||
|
*/
|
||||||
|
public unsetHeader(name: string): this {
|
||||||
|
this.logging.verbose(`Will unset header on response: ${name}`)
|
||||||
|
if ( this.sentHeaders ) {
|
||||||
|
throw new HeadersAlreadySentError(this, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete this.headers[name]
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bulk set the specified headers in the response.
|
* Bulk set the specified headers in the response.
|
||||||
* @param data
|
* @param data
|
||||||
|
@ -372,21 +372,37 @@ export class Route<TReturn extends ResponseObject, THandlerParams extends unknow
|
|||||||
return this as HandledRoute<TReturn, THandlerParams>
|
return this as HandledRoute<TReturn, THandlerParams>
|
||||||
}
|
}
|
||||||
|
|
||||||
public pre(middleware: Instantiable<Middleware>): this {
|
public pre(middleware: Instantiable<Middleware>|Constructable<Middleware>): this {
|
||||||
|
let name: string
|
||||||
|
if ( middleware instanceof Pipeline ) {
|
||||||
|
this.preflight.prepend(request => middleware.apply(request).apply())
|
||||||
|
name = '(unknown pipeline)'
|
||||||
|
} else {
|
||||||
this.preflight.prepend(request => request.make<Middleware>(middleware, request).apply())
|
this.preflight.prepend(request => request.make<Middleware>(middleware, request).apply())
|
||||||
|
name = middleware.name
|
||||||
|
}
|
||||||
|
|
||||||
this.displays.push({
|
this.displays.push({
|
||||||
stage: 'pre',
|
stage: 'pre',
|
||||||
display: `${middleware.name}`,
|
display: name,
|
||||||
})
|
})
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
public post(middleware: Instantiable<Middleware>): this {
|
public post(middleware: Instantiable<Middleware>|Constructable<Middleware>): this {
|
||||||
this.postflight.push(request => request.make<Middleware>(middleware, request).apply())
|
let name: string
|
||||||
|
if ( middleware instanceof Pipeline ) {
|
||||||
|
this.postflight.push(request => middleware.apply(request).apply())
|
||||||
|
name = '(unknown pipeline)'
|
||||||
|
} else {
|
||||||
|
this.preflight.push(request => request.make<Middleware>(middleware, request).apply())
|
||||||
|
name = middleware.name
|
||||||
|
}
|
||||||
|
|
||||||
this.displays.push({
|
this.displays.push({
|
||||||
stage: 'pre',
|
stage: 'post',
|
||||||
display: `${middleware.name}`,
|
display: name,
|
||||||
})
|
})
|
||||||
|
|
||||||
return this
|
return this
|
||||||
|
Loading…
Reference in New Issue
Block a user