Skip to content

Commit

Permalink
fix: add token validation directive
Browse files Browse the repository at this point in the history
  • Loading branch information
Matheus-Aguilar committed Jun 14, 2024
1 parent 3fef2e0 commit 34d267d
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 13 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

### Added
- Add token validation directive

## [1.40.7] - 2024-06-11

### Fixed
Expand Down
1 change: 1 addition & 0 deletions graphql/directives.graphql
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
directive @checkUserAccess on FIELD | FIELD_DEFINITION
directive @validateUserAccess on FIELD | FIELD_DEFINITION
directive @checkAdminAccess on FIELD | FIELD_DEFINITION
directive @withSession on FIELD_DEFINITION
directive @withSender on FIELD_DEFINITION
Expand Down
8 changes: 2 additions & 6 deletions graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ type Query {
listAllUsers: [User]
@cacheControl(scope: PRIVATE, maxAge: SHORT)
@withSender
@checkUserAccess
@validateUserAccess

listUsers(organizationId: ID, costCenterId: ID, roleId: ID): [User]
@cacheControl(scope: PRIVATE, maxAge: SHORT)
Expand Down Expand Up @@ -71,11 +71,7 @@ type Query {

getSessionWatcher: Boolean @cacheControl(scope: PRIVATE)

getUsersByEmail(
email: String!
orgId: ID
costId: ID
): [User]
getUsersByEmail(email: String!, orgId: ID, costId: ID): [User]
@cacheControl(scope: PRIVATE)
@checkUserAccess

Expand Down
2 changes: 2 additions & 0 deletions node/directives/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { WithSender } from './withSender'
import { WithUserPermissions } from './withUserPermissions'
import { CheckAdminAccess } from './checkAdminAccess'
import { CheckUserAccess } from './checkUserAccess'
import { ValidateUserAccess } from './validateUserAccess'
import { AuditAccess } from './auditAccess'

export const schemaDirectives = {
checkAdminAccess: CheckAdminAccess as any,
checkUserAccess: CheckUserAccess as any,
validateUserAccess: ValidateUserAccess as any,
withSession: WithSession,
withSender: WithSender,
withUserPermissions: WithUserPermissions,
Expand Down
122 changes: 122 additions & 0 deletions node/directives/validateUserAccess.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { AuthenticationError, ForbiddenError } from '@vtex/api'
import type { GraphQLField } from 'graphql'
import { defaultFieldResolver } from 'graphql'
import { SchemaDirectiveVisitor } from 'graphql-tools'

import type { AuthAuditTokenMetrics } from '../metrics/auth'
import sendAuthMetric, { AuthMetric } from '../metrics/auth'
import {
validateAdminToken,
validateApiToken,
validateStoreToken,
} from './helper'

export class ValidateUserAccess extends SchemaDirectiveVisitor {
public visitFieldDefinition(field: GraphQLField<any, any>) {
const { resolve = defaultFieldResolver } = field

field.resolve = async (
root: any,
args: any,
context: Context,
info: any
) => {
const {
vtex: { adminUserAuthToken, storeUserAuthToken, logger },
} = context

let hasTokens = true
let hasValidTokens = true

const { hasAdminToken, hasValidAdminToken } = await validateAdminToken(
context,
adminUserAuthToken as string
)

// add Admin token metrics
let tokenMetrics: AuthAuditTokenMetrics = {
hasAdminToken,
hasValidAdminToken,
}

if (!hasAdminToken || !hasValidAdminToken) {
const { hasApiToken, hasValidApiToken } = await validateApiToken(
context
)

// add API token metrics
tokenMetrics = {
...tokenMetrics,
hasApiToken,
hasValidApiToken,
}

if (!hasApiToken || !hasValidApiToken) {
const { hasStoreToken, hasValidStoreToken } =
await validateStoreToken(context, storeUserAuthToken as string)

// add store token metrics
tokenMetrics = {
...tokenMetrics,
hasStoreToken,
hasValidStoreToken,
}

if (!hasStoreToken || !hasValidStoreToken) {
hasTokens = hasAdminToken || hasApiToken || hasStoreToken
hasValidTokens =
hasValidAdminToken || hasValidApiToken || hasValidStoreToken
}
}
}

// now we emit a metric with all the collected data before we proceed
const operation = field?.astNode?.name?.value ?? context?.request?.url
const userAgent = context?.request?.headers['user-agent'] as string
const caller = context?.request?.headers['x-vtex-caller'] as string
const forwardedHost = context?.request?.headers[
'x-forwarded-host'
] as string

const auditMetric = new AuthMetric(
context?.vtex?.account,
{
operation,
forwardedHost,
caller,
userAgent,
...tokenMetrics,
},
'ValidateUserAccessAudit'
)

sendAuthMetric(logger, auditMetric)

if (!hasTokens) {
logger.warn({
message: 'ValidateUserAccess: No token provided',
userAgent,
caller,
forwardedHost,
operation,
...tokenMetrics,
})
throw new AuthenticationError('No token was provided')
}

if (!hasValidTokens) {
logger.warn({
message: `ValidateUserAccess: Invalid token`,
userAgent,
caller,
forwardedHost,
operation,
...tokenMetrics,
})
throw new ForbiddenError('Unauthorized Access')
}

return resolve(root, args, context, info)
}
}
}
17 changes: 10 additions & 7 deletions node/metrics/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,22 @@ import type { Logger } from '@vtex/api/lib/service/logger/logger'
import type { Metric } from '../clients/metrics'
import { B2B_METRIC_NAME, sendMetric } from '../clients/metrics'

export interface AuthAuditMetric {
export interface AuthAuditTokenMetrics {
hasAdminToken: boolean
hasValidAdminToken?: boolean
hasStoreToken?: boolean
hasValidStoreToken?: boolean
hasApiToken?: boolean
hasValidApiToken?: boolean
}

export interface AuthAuditMetric extends AuthAuditTokenMetrics {
operation: string
forwardedHost: string
caller: string
userAgent: string
role?: string
permissions?: string[]
hasAdminToken: boolean
hasValidAdminToken?: boolean
hasStoreToken: boolean
hasValidStoreToken?: boolean
hasApiToken: boolean
hasValidApiToken?: boolean
}

export class AuthMetric implements Metric {
Expand Down

0 comments on commit 34d267d

Please sign in to comment.