feat: refactor auth middleware to handle required and optional cross service token scenarios (#612)

* wip: fix variable name

* wip: remove redundant middleware in auth

* fix: auth middleware refactor

* fix(auth): fetching user for key params

* fix(auth): specs

* fix(auth): registering session controller endpoints
This commit is contained in:
Karol Sójko
2023-05-25 09:43:00 +02:00
committed by GitHub
parent ec75795a02
commit 1e4c7d0f31
45 changed files with 296 additions and 473 deletions

View File

@@ -8,7 +8,6 @@ import { Timer, TimerInterface } from '@standardnotes/time'
import { Env } from './Env'
import { TYPES } from './Types'
import { AuthMiddleware } from '../Controller/AuthMiddleware'
import { ServiceProxyInterface } from '../Service/Http/ServiceProxyInterface'
import { HttpServiceProxy } from '../Service/Http/HttpServiceProxy'
import { SubscriptionTokenAuthMiddleware } from '../Controller/SubscriptionTokenAuthMiddleware'
@@ -20,6 +19,8 @@ import { DirectCallServiceProxy } from '../Service/Proxy/DirectCallServiceProxy'
import { ServiceContainerInterface } from '@standardnotes/domain-core'
import { EndpointResolverInterface } from '../Service/Resolver/EndpointResolverInterface'
import { EndpointResolver } from '../Service/Resolver/EndpointResolver'
import { RequiredCrossServiceTokenMiddleware } from '../Controller/RequiredCrossServiceTokenMiddleware'
import { OptionalCrossServiceTokenMiddleware } from '../Controller/OptionalCrossServiceTokenMiddleware'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const newrelicFormatter = require('@newrelic/winston-enricher')
@@ -77,7 +78,12 @@ export class ContainerConfigLoader {
container.bind(TYPES.CROSS_SERVICE_TOKEN_CACHE_TTL).toConstantValue(+env.get('CROSS_SERVICE_TOKEN_CACHE_TTL', true))
// Middleware
container.bind<AuthMiddleware>(TYPES.AuthMiddleware).to(AuthMiddleware)
container
.bind<RequiredCrossServiceTokenMiddleware>(TYPES.RequiredCrossServiceTokenMiddleware)
.to(RequiredCrossServiceTokenMiddleware)
container
.bind<OptionalCrossServiceTokenMiddleware>(TYPES.OptionalCrossServiceTokenMiddleware)
.to(OptionalCrossServiceTokenMiddleware)
container.bind<WebSocketAuthMiddleware>(TYPES.WebSocketAuthMiddleware).to(WebSocketAuthMiddleware)
container
.bind<SubscriptionTokenAuthMiddleware>(TYPES.SubscriptionTokenAuthMiddleware)

View File

@@ -15,7 +15,8 @@ export const TYPES = {
VERSION: Symbol.for('VERSION'),
CROSS_SERVICE_TOKEN_CACHE_TTL: Symbol.for('CROSS_SERVICE_TOKEN_CACHE_TTL'),
// Middleware
AuthMiddleware: Symbol.for('AuthMiddleware'),
RequiredCrossServiceTokenMiddleware: Symbol.for('RequiredCrossServiceTokenMiddleware'),
OptionalCrossServiceTokenMiddleware: Symbol.for('OptionalCrossServiceTokenMiddleware'),
WebSocketAuthMiddleware: Symbol.for('WebSocketAuthMiddleware'),
SubscriptionTokenAuthMiddleware: Symbol.for('SubscriptionTokenAuthMiddleware'),
// Services

View File

@@ -2,43 +2,33 @@ import { CrossServiceTokenData } from '@standardnotes/security'
import { RoleName } from '@standardnotes/domain-core'
import { TimerInterface } from '@standardnotes/time'
import { NextFunction, Request, Response } from 'express'
import { inject, injectable } from 'inversify'
import { BaseMiddleware } from 'inversify-express-utils'
import { verify } from 'jsonwebtoken'
import { AxiosError } from 'axios'
import { Logger } from 'winston'
import { TYPES } from '../Bootstrap/Types'
import { CrossServiceTokenCacheInterface } from '../Service/Cache/CrossServiceTokenCacheInterface'
import { ServiceProxyInterface } from '../Service/Http/ServiceProxyInterface'
@injectable()
export class AuthMiddleware extends BaseMiddleware {
export abstract class AuthMiddleware extends BaseMiddleware {
constructor(
@inject(TYPES.ServiceProxy) private serviceProxy: ServiceProxyInterface,
@inject(TYPES.AUTH_JWT_SECRET) private jwtSecret: string,
@inject(TYPES.CROSS_SERVICE_TOKEN_CACHE_TTL) private crossServiceTokenCacheTTL: number,
@inject(TYPES.CrossServiceTokenCache) private crossServiceTokenCache: CrossServiceTokenCacheInterface,
@inject(TYPES.Timer) private timer: TimerInterface,
@inject(TYPES.Logger) private logger: Logger,
private serviceProxy: ServiceProxyInterface,
private jwtSecret: string,
private crossServiceTokenCacheTTL: number,
private crossServiceTokenCache: CrossServiceTokenCacheInterface,
private timer: TimerInterface,
private logger: Logger,
) {
super()
}
async handler(request: Request, response: Response, next: NextFunction): Promise<void> {
const authHeaderValue = request.headers.authorization as string
if (!authHeaderValue) {
response.status(401).send({
error: {
tag: 'invalid-auth',
message: 'Invalid login credentials.',
},
})
if (!this.handleMissingAuthHeader(request.headers.authorization, response, next)) {
return
}
const authHeaderValue = request.headers.authorization as string
try {
let crossServiceTokenFetchedFromCache = true
let crossServiceToken = null
@@ -49,10 +39,7 @@ export class AuthMiddleware extends BaseMiddleware {
if (crossServiceToken === null) {
const authResponse = await this.serviceProxy.validateSession(authHeaderValue)
if (authResponse.status > 200) {
response.setHeader('content-type', authResponse.headers.contentType)
response.status(authResponse.status).send(authResponse.data)
if (!this.handleSessionValidationResponse(authResponse, response, next)) {
return
}
@@ -78,6 +65,7 @@ export class AuthMiddleware extends BaseMiddleware {
}
response.locals.user = decodedToken.user
response.locals.session = decodedToken.session
response.locals.roles = decodedToken.roles
} catch (error) {
const errorMessage = (error as AxiosError).isAxiosError
@@ -105,6 +93,24 @@ export class AuthMiddleware extends BaseMiddleware {
return next()
}
protected abstract handleSessionValidationResponse(
authResponse: {
status: number
data: unknown
headers: {
contentType: string
}
},
response: Response,
next: NextFunction,
): boolean
protected abstract handleMissingAuthHeader(
authHeaderValue: string | undefined,
response: Response,
next: NextFunction,
): boolean
private getCrossServiceTokenCacheExpireTimestamp(token: CrossServiceTokenData): number {
const crossServiceTokenDefaultCacheExpiration = this.timer.getTimestampInSeconds() + this.crossServiceTokenCacheTTL

View File

@@ -29,17 +29,17 @@ export class LegacyController extends BaseHttpController {
])
}
@httpPost('/items/sync', TYPES.AuthMiddleware)
@httpPost('/items/sync', TYPES.RequiredCrossServiceTokenMiddleware)
async legacyItemsSync(request: Request, response: Response): Promise<void> {
await this.httpService.callLegacySyncingServer(request, response, request.path.substring(1), request.body)
}
@httpGet('/items/:item_id/revisions', TYPES.AuthMiddleware)
@httpGet('/items/:item_id/revisions', TYPES.RequiredCrossServiceTokenMiddleware)
async legacyGetRevisions(request: Request, response: Response): Promise<void> {
await this.httpService.callLegacySyncingServer(request, response, request.path.substring(1), request.body)
}
@httpGet('/items/:item_id/revisions/:id', TYPES.AuthMiddleware)
@httpGet('/items/:item_id/revisions/:id', TYPES.RequiredCrossServiceTokenMiddleware)
async legacyGetRevision(request: Request, response: Response): Promise<void> {
await this.httpService.callLegacySyncingServer(request, response, request.path.substring(1), request.body)
}

View File

@@ -0,0 +1,51 @@
import { TimerInterface } from '@standardnotes/time'
import { NextFunction, Response } from 'express'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import { TYPES } from '../Bootstrap/Types'
import { CrossServiceTokenCacheInterface } from '../Service/Cache/CrossServiceTokenCacheInterface'
import { ServiceProxyInterface } from '../Service/Http/ServiceProxyInterface'
import { AuthMiddleware } from './AuthMiddleware'
@injectable()
export class OptionalCrossServiceTokenMiddleware extends AuthMiddleware {
constructor(
@inject(TYPES.ServiceProxy) serviceProxy: ServiceProxyInterface,
@inject(TYPES.AUTH_JWT_SECRET) jwtSecret: string,
@inject(TYPES.CROSS_SERVICE_TOKEN_CACHE_TTL) crossServiceTokenCacheTTL: number,
@inject(TYPES.CrossServiceTokenCache) crossServiceTokenCache: CrossServiceTokenCacheInterface,
@inject(TYPES.Timer) timer: TimerInterface,
@inject(TYPES.Logger) logger: Logger,
) {
super(serviceProxy, jwtSecret, crossServiceTokenCacheTTL, crossServiceTokenCache, timer, logger)
}
protected override handleSessionValidationResponse(
authResponse: { status: number; data: unknown; headers: { contentType: string } },
_response: Response,
next: NextFunction,
): boolean {
if (authResponse.status > 200) {
next()
return false
}
return true
}
protected override handleMissingAuthHeader(
authHeaderValue: string | undefined,
_response: Response,
next: NextFunction,
): boolean {
if (!authHeaderValue) {
next()
return false
}
return true
}
}

View File

@@ -0,0 +1,57 @@
import { TimerInterface } from '@standardnotes/time'
import { NextFunction, Response } from 'express'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import { TYPES } from '../Bootstrap/Types'
import { CrossServiceTokenCacheInterface } from '../Service/Cache/CrossServiceTokenCacheInterface'
import { ServiceProxyInterface } from '../Service/Http/ServiceProxyInterface'
import { AuthMiddleware } from './AuthMiddleware'
@injectable()
export class RequiredCrossServiceTokenMiddleware extends AuthMiddleware {
constructor(
@inject(TYPES.ServiceProxy) serviceProxy: ServiceProxyInterface,
@inject(TYPES.AUTH_JWT_SECRET) jwtSecret: string,
@inject(TYPES.CROSS_SERVICE_TOKEN_CACHE_TTL) crossServiceTokenCacheTTL: number,
@inject(TYPES.CrossServiceTokenCache) crossServiceTokenCache: CrossServiceTokenCacheInterface,
@inject(TYPES.Timer) timer: TimerInterface,
@inject(TYPES.Logger) logger: Logger,
) {
super(serviceProxy, jwtSecret, crossServiceTokenCacheTTL, crossServiceTokenCache, timer, logger)
}
protected override handleSessionValidationResponse(
authResponse: { status: number; data: unknown; headers: { contentType: string } },
response: Response,
_next: NextFunction,
): boolean {
if (authResponse.status > 200) {
response.setHeader('content-type', authResponse.headers.contentType)
response.status(authResponse.status).send(authResponse.data)
return false
}
return true
}
protected override handleMissingAuthHeader(
authHeaderValue: string | undefined,
response: Response,
_next: NextFunction,
): boolean {
if (!authHeaderValue) {
response.status(401).send({
error: {
tag: 'invalid-auth',
message: 'Invalid login credentials.',
},
})
return false
}
return true
}
}

View File

@@ -24,7 +24,7 @@ export class ActionsController extends BaseHttpController {
)
}
@httpGet('/login-params')
@httpGet('/login-params', TYPES.OptionalCrossServiceTokenMiddleware)
async loginParams(request: Request, response: Response): Promise<void> {
await this.serviceProxy.callAuthServer(
request,
@@ -34,7 +34,7 @@ export class ActionsController extends BaseHttpController {
)
}
@httpPost('/logout')
@httpPost('/logout', TYPES.OptionalCrossServiceTokenMiddleware)
async logout(request: Request, response: Response): Promise<void> {
await this.serviceProxy.callAuthServer(
request,
@@ -54,7 +54,7 @@ export class ActionsController extends BaseHttpController {
)
}
@httpPost('/recovery/codes', TYPES.AuthMiddleware)
@httpPost('/recovery/codes', TYPES.RequiredCrossServiceTokenMiddleware)
async recoveryCodes(request: Request, response: Response): Promise<void> {
await this.serviceProxy.callAuthServer(
request,

View File

@@ -15,7 +15,7 @@ export class AuthenticatorsController extends BaseHttpController {
super()
}
@httpDelete('/:authenticatorId', TYPES.AuthMiddleware)
@httpDelete('/:authenticatorId', TYPES.RequiredCrossServiceTokenMiddleware)
async delete(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -29,7 +29,7 @@ export class AuthenticatorsController extends BaseHttpController {
)
}
@httpGet('/', TYPES.AuthMiddleware)
@httpGet('/', TYPES.RequiredCrossServiceTokenMiddleware)
async list(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -39,7 +39,7 @@ export class AuthenticatorsController extends BaseHttpController {
)
}
@httpGet('/generate-registration-options', TYPES.AuthMiddleware)
@httpGet('/generate-registration-options', TYPES.RequiredCrossServiceTokenMiddleware)
async generateRegistrationOptions(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -59,7 +59,7 @@ export class AuthenticatorsController extends BaseHttpController {
)
}
@httpPost('/verify-registration', TYPES.AuthMiddleware)
@httpPost('/verify-registration', TYPES.RequiredCrossServiceTokenMiddleware)
async verifyRegistration(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,

View File

@@ -15,7 +15,7 @@ export class FilesController extends BaseHttpController {
super()
}
@httpPost('/valet-tokens', TYPES.AuthMiddleware)
@httpPost('/valet-tokens', TYPES.RequiredCrossServiceTokenMiddleware)
async createToken(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,

View File

@@ -5,7 +5,7 @@ import { TYPES } from '../../Bootstrap/Types'
import { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolverInterface'
@controller('/v1/items', TYPES.AuthMiddleware)
@controller('/v1/items', TYPES.RequiredCrossServiceTokenMiddleware)
export class ItemsController extends BaseHttpController {
constructor(
@inject(TYPES.ServiceProxy) private serviceProxy: ServiceProxyInterface,

View File

@@ -1,7 +1,7 @@
import { BaseHttpController, controller, httpDelete, httpGet, results } from 'inversify-express-utils'
import { TYPES } from '../../Bootstrap/Types'
@controller('/v1/items/:item_id/revisions', TYPES.AuthMiddleware)
@controller('/v1/items/:item_id/revisions', TYPES.RequiredCrossServiceTokenMiddleware)
export class RevisionsController extends BaseHttpController {
@httpGet('/')
async getRevisions(): Promise<results.JsonResult> {

View File

@@ -14,7 +14,7 @@ export class SessionsController extends BaseHttpController {
super()
}
@httpGet('/', TYPES.AuthMiddleware)
@httpGet('/', TYPES.RequiredCrossServiceTokenMiddleware)
async getSessions(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -23,7 +23,7 @@ export class SessionsController extends BaseHttpController {
)
}
@httpDelete('/:uuid', TYPES.AuthMiddleware)
@httpDelete('/:uuid', TYPES.RequiredCrossServiceTokenMiddleware)
async deleteSession(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -35,7 +35,7 @@ export class SessionsController extends BaseHttpController {
)
}
@httpDelete('/', TYPES.AuthMiddleware)
@httpDelete('/', TYPES.RequiredCrossServiceTokenMiddleware)
async deleteSessions(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,

View File

@@ -15,7 +15,7 @@ export class SubscriptionInvitesController extends BaseHttpController {
super()
}
@httpPost('/', TYPES.AuthMiddleware)
@httpPost('/', TYPES.RequiredCrossServiceTokenMiddleware)
async inviteToSubscriptionSharing(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -25,7 +25,7 @@ export class SubscriptionInvitesController extends BaseHttpController {
)
}
@httpGet('/', TYPES.AuthMiddleware)
@httpGet('/', TYPES.RequiredCrossServiceTokenMiddleware)
async listInvites(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -35,7 +35,7 @@ export class SubscriptionInvitesController extends BaseHttpController {
)
}
@httpDelete('/:inviteUuid', TYPES.AuthMiddleware)
@httpDelete('/:inviteUuid', TYPES.RequiredCrossServiceTokenMiddleware)
async cancelSubscriptionSharing(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -48,7 +48,7 @@ export class SubscriptionInvitesController extends BaseHttpController {
)
}
@httpPost('/:inviteUuid/accept', TYPES.AuthMiddleware)
@httpPost('/:inviteUuid/accept', TYPES.RequiredCrossServiceTokenMiddleware)
async acceptInvite(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,

View File

@@ -15,7 +15,7 @@ export class TokensController extends BaseHttpController {
super()
}
@httpPost('/', TYPES.AuthMiddleware)
@httpPost('/', TYPES.RequiredCrossServiceTokenMiddleware)
async createToken(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,

View File

@@ -37,7 +37,7 @@ export class UsersController extends BaseHttpController {
await this.httpService.callPaymentsServer(request, response, 'api/pro_users/send-activation-code', request.body)
}
@httpPatch('/:userId', TYPES.AuthMiddleware)
@httpPatch('/:userId', TYPES.RequiredCrossServiceTokenMiddleware)
async updateUser(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -47,7 +47,7 @@ export class UsersController extends BaseHttpController {
)
}
@httpPut('/:userUuid/password', TYPES.AuthMiddleware)
@httpPut('/:userUuid/password', TYPES.RequiredCrossServiceTokenMiddleware)
async changePassword(request: Request, response: Response): Promise<void> {
this.logger.debug(
'[DEPRECATED] use endpoint /v1/users/:userUuid/attributes/credentials instead of /v1/users/:userUuid/password',
@@ -65,7 +65,7 @@ export class UsersController extends BaseHttpController {
)
}
@httpPut('/:userUuid/attributes/credentials', TYPES.AuthMiddleware)
@httpPut('/:userUuid/attributes/credentials', TYPES.RequiredCrossServiceTokenMiddleware)
async changeCredentials(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -79,7 +79,7 @@ export class UsersController extends BaseHttpController {
)
}
@httpGet('/:userId/params', TYPES.AuthMiddleware)
@httpGet('/:userId/params', TYPES.RequiredCrossServiceTokenMiddleware)
async getKeyParams(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -88,12 +88,12 @@ export class UsersController extends BaseHttpController {
)
}
@all('/:userId/mfa', TYPES.AuthMiddleware)
@all('/:userId/mfa', TYPES.RequiredCrossServiceTokenMiddleware)
async blockMFA(): Promise<results.StatusCodeResult> {
return this.statusCode(401)
}
@httpPost('/:userUuid/integrations/listed', TYPES.AuthMiddleware)
@httpPost('/:userUuid/integrations/listed', TYPES.RequiredCrossServiceTokenMiddleware)
async createListedAccount(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -113,7 +113,7 @@ export class UsersController extends BaseHttpController {
)
}
@httpGet('/:userUuid/settings', TYPES.AuthMiddleware)
@httpGet('/:userUuid/settings', TYPES.RequiredCrossServiceTokenMiddleware)
async listSettings(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -126,7 +126,7 @@ export class UsersController extends BaseHttpController {
)
}
@httpPut('/:userUuid/settings', TYPES.AuthMiddleware)
@httpPut('/:userUuid/settings', TYPES.RequiredCrossServiceTokenMiddleware)
async putSetting(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -140,7 +140,7 @@ export class UsersController extends BaseHttpController {
)
}
@httpGet('/:userUuid/settings/:settingName', TYPES.AuthMiddleware)
@httpGet('/:userUuid/settings/:settingName', TYPES.RequiredCrossServiceTokenMiddleware)
async getSetting(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -154,7 +154,7 @@ export class UsersController extends BaseHttpController {
)
}
@httpDelete('/:userUuid/settings/:settingName', TYPES.AuthMiddleware)
@httpDelete('/:userUuid/settings/:settingName', TYPES.RequiredCrossServiceTokenMiddleware)
async deleteSetting(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -169,7 +169,7 @@ export class UsersController extends BaseHttpController {
)
}
@httpGet('/:userUuid/subscription-settings/:subscriptionSettingName', TYPES.AuthMiddleware)
@httpGet('/:userUuid/subscription-settings/:subscriptionSettingName', TYPES.RequiredCrossServiceTokenMiddleware)
async getSubscriptionSetting(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -183,7 +183,7 @@ export class UsersController extends BaseHttpController {
)
}
@httpGet('/:userUuid/features', TYPES.AuthMiddleware)
@httpGet('/:userUuid/features', TYPES.RequiredCrossServiceTokenMiddleware)
async getFeatures(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -196,7 +196,7 @@ export class UsersController extends BaseHttpController {
)
}
@httpGet('/:userUuid/subscription', TYPES.AuthMiddleware)
@httpGet('/:userUuid/subscription', TYPES.RequiredCrossServiceTokenMiddleware)
async getSubscription(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@@ -232,12 +232,12 @@ export class UsersController extends BaseHttpController {
)
}
@httpDelete('/:userUuid', TYPES.AuthMiddleware)
@httpDelete('/:userUuid', TYPES.RequiredCrossServiceTokenMiddleware)
async deleteUser(request: Request, response: Response): Promise<void> {
await this.httpService.callPaymentsServer(request, response, 'api/account', request.body)
}
@httpPost('/:userUuid/requests', TYPES.AuthMiddleware)
@httpPost('/:userUuid/requests', TYPES.RequiredCrossServiceTokenMiddleware)
async submitRequest(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,

View File

@@ -17,7 +17,7 @@ export class WebSocketsController extends BaseHttpController {
super()
}
@httpPost('/tokens', TYPES.AuthMiddleware)
@httpPost('/tokens', TYPES.RequiredCrossServiceTokenMiddleware)
async createWebSocketConnectionToken(request: Request, response: Response): Promise<void> {
await this.httpService.callWebSocketServer(
request,

View File

@@ -9,7 +9,7 @@ import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolv
@controller('/v2')
export class ActionsControllerV2 extends BaseHttpController {
constructor(
@inject(TYPES.ServiceProxy) private httpService: ServiceProxyInterface,
@inject(TYPES.ServiceProxy) private serviceProxy: ServiceProxyInterface,
@inject(TYPES.EndpointResolver) private endpointResolver: EndpointResolverInterface,
) {
super()
@@ -17,7 +17,7 @@ export class ActionsControllerV2 extends BaseHttpController {
@httpPost('/login')
async login(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
await this.serviceProxy.callAuthServer(
request,
response,
this.endpointResolver.resolveEndpointOrMethodIdentifier('POST', 'auth/pkce_sign_in'),
@@ -25,9 +25,9 @@ export class ActionsControllerV2 extends BaseHttpController {
)
}
@httpPost('/login-params')
@httpPost('/login-params', TYPES.OptionalCrossServiceTokenMiddleware)
async loginParams(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
await this.serviceProxy.callAuthServer(
request,
response,
this.endpointResolver.resolveEndpointOrMethodIdentifier('POST', 'auth/pkce_params'),

View File

@@ -6,7 +6,7 @@ import { TYPES } from '../../Bootstrap/Types'
import { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolverInterface'
@controller('/v2/items/:itemUuid/revisions', TYPES.AuthMiddleware)
@controller('/v2/items/:itemUuid/revisions', TYPES.RequiredCrossServiceTokenMiddleware)
export class RevisionsControllerV2 extends BaseHttpController {
constructor(
@inject(TYPES.ServiceProxy) private httpService: ServiceProxyInterface,

View File

@@ -241,17 +241,16 @@ import { InversifyExpressSubscriptionTokensController } from '../Infra/Inversify
import { InversifyExpressSubscriptionSettingsController } from '../Infra/InversifyExpressUtils/InversifyExpressSubscriptionSettingsController'
import { InversifyExpressSettingsController } from '../Infra/InversifyExpressUtils/InversifyExpressSettingsController'
import { SessionMiddleware } from '../Infra/InversifyExpressUtils/Middleware/SessionMiddleware'
import { ApiGatewayAuthMiddleware } from '../Infra/InversifyExpressUtils/Middleware/ApiGatewayAuthMiddleware'
import { ApiGatewayOfflineAuthMiddleware } from '../Infra/InversifyExpressUtils/Middleware/ApiGatewayOfflineAuthMiddleware'
import { AuthMiddleware } from '../Infra/InversifyExpressUtils/Middleware/AuthMiddleware'
import { OfflineUserAuthMiddleware } from '../Infra/InversifyExpressUtils/Middleware/OfflineUserAuthMiddleware'
import { AuthMiddlewareWithoutResponse } from '../Infra/InversifyExpressUtils/Middleware/AuthMiddlewareWithoutResponse'
import { LockMiddleware } from '../Infra/InversifyExpressUtils/Middleware/LockMiddleware'
import { InversifyExpressSessionController } from '../Infra/InversifyExpressUtils/InversifyExpressSessionController'
import { InversifyExpressOfflineController } from '../Infra/InversifyExpressUtils/InversifyExpressOfflineController'
import { InversifyExpressListedController } from '../Infra/InversifyExpressUtils/InversifyExpressListedController'
import { InversifyExpressInternalController } from '../Infra/InversifyExpressUtils/InversifyExpressInternalController'
import { InversifyExpressFeaturesController } from '../Infra/InversifyExpressUtils/InversifyExpressFeaturesController'
import { RequiredCrossServiceTokenMiddleware } from '../Infra/InversifyExpressUtils/Middleware/RequiredCrossServiceTokenMiddleware'
import { OptionalCrossServiceTokenMiddleware } from '../Infra/InversifyExpressUtils/Middleware/OptionalCrossServiceTokenMiddleware'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const newrelicFormatter = require('@newrelic/winston-enricher')
@@ -448,13 +447,14 @@ export class ContainerConfigLoader {
)
// Middleware
container.bind<AuthMiddleware>(TYPES.Auth_AuthMiddleware).to(AuthMiddleware)
container.bind<SessionMiddleware>(TYPES.Auth_SessionMiddleware).to(SessionMiddleware)
container.bind<LockMiddleware>(TYPES.Auth_LockMiddleware).to(LockMiddleware)
container
.bind<AuthMiddlewareWithoutResponse>(TYPES.Auth_AuthMiddlewareWithoutResponse)
.to(AuthMiddlewareWithoutResponse)
container.bind<ApiGatewayAuthMiddleware>(TYPES.Auth_ApiGatewayAuthMiddleware).to(ApiGatewayAuthMiddleware)
.bind<RequiredCrossServiceTokenMiddleware>(TYPES.Auth_RequiredCrossServiceTokenMiddleware)
.to(RequiredCrossServiceTokenMiddleware)
container
.bind<OptionalCrossServiceTokenMiddleware>(TYPES.Auth_OptionalCrossServiceTokenMiddleware)
.to(OptionalCrossServiceTokenMiddleware)
container
.bind<ApiGatewayOfflineAuthMiddleware>(TYPES.Auth_ApiGatewayOfflineAuthMiddleware)
.to(ApiGatewayOfflineAuthMiddleware)

View File

@@ -51,11 +51,10 @@ const TYPES = {
Auth_ORMAuthenticatorChallengeRepository: Symbol.for('Auth_ORMAuthenticatorChallengeRepository'),
Auth_ORMCacheEntryRepository: Symbol.for('Auth_ORMCacheEntryRepository'),
// Middleware
Auth_AuthMiddleware: Symbol.for('Auth_AuthMiddleware'),
Auth_ApiGatewayAuthMiddleware: Symbol.for('Auth_ApiGatewayAuthMiddleware'),
Auth_RequiredCrossServiceTokenMiddleware: Symbol.for('Auth_RequiredCrossServiceTokenMiddleware'),
Auth_OptionalCrossServiceTokenMiddleware: Symbol.for('Auth_OptionalCrossServiceTokenMiddleware'),
Auth_ApiGatewayOfflineAuthMiddleware: Symbol.for('Auth_ApiGatewayOfflineAuthMiddleware'),
Auth_OfflineUserAuthMiddleware: Symbol.for('Auth_OfflineUserAuthMiddleware'),
Auth_AuthMiddlewareWithoutResponse: Symbol.for('Auth_AuthMiddlewareWithoutResponse'),
Auth_LockMiddleware: Symbol.for('Auth_LockMiddleware'),
Auth_SessionMiddleware: Symbol.for('Auth_SessionMiddleware'),
// Projectors

View File

@@ -35,9 +35,7 @@ describe('GetUserKeyParams', () => {
})
it('should get key params for an authenticated user - searching by email', async () => {
expect(
await createUseCase().execute({ email: 'test@test.te', authenticated: true, authenticatedUser: user }),
).toEqual({
expect(await createUseCase().execute({ email: 'test@test.te', authenticated: true })).toEqual({
keyParams: {
foo: 'bar',
},
@@ -63,7 +61,7 @@ describe('GetUserKeyParams', () => {
})
it('should get key params for an authenticated user - searching by uuid', async () => {
expect(await createUseCase().execute({ userUuid: '1-2-3', authenticated: true, authenticatedUser: user })).toEqual({
expect(await createUseCase().execute({ userUuid: '1-2-3', authenticated: true })).toEqual({
keyParams: {
foo: 'bar',
},

View File

@@ -22,16 +22,6 @@ export class GetUserKeyParams implements UseCaseInterface {
) {}
async execute(dto: GetUserKeyParamsDTO): Promise<GetUserKeyParamsResponse> {
if (dto.authenticatedUser) {
this.logger.debug(`Creating key params for authenticated user ${dto.authenticatedUser.email}`)
const keyParams = await this.createKeyParams(dto, dto.authenticatedUser, true)
return {
keyParams,
}
}
let user: User | null = null
if (dto.email !== undefined) {
const usernameOrError = Username.create(dto.email)

View File

@@ -1,8 +1,5 @@
import { User } from '../../User/User'
export type GetUserKeyParamsDTOV1Unchallenged = {
authenticated: boolean
email?: string
userUuid?: string
authenticatedUser?: User
}

View File

@@ -1,9 +1,6 @@
import { User } from '../../User/User'
export type GetUserKeyParamsDTOV2Challenged = {
authenticated: boolean
codeChallenge: string
email?: string
userUuid?: string
authenticatedUser?: User
}

View File

@@ -44,13 +44,12 @@ export class InversifyExpressAuthController extends BaseHttpController {
this.controllerContainer.register('auth.signOut', this.signOut.bind(this))
}
@httpGet('/params', TYPES.Auth_AuthMiddlewareWithoutResponse)
@httpGet('/params', TYPES.Auth_OptionalCrossServiceTokenMiddleware)
async params(request: Request, response: Response): Promise<results.JsonResult> {
if (response.locals.session) {
const result = await this.getUserKeyParams.execute({
email: response.locals.user.email,
authenticated: true,
authenticatedUser: response.locals.user,
})
return this.json(result.keyParams)
@@ -155,7 +154,7 @@ export class InversifyExpressAuthController extends BaseHttpController {
return this.json(signInResult.authResponse)
}
@httpPost('/pkce_params', TYPES.Auth_AuthMiddlewareWithoutResponse)
@httpPost('/pkce_params', TYPES.Auth_OptionalCrossServiceTokenMiddleware)
async pkceParams(request: Request, response: Response): Promise<results.JsonResult> {
if (!request.body.code_challenge) {
return this.json(
@@ -172,7 +171,6 @@ export class InversifyExpressAuthController extends BaseHttpController {
const result = await this.getUserKeyParams.execute({
email: response.locals.user.email,
authenticated: true,
authenticatedUser: response.locals.user,
codeChallenge: request.body.code_challenge as string,
})
@@ -261,7 +259,7 @@ export class InversifyExpressAuthController extends BaseHttpController {
return this.json(signInResult.authResponse)
}
@httpPost('/recovery/codes', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpPost('/recovery/codes', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async generateRecoveryCodes(_request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.authController.generateRecoveryCodes({
userUuid: response.locals.user.uuid,
@@ -296,7 +294,7 @@ export class InversifyExpressAuthController extends BaseHttpController {
return this.json(result.data, result.status)
}
@httpPost('/sign_out', TYPES.Auth_AuthMiddlewareWithoutResponse)
@httpPost('/sign_out', TYPES.Auth_OptionalCrossServiceTokenMiddleware)
async signOut(request: Request, response: Response): Promise<results.JsonResult | void> {
const result = await this.authController.signOut({
readOnlyAccess: response.locals.readOnlyAccess,

View File

@@ -37,7 +37,7 @@ export class InversifyExpressAuthenticatorsController extends BaseHttpController
)
}
@httpGet('/', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpGet('/', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async list(_request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.authenticatorsController.list({
userUuid: response.locals.user.uuid,
@@ -46,7 +46,7 @@ export class InversifyExpressAuthenticatorsController extends BaseHttpController
return this.json(result.data, result.status)
}
@httpDelete('/:authenticatorId', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpDelete('/:authenticatorId', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async delete(request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.authenticatorsController.delete({
userUuid: response.locals.user.uuid,
@@ -56,7 +56,7 @@ export class InversifyExpressAuthenticatorsController extends BaseHttpController
return this.json(result.data, result.status)
}
@httpGet('/generate-registration-options', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpGet('/generate-registration-options', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async generateRegistrationOptions(_request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.authenticatorsController.generateRegistrationOptions({
username: response.locals.user.email,
@@ -66,7 +66,7 @@ export class InversifyExpressAuthenticatorsController extends BaseHttpController
return this.json(result.data, result.status)
}
@httpPost('/verify-registration', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpPost('/verify-registration', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async verifyRegistration(request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.authenticatorsController.verifyRegistrationResponse({
userUuid: response.locals.user.uuid,

View File

@@ -22,7 +22,7 @@ export class InversifyExpressFeaturesController extends BaseHttpController {
this.controllerContainer.register('auth.users.getFeatures', this.getFeatures.bind(this))
}
@httpGet('/', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpGet('/', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async getFeatures(request: Request, response: Response): Promise<results.JsonResult> {
if (request.params.userUuid !== response.locals.user.uuid) {
return this.json(

View File

@@ -18,7 +18,7 @@ export class InversifyExpressListedController extends BaseHttpController {
this.controllerContainer.register('auth.users.createListedAccount', this.createListedAccount.bind(this))
}
@httpPost('/', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpPost('/', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async createListedAccount(_request: Request, response: Response): Promise<results.JsonResult> {
if (response.locals.readOnlyAccess) {
return this.json(

View File

@@ -26,12 +26,12 @@ export class InversifyExpressSessionController extends BaseHttpController {
) {
super()
this.controllerContainer.register('auth.session.delete', this.deleteSession.bind(this))
this.controllerContainer.register('auth.session.deleteAll', this.deleteAllSessions.bind(this))
this.controllerContainer.register('auth.session.refresh', this.refresh.bind(this))
this.controllerContainer.register('auth.sessions.delete', this.deleteSession.bind(this))
this.controllerContainer.register('auth.sessions.deleteAll', this.deleteAllSessions.bind(this))
this.controllerContainer.register('auth.sessions.refresh', this.refresh.bind(this))
}
@httpDelete('/', TYPES.Auth_AuthMiddleware, TYPES.Auth_SessionMiddleware)
@httpDelete('/', TYPES.Auth_RequiredCrossServiceTokenMiddleware, TYPES.Auth_SessionMiddleware)
async deleteSession(request: Request, response: Response): Promise<results.JsonResult | void> {
if (response.locals.readOnlyAccess) {
return this.json(
@@ -87,7 +87,7 @@ export class InversifyExpressSessionController extends BaseHttpController {
response.status(204).send()
}
@httpDelete('/all', TYPES.Auth_AuthMiddleware, TYPES.Auth_SessionMiddleware)
@httpDelete('/all', TYPES.Auth_RequiredCrossServiceTokenMiddleware, TYPES.Auth_SessionMiddleware)
async deleteAllSessions(_request: Request, response: Response): Promise<results.JsonResult | void> {
if (response.locals.readOnlyAccess) {
return this.json(

View File

@@ -62,7 +62,7 @@ export class InversifyExpressSessionsController extends BaseHttpController {
return this.json({ authToken: result.token })
}
@httpGet('/', TYPES.Auth_AuthMiddleware, TYPES.Auth_SessionMiddleware)
@httpGet('/', TYPES.Auth_RequiredCrossServiceTokenMiddleware, TYPES.Auth_SessionMiddleware)
async getSessions(_request: Request, response: Response): Promise<results.JsonResult> {
if (response.locals.readOnlyAccess) {
return this.json([])

View File

@@ -35,7 +35,7 @@ export class InversifyExpressSettingsController extends BaseHttpController {
this.controllerContainer.register('auth.users.deleteSetting', this.deleteSetting.bind(this))
}
@httpGet('/settings', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpGet('/settings', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async getSettings(request: Request, response: Response): Promise<results.JsonResult> {
if (request.params.userUuid !== response.locals.user.uuid) {
return this.json(
@@ -54,7 +54,7 @@ export class InversifyExpressSettingsController extends BaseHttpController {
return this.json(result)
}
@httpGet('/settings/:settingName', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpGet('/settings/:settingName', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async getSetting(request: Request, response: Response): Promise<results.JsonResult> {
if (request.params.userUuid !== response.locals.user.uuid) {
return this.json(
@@ -77,7 +77,7 @@ export class InversifyExpressSettingsController extends BaseHttpController {
return this.json(result, 400)
}
@httpPut('/settings', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpPut('/settings', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async updateSetting(request: Request, response: Response): Promise<results.JsonResult | results.StatusCodeResult> {
if (response.locals.readOnlyAccess) {
return this.json(
@@ -124,7 +124,7 @@ export class InversifyExpressSettingsController extends BaseHttpController {
return this.json(result, result.statusCode)
}
@httpDelete('/settings/:settingName', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpDelete('/settings/:settingName', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async deleteSetting(request: Request, response: Response): Promise<results.JsonResult> {
if (response.locals.readOnlyAccess) {
return this.json(

View File

@@ -31,7 +31,7 @@ export class InversifyExpressSubscriptionInvitesController extends BaseHttpContr
this.controllerContainer.register('auth.subscriptionInvites.list', this.listInvites.bind(this))
}
@httpPost('/:inviteUuid/accept', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpPost('/:inviteUuid/accept', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async acceptInvite(request: Request, response: Response): Promise<void> {
const result = await this.subscriptionInvitesController.acceptInvite({
api: request.query.api as ApiVersion,
@@ -52,7 +52,7 @@ export class InversifyExpressSubscriptionInvitesController extends BaseHttpContr
return this.json(response.data, response.status)
}
@httpPost('/', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpPost('/', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async inviteToSubscriptionSharing(request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.subscriptionInvitesController.invite({
...request.body,
@@ -64,7 +64,7 @@ export class InversifyExpressSubscriptionInvitesController extends BaseHttpContr
return this.json(result.data, result.status)
}
@httpDelete('/:inviteUuid', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpDelete('/:inviteUuid', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async cancelSubscriptionSharing(request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.subscriptionInvitesController.cancelInvite({
...request.body,
@@ -75,7 +75,7 @@ export class InversifyExpressSubscriptionInvitesController extends BaseHttpContr
return this.json(result.data, result.status)
}
@httpGet('/', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpGet('/', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async listInvites(request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.subscriptionInvitesController.listInvites({
...request.body,

View File

@@ -22,7 +22,7 @@ export class InversifyExpressSubscriptionSettingsController extends BaseHttpCont
this.controllerContainer.register('auth.users.getSubscriptionSetting', this.getSubscriptionSetting.bind(this))
}
@httpGet('/subscription-settings/:subscriptionSettingName', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpGet('/subscription-settings/:subscriptionSettingName', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async getSubscriptionSetting(request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.doGetSetting.execute({
userUuid: response.locals.user.uuid,

View File

@@ -37,7 +37,7 @@ export class InversifyExpressSubscriptionTokensController extends BaseHttpContro
this.controllerContainer.register('auth.subscription-tokens.create', this.createToken.bind(this))
}
@httpPost('/', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpPost('/', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async createToken(_request: Request, response: Response): Promise<results.JsonResult> {
if (response.locals.readOnlyAccess) {
return this.json(

View File

@@ -17,7 +17,7 @@ export class InversifyExpressUserRequestsController extends BaseHttpController {
this.controllerContainer.register('auth.users.createRequest', this.submitRequest.bind(this))
}
@httpPost('/', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpPost('/', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async submitRequest(request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.userRequestsController.submitUserRequest({
requestType: request.body.requestType,

View File

@@ -41,7 +41,7 @@ export class InversifyExpressUsersController extends BaseHttpController {
this.controllerContainer.register('auth.users.updateCredentials', this.changeCredentials.bind(this))
}
@httpPatch('/:userId', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpPatch('/:userId', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async update(request: Request, response: Response): Promise<results.JsonResult | void> {
if (response.locals.readOnlyAccess) {
return this.json(
@@ -132,7 +132,7 @@ export class InversifyExpressUsersController extends BaseHttpController {
return this.json({ message: result.message }, result.responseCode)
}
@httpGet('/:userUuid/subscription', TYPES.Auth_ApiGatewayAuthMiddleware)
@httpGet('/:userUuid/subscription', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async getSubscription(request: Request, response: Response): Promise<results.JsonResult> {
if (request.params.userUuid !== response.locals.user.uuid) {
return this.json(
@@ -156,7 +156,7 @@ export class InversifyExpressUsersController extends BaseHttpController {
return this.json(result, 400)
}
@httpPut('/:userId/attributes/credentials', TYPES.Auth_AuthMiddleware)
@httpPut('/:userId/attributes/credentials', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async changeCredentials(request: Request, response: Response): Promise<results.JsonResult | void> {
if (response.locals.readOnlyAccess) {
return this.json(

View File

@@ -13,7 +13,7 @@ import { ControllerContainerInterface, Uuid } from '@standardnotes/domain-core'
import TYPES from '../../Bootstrap/Types'
import { CreateValetToken } from '../../Domain/UseCase/CreateValetToken/CreateValetToken'
@controller('/valet-tokens', TYPES.Auth_ApiGatewayAuthMiddleware)
@controller('/valet-tokens', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
export class InversifyExpressValetTokenController extends BaseHttpController {
constructor(
@inject(TYPES.Auth_CreateValetToken) private createValetKey: CreateValetToken,

View File

@@ -1,99 +0,0 @@
import 'reflect-metadata'
import { ApiGatewayAuthMiddleware } from './ApiGatewayAuthMiddleware'
import { NextFunction, Request, Response } from 'express'
import { Logger } from 'winston'
import { CrossServiceTokenData, TokenDecoderInterface } from '@standardnotes/security'
import { RoleName } from '@standardnotes/domain-core'
describe('ApiGatewayAuthMiddleware', () => {
let tokenDecoder: TokenDecoderInterface<CrossServiceTokenData>
let request: Request
let response: Response
let next: NextFunction
const logger = {
debug: jest.fn(),
} as unknown as jest.Mocked<Logger>
const createMiddleware = () => new ApiGatewayAuthMiddleware(tokenDecoder, logger)
beforeEach(() => {
tokenDecoder = {} as jest.Mocked<TokenDecoderInterface<CrossServiceTokenData>>
tokenDecoder.decodeToken = jest.fn().mockReturnValue({
user: {
uuid: '1-2-3',
email: 'test@test.te',
},
roles: [
{
uuid: 'a-b-c',
name: RoleName.NAMES.CoreUser,
},
],
})
request = {
headers: {},
} as jest.Mocked<Request>
response = {
locals: {},
} as jest.Mocked<Response>
response.status = jest.fn().mockReturnThis()
response.send = jest.fn()
next = jest.fn()
})
it('should authorize user', async () => {
request.headers['x-auth-token'] = 'auth-jwt-token'
await createMiddleware().handler(request, response, next)
expect(response.locals.user).toEqual({
uuid: '1-2-3',
email: 'test@test.te',
})
expect(response.locals.roles).toEqual([
{
uuid: 'a-b-c',
name: RoleName.NAMES.CoreUser,
},
])
expect(next).toHaveBeenCalled()
})
it('should not authorize if request is missing auth jwt token in headers', async () => {
await createMiddleware().handler(request, response, next)
expect(response.status).toHaveBeenCalledWith(401)
expect(next).not.toHaveBeenCalled()
})
it('should not authorize if auth jwt token is malformed', async () => {
request.headers['x-auth-token'] = 'auth-jwt-token'
tokenDecoder.decodeToken = jest.fn().mockReturnValue(undefined)
await createMiddleware().handler(request, response, next)
expect(response.status).toHaveBeenCalledWith(401)
expect(next).not.toHaveBeenCalled()
})
it('should pass the error to next middleware if one occurres', async () => {
request.headers['x-auth-token'] = 'auth-jwt-token'
const error = new Error('Ooops')
tokenDecoder.decodeToken = jest.fn().mockImplementation(() => {
throw error
})
await createMiddleware().handler(request, response, next)
expect(response.status).not.toHaveBeenCalled()
expect(next).toHaveBeenCalledWith(error)
})
})

View File

@@ -1,31 +1,16 @@
import { CrossServiceTokenData, TokenDecoderInterface } from '@standardnotes/security'
import { NextFunction, Request, Response } from 'express'
import { inject, injectable } from 'inversify'
import { BaseMiddleware } from 'inversify-express-utils'
import { Logger } from 'winston'
import TYPES from '../../../Bootstrap/Types'
@injectable()
export class ApiGatewayAuthMiddleware extends BaseMiddleware {
constructor(
@inject(TYPES.Auth_CrossServiceTokenDecoder) private tokenDecoder: TokenDecoderInterface<CrossServiceTokenData>,
@inject(TYPES.Auth_Logger) private logger: Logger,
) {
export abstract class ApiGatewayAuthMiddleware extends BaseMiddleware {
constructor(private tokenDecoder: TokenDecoderInterface<CrossServiceTokenData>, private logger: Logger) {
super()
}
async handler(request: Request, response: Response, next: NextFunction): Promise<void> {
try {
if (!request.headers['x-auth-token']) {
this.logger.debug('ApiGatewayAuthMiddleware missing x-auth-token header.')
response.status(401).send({
error: {
tag: 'invalid-auth',
message: 'Invalid login credentials.',
},
})
if (!this.handleMissingToken(request, response, next)) {
return
}
@@ -56,4 +41,6 @@ export class ApiGatewayAuthMiddleware extends BaseMiddleware {
return next(error)
}
}
protected abstract handleMissingToken(request: Request, response: Response, next: NextFunction): boolean
}

View File

@@ -1,79 +0,0 @@
import 'reflect-metadata'
import { AuthMiddleware } from './AuthMiddleware'
import { NextFunction, Request, Response } from 'express'
import { User } from '../../../Domain/User/User'
import { AuthenticateRequest } from '../../../Domain/UseCase/AuthenticateRequest'
import { Session } from '../../../Domain/Session/Session'
import { Logger } from 'winston'
describe('AuthMiddleware', () => {
let authenticateRequest: AuthenticateRequest
let request: Request
let response: Response
let next: NextFunction
const logger = {
debug: jest.fn(),
} as unknown as jest.Mocked<Logger>
const createMiddleware = () => new AuthMiddleware(authenticateRequest, logger)
beforeEach(() => {
authenticateRequest = {} as jest.Mocked<AuthenticateRequest>
authenticateRequest.execute = jest.fn()
request = {
headers: {},
} as jest.Mocked<Request>
response = {
locals: {},
} as jest.Mocked<Response>
response.status = jest.fn().mockReturnThis()
response.send = jest.fn()
next = jest.fn()
})
it('should authorize user', async () => {
const user = {} as jest.Mocked<User>
const session = {} as jest.Mocked<Session>
authenticateRequest.execute = jest.fn().mockReturnValue({
success: true,
user,
session,
})
await createMiddleware().handler(request, response, next)
expect(response.locals.user).toEqual(user)
expect(response.locals.session).toEqual(session)
expect(next).toHaveBeenCalled()
})
it('should not authorize if request authentication fails', async () => {
authenticateRequest.execute = jest.fn().mockReturnValue({
success: false,
responseCode: 401,
})
await createMiddleware().handler(request, response, next)
expect(response.status).toHaveBeenCalledWith(401)
expect(next).not.toHaveBeenCalled()
})
it('should pass the error to next middleware if one occurres', async () => {
const error = new Error('Ooops')
authenticateRequest.execute = jest.fn().mockImplementation(() => {
throw error
})
await createMiddleware().handler(request, response, next)
expect(response.status).not.toHaveBeenCalled()
expect(next).toHaveBeenCalledWith(error)
})
})

View File

@@ -1,45 +0,0 @@
import { NextFunction, Request, Response } from 'express'
import { inject, injectable } from 'inversify'
import { BaseMiddleware } from 'inversify-express-utils'
import { Logger } from 'winston'
import TYPES from '../../../Bootstrap/Types'
import { AuthenticateRequest } from '../../../Domain/UseCase/AuthenticateRequest'
@injectable()
export class AuthMiddleware extends BaseMiddleware {
constructor(
@inject(TYPES.Auth_AuthenticateRequest) private authenticateRequest: AuthenticateRequest,
@inject(TYPES.Auth_Logger) private logger: Logger,
) {
super()
}
async handler(request: Request, response: Response, next: NextFunction): Promise<void> {
try {
const authenticateRequestResponse = await this.authenticateRequest.execute({
authorizationHeader: request.headers.authorization,
})
if (!authenticateRequestResponse.success) {
this.logger.debug('AuthMiddleware authentication failure.')
response.status(authenticateRequestResponse.responseCode).send({
error: {
tag: authenticateRequestResponse.errorTag,
message: authenticateRequestResponse.errorMessage,
},
})
return
}
response.locals.user = authenticateRequestResponse.user
response.locals.session = authenticateRequestResponse.session
response.locals.readOnlyAccess = authenticateRequestResponse.session?.readonlyAccess ?? false
return next()
} catch (error) {
return next(error)
}
}
}

View File

@@ -1,68 +0,0 @@
import 'reflect-metadata'
import { AuthMiddlewareWithoutResponse } from './AuthMiddlewareWithoutResponse'
import { NextFunction, Request, Response } from 'express'
import { User } from '../../../Domain/User/User'
import { AuthenticateRequest } from '../../../Domain/UseCase/AuthenticateRequest'
import { Session } from '../../../Domain/Session/Session'
describe('AuthMiddlewareWithoutResponse', () => {
let authenticateRequest: AuthenticateRequest
let request: Request
let response: Response
let next: NextFunction
const createMiddleware = () => new AuthMiddlewareWithoutResponse(authenticateRequest)
beforeEach(() => {
authenticateRequest = {} as jest.Mocked<AuthenticateRequest>
authenticateRequest.execute = jest.fn()
request = {
headers: {},
} as jest.Mocked<Request>
response = {
locals: {},
} as jest.Mocked<Response>
response.status = jest.fn().mockReturnThis()
response.send = jest.fn()
next = jest.fn()
})
it('should authorize user', async () => {
const user = {} as jest.Mocked<User>
const session = {} as jest.Mocked<Session>
authenticateRequest.execute = jest.fn().mockReturnValue({
success: true,
user,
session,
})
await createMiddleware().handler(request, response, next)
expect(response.locals.user).toEqual(user)
expect(response.locals.session).toEqual(session)
expect(next).toHaveBeenCalled()
})
it('should skip middleware if authentication fails', async () => {
authenticateRequest.execute = jest.fn().mockReturnValue({
success: false,
})
await createMiddleware().handler(request, response, next)
expect(next).toHaveBeenCalled()
})
it('should skip middleware if authentication errors', async () => {
authenticateRequest.execute = jest.fn().mockImplementation(() => {
throw new Error('Ooops')
})
await createMiddleware().handler(request, response, next)
expect(next).toHaveBeenCalled()
})
})

View File

@@ -1,32 +0,0 @@
import { NextFunction, Request, Response } from 'express'
import { inject, injectable } from 'inversify'
import { BaseMiddleware } from 'inversify-express-utils'
import TYPES from '../../../Bootstrap/Types'
import { AuthenticateRequest } from '../../../Domain/UseCase/AuthenticateRequest'
@injectable()
export class AuthMiddlewareWithoutResponse extends BaseMiddleware {
constructor(@inject(TYPES.Auth_AuthenticateRequest) private authenticateRequest: AuthenticateRequest) {
super()
}
async handler(request: Request, response: Response, next: NextFunction): Promise<void> {
try {
const authenticateRequestResponse = await this.authenticateRequest.execute({
authorizationHeader: request.headers.authorization,
})
if (!authenticateRequestResponse.success) {
return next()
}
response.locals.user = authenticateRequestResponse.user
response.locals.session = authenticateRequestResponse.session
response.locals.readOnlyAccess = authenticateRequestResponse.session?.readonlyAccess ?? false
return next()
} catch (error) {
return next()
}
}
}

View File

@@ -0,0 +1,27 @@
import { CrossServiceTokenData, TokenDecoderInterface } from '@standardnotes/security'
import { NextFunction, Request, Response } from 'express'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../../Bootstrap/Types'
import { ApiGatewayAuthMiddleware } from './ApiGatewayAuthMiddleware'
@injectable()
export class OptionalCrossServiceTokenMiddleware extends ApiGatewayAuthMiddleware {
constructor(
@inject(TYPES.Auth_CrossServiceTokenDecoder) tokenDecoder: TokenDecoderInterface<CrossServiceTokenData>,
@inject(TYPES.Auth_Logger) logger: Logger,
) {
super(tokenDecoder, logger)
}
protected override handleMissingToken(request: Request, _response: Response, next: NextFunction): boolean {
if (!request.headers['x-auth-token']) {
next()
return false
}
return true
}
}

View File

@@ -0,0 +1,32 @@
import { CrossServiceTokenData, TokenDecoderInterface } from '@standardnotes/security'
import { NextFunction, Request, Response } from 'express'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../../Bootstrap/Types'
import { ApiGatewayAuthMiddleware } from './ApiGatewayAuthMiddleware'
@injectable()
export class RequiredCrossServiceTokenMiddleware extends ApiGatewayAuthMiddleware {
constructor(
@inject(TYPES.Auth_CrossServiceTokenDecoder) tokenDecoder: TokenDecoderInterface<CrossServiceTokenData>,
@inject(TYPES.Auth_Logger) logger: Logger,
) {
super(tokenDecoder, logger)
}
protected override handleMissingToken(request: Request, response: Response, _next: NextFunction): boolean {
if (!request.headers['x-auth-token']) {
response.status(401).send({
error: {
tag: 'invalid-auth',
message: 'Invalid login credentials.',
},
})
return false
}
return true
}
}