Skip to content

Commit

Permalink
chore: add token validation logs (#137)
Browse files Browse the repository at this point in the history
  • Loading branch information
enzomerca authored Apr 29, 2024
1 parent 61c8cbc commit 2ed894f
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 4 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 logs

### Removed
- Reverted changes from versions 1.40.3, 1.40.2 and 1.40.1

Expand Down
2 changes: 2 additions & 0 deletions node/directives/auditAccess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export class AuditAccess extends SchemaDirectiveVisitor {
} = context

const operation = field.astNode?.name?.value ?? request.url
const userAgent = request.headers['user-agent'] as string
const forwardedHost = request.headers['x-forwarded-host'] as string
const caller =
context.vtex.sender ?? (request.headers['x-vtex-caller'] as string)
Expand All @@ -40,6 +41,7 @@ export class AuditAccess extends SchemaDirectiveVisitor {

const authMetric = new AuthMetric(account, {
caller,
userAgent,
forwardedHost,
hasAdminToken,
hasApiToken,
Expand Down
51 changes: 50 additions & 1 deletion node/directives/checkAdminAccess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { AuthenticationError, ForbiddenError } from '@vtex/api'
import type { GraphQLField } from 'graphql'
import { defaultFieldResolver } from 'graphql'

import sendAuthMetric, { AuthMetric } from '../metrics/auth'

export class CheckAdminAccess extends SchemaDirectiveVisitor {
public visitFieldDefinition(field: GraphQLField<any, any>) {
const { resolve = defaultFieldResolver } = field
Expand All @@ -18,16 +20,63 @@ export class CheckAdminAccess extends SchemaDirectiveVisitor {
clients: { identity },
} = context

const operation = field.astNode?.name?.value ?? context.request.url
const metric = new AuthMetric(
context.vtex.account,
{
operation,
forwardedHost: context.request.header['x-forwarded-host'] as string,
caller: context.request.header['x-vtex-caller'] as string,
userAgent: context.request.header['user-agent'] as string,
hasAdminToken: !!adminUserAuthToken,
hasStoreToken: false,
hasApiToken: false,
},
'CheckAdminAccess'
)

if (!adminUserAuthToken) {
metric.error = 'No admin token provided'
sendAuthMetric(logger, metric)
logger.warn({
message: 'CheckAdminAccess: No admin token provided',
userAgent: context.request.header['user-agent'],
vtexCaller: context.request.header['x-vtex-caller'],
forwardedHost: context.request.header['x-forwarded-host'],
operation,
})
throw new AuthenticationError('No token was provided')
}

try {
await identity.validateToken({ token: adminUserAuthToken })
const authUser = await identity.validateToken({
token: adminUserAuthToken,
})

// This is the first step before actually enabling this code.
// For now we only log in case of errors, but in follow up commits
// we should also throw an exception inside this if in case of errors
if (!authUser?.audience || authUser?.audience !== 'admin') {
metric.error = 'Token is not an admin token'
sendAuthMetric(logger, metric)
logger.warn({
message: `CheckUserAccess: Token is not an admin token`,
userAgent: context.request.header['user-agent'],
vtexCaller: context.request.header['x-vtex-caller'],
forwardedHost: context.request.header['x-forwarded-host'],
operation,
})
}
} catch (err) {
metric.error = 'Invalid token'
sendAuthMetric(logger, metric)
logger.warn({
error: err,
message: 'CheckAdminAccess: Invalid token',
userAgent: context.request.header['user-agent'],
vtexCaller: context.request.header['x-vtex-caller'],
forwardedHost: context.request.header['x-forwarded-host'],
operation,
token: adminUserAuthToken,
})
throw new ForbiddenError('Unauthorized Access')
Expand Down
91 changes: 90 additions & 1 deletion node/directives/checkUserAccess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import type { GraphQLField } from 'graphql'
import { defaultFieldResolver } from 'graphql'
import { SchemaDirectiveVisitor } from 'graphql-tools'

import { getActiveUserByEmail } from '../resolvers/Queries/Users'
import sendAuthMetric, { AuthMetric } from '../metrics/auth'

export async function checkUserOrAdminTokenAccess(
ctx: Context,
operation?: string
Expand All @@ -12,21 +15,62 @@ export async function checkUserOrAdminTokenAccess(
clients: { identity, vtexId },
} = ctx

const metric = new AuthMetric(
ctx.vtex.account,
{
operation: operation ?? ctx.request.url,
forwardedHost: ctx.request.header['x-forwarded-host'] as string,
caller: ctx.request.header['x-vtex-caller'] as string,
userAgent: ctx.request.header['user-agent'] as string,
hasAdminToken: !!adminUserAuthToken,
hasStoreToken: !!storeUserAuthToken,
hasApiToken: false,
},
'CheckUserAccess'
)

if (!adminUserAuthToken && !storeUserAuthToken) {
metric.error = 'No admin or store token was provided'
sendAuthMetric(logger, metric)
logger.warn({
message: `CheckUserAccess: No admin or store token was provided`,
userAgent: ctx.request.header['user-agent'],
vtexCaller: ctx.request.header['x-vtex-caller'],
forwardedHost: ctx.request.header['x-forwarded-host'],
operation,
})
throw new AuthenticationError('No admin or store token was provided')
}

if (adminUserAuthToken) {
try {
await identity.validateToken({ token: adminUserAuthToken })
const authUser = await identity.validateToken({
token: adminUserAuthToken,
})

// This is the first step before actually enabling this code.
// For now we only log in case of errors, but in follow up commits
// we should also throw an exception inside this if in case of errors
if (!authUser?.audience || authUser?.audience !== 'admin') {
metric.error = 'Token is not an admin token'
sendAuthMetric(logger, metric)
logger.warn({
message: `CheckUserAccess: Token is not an admin token`,
userAgent: ctx.request.header['user-agent'],
vtexCaller: ctx.request.header['x-vtex-caller'],
forwardedHost: ctx.request.header['x-forwarded-host'],
operation,
})
}
} catch (err) {
metric.error = 'Invalid admin token'
sendAuthMetric(logger, metric)
logger.warn({
error: err,
message: `CheckUserAccess: Invalid admin token`,
userAgent: ctx.request.header['user-agent'],
vtexCaller: ctx.request.header['x-vtex-caller'],
forwardedHost: ctx.request.header['x-forwarded-host'],
operation,
})
throw new ForbiddenError('Unauthorized Access')
Expand All @@ -37,16 +81,61 @@ export async function checkUserOrAdminTokenAccess(
try {
authUser = await vtexId.getAuthenticatedUser(storeUserAuthToken)
if (!authUser?.user) {
metric.error = 'No valid user found by store user token'
sendAuthMetric(logger, metric)
logger.warn({
message: `CheckUserAccess: No valid user found by store user token`,
userAgent: ctx.request.header['user-agent'],
vtexCaller: ctx.request.header['x-vtex-caller'],
forwardedHost: ctx.request.header['x-forwarded-host'],
operation,
})
authUser = null
} else {
// This is the first step before actually enabling this code.
// For now we only log in case of errors, but in follow up commits
// we will remove this additional try/catch and set authUser = null
// in case of errors
try {
const user = (await getActiveUserByEmail(
null,
{ email: authUser?.user },
ctx
)) as { roleId: string } | null

if (!user?.roleId) {
metric.error = 'No active user found by store user token'
sendAuthMetric(logger, metric)
logger.warn({
message: `CheckUserAccess: No active user found by store user token`,
userAgent: ctx.request.header['user-agent'],
vtexCaller: ctx.request.header['x-vtex-caller'],
forwardedHost: ctx.request.header['x-forwarded-host'],
operation,
})
}
} catch (err) {
metric.error = 'Error getting user by email'
sendAuthMetric(logger, metric)
logger.warn({
error: err,
message: `CheckUserAccess: Error getting user by email`,
userAgent: ctx.request.header['user-agent'],
vtexCaller: ctx.request.header['x-vtex-caller'],
forwardedHost: ctx.request.header['x-forwarded-host'],
operation,
})
}
}
} catch (err) {
metric.error = 'Invalid store user token'
sendAuthMetric(logger, metric)
logger.warn({
error: err,
message: `CheckUserAccess: Invalid store user token`,
userAgent: ctx.request.header['user-agent'],
vtexCaller: ctx.request.header['x-vtex-caller'],
forwardedHost: ctx.request.header['x-forwarded-host'],
operation,
})
authUser = null
Expand Down
6 changes: 4 additions & 2 deletions node/metrics/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface AuthAuditMetric {
operation: string
forwardedHost: string
caller: string
userAgent: string
role?: string
permissions?: string[]
hasAdminToken: boolean
Expand All @@ -20,12 +21,13 @@ export class AuthMetric implements Metric {
public readonly account: string
public readonly fields: AuthAuditMetric
public readonly name = B2B_METRIC_NAME
public error?: string

constructor(account: string, fields: AuthAuditMetric) {
constructor(account: string, fields: AuthAuditMetric, description?: string) {
this.account = account
this.fields = fields
this.kind = 'b2b-storefront-permissions-auth-event'
this.description = 'Auth metric event'
this.description = description ?? 'Auth metric event'
}
}

Expand Down

0 comments on commit 2ed894f

Please sign in to comment.