Implement /oauth2/token endpoint; token auth middleware
This commit is contained in:
@@ -10,13 +10,24 @@ import {
|
||||
OAuth2Scope,
|
||||
RedemptionCodeRepository,
|
||||
ScopeRepository,
|
||||
TokenRepository,
|
||||
} from './types'
|
||||
import {HTTPError} from '../../http/HTTPError'
|
||||
import {HTTPStatus, Maybe} from '../../util'
|
||||
import {Awaitable, HTTPStatus, left, Maybe} from '../../util'
|
||||
import {view} from '../../http/response/ViewResponseFactory'
|
||||
import {SecurityContext} from '../context/SecurityContext'
|
||||
import {redirect} from '../../http/response/RedirectResponseFactory'
|
||||
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()
|
||||
export class OAuth2Server extends Controller {
|
||||
@@ -33,13 +44,78 @@ export class OAuth2Server extends Controller {
|
||||
.passingRequest()
|
||||
.calls<OAuth2Server>(OAuth2Server, x => x.authorizeAndRedirect)
|
||||
|
||||
Route.post('/oauth2/redeem')
|
||||
.alias('@oauth2:authorize:redeem')
|
||||
Route.post('/oauth2/token')
|
||||
.alias('@oauth2:token')
|
||||
.parameterMiddleware<OAuth2Client>(async req =>
|
||||
left(req.make<OAuth2Server>(OAuth2Server).getClientFromRequest(req)))
|
||||
.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(':')
|
||||
if ( authParts.length !== 2 ) {
|
||||
throw new HTTPError(HTTPStatus.BAD_REQUEST)
|
||||
@@ -52,22 +128,13 @@ export class OAuth2Server extends Controller {
|
||||
throw new HTTPError(HTTPStatus.UNAUTHORIZED)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
async authorizeAndRedirect(request: Request): Promise<ResponseObject> {
|
||||
// Look up the client in the client repo
|
||||
const session = <Session> request.make(Session)
|
||||
const clientId = session.safe('oauth2.authorize.clientId').string()
|
||||
const client = await this.getClient(request, clientId)
|
||||
|
||||
const client = await this.getClientFromRequest(request)
|
||||
const flowType = session.safe('oauth2.authorize.flow').in(client.allowedFlows)
|
||||
if ( flowType === OAuth2FlowType.code ) {
|
||||
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')
|
||||
.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))
|
||||
if ( user ) {
|
||||
token.userId = String(user.getUniqueIdentifier())
|
||||
}
|
||||
await token.save()
|
||||
|
||||
return token
|
||||
@@ -40,10 +42,10 @@ export class ORMTokenRepository extends TokenRepository {
|
||||
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.userId ? { userId: token.userId } : {}),
|
||||
...(token.scope ? { scope: token.scope } : {}),
|
||||
}
|
||||
|
||||
@@ -74,10 +76,10 @@ export class ORMTokenRepository extends TokenRepository {
|
||||
|
||||
const value = {
|
||||
id: decoded.id,
|
||||
userId: decoded.userId,
|
||||
clientId: decoded.clientId,
|
||||
issued: new Date(decoded.iat * 1000),
|
||||
expires: new Date(decoded.exp * 1000),
|
||||
...(decoded.userId ? { userId: decoded.userId } : {}),
|
||||
...(decoded.scope ? { scope: decoded.scope } : {}),
|
||||
}
|
||||
|
||||
|
||||
@@ -83,13 +83,19 @@ export abstract class ScopeRepository {
|
||||
abstract findByName(name: string): Awaitable<Maybe<OAuth2Scope>>
|
||||
}
|
||||
|
||||
export interface OAuth2Token {
|
||||
id: string
|
||||
userId: AuthenticatableIdentifier
|
||||
clientId: string
|
||||
issued: Date
|
||||
expires: Date
|
||||
scope?: string
|
||||
export abstract class OAuth2Token {
|
||||
abstract id: string
|
||||
|
||||
/** When undefined, these are client credentials. */
|
||||
abstract userId?: AuthenticatableIdentifier
|
||||
|
||||
abstract clientId: string
|
||||
|
||||
abstract issued: Date
|
||||
|
||||
abstract expires: Date
|
||||
|
||||
abstract scope?: string
|
||||
}
|
||||
|
||||
export type OAuth2TokenString = TypeTag<'@extollo/lib.OAuth2TokenString'> & string
|
||||
@@ -105,7 +111,6 @@ export function isOAuth2Token(what: unknown): what is OAuth2Token {
|
||||
|
||||
if (
|
||||
!hasOwnProperty(what, 'id')
|
||||
|| !hasOwnProperty(what, 'userId')
|
||||
|| !hasOwnProperty(what, 'clientId')
|
||||
|| !hasOwnProperty(what, 'issued')
|
||||
|| !hasOwnProperty(what, 'expires')
|
||||
@@ -115,7 +120,7 @@ export function isOAuth2Token(what: unknown): what is OAuth2Token {
|
||||
|
||||
if (
|
||||
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'
|
||||
|| !(what.issued instanceof Date)
|
||||
|| !(what.expires instanceof Date)
|
||||
@@ -129,7 +134,7 @@ export function isOAuth2Token(what: unknown): what is OAuth2Token {
|
||||
export abstract class TokenRepository {
|
||||
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>>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user