Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add directive to validate auth token #123

Merged
merged 9 commits into from
Dec 14, 2023
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

### Added

- add directive to validate auth token for some operations

## [1.37.2] - 2023-11-10

### Fixed
17 changes: 14 additions & 3 deletions graphql/schema.graphql
Original file line number Diff line number Diff line change
@@ -33,16 +33,19 @@ type Query {
@cacheControl(scope: PRIVATE)
@withSender
@auditAccess
@checkUserAccess

getUserByEmail(email: String!): [User]
@cacheControl(scope: PRIVATE)
@withSender
@auditAccess
@checkUserAccess

listAllUsers: [User]
@cacheControl(scope: PRIVATE, maxAge: SHORT)
@withSender
@auditAccess
@checkUserAccess

listUsers(organizationId: ID, costCenterId: ID, roleId: ID): [User]
@cacheControl(scope: PRIVATE, maxAge: SHORT)
@@ -59,7 +62,9 @@ type Query {
search: String
sortOrder: String
sortedBy: String
): UserPagination @cacheControl(scope: PRIVATE, maxAge: SHORT)
): UserPagination
@cacheControl(scope: PRIVATE, maxAge: SHORT)
@checkUserAccess

checkImpersonation: UserImpersonation
@settings(settingsType: "workspace")
@@ -72,7 +77,7 @@ type Query {
@withSender
@cacheControl(scope: PRIVATE)

getSessionWatcher: Boolean @cacheControl(scope: PRIVATE)
getSessionWatcher: Boolean @cacheControl(scope: PRIVATE) @checkUserAccess

getUsersByEmail(email: String!): [User] @cacheControl(scope: PRIVATE)

@@ -90,12 +95,17 @@ type Mutation {
name: String!
slug: String
features: [FeatureInput]
): MutationResponse @cacheControl(scope: PRIVATE) @withSender @auditAccess
): MutationResponse
@cacheControl(scope: PRIVATE)
@withSender
@auditAccess
@checkUserAccess

deleteRole(id: ID!): MutationResponse
@cacheControl(scope: PRIVATE)
@withSender
@auditAccess
@checkUserAccess

saveUser(
id: ID
@@ -160,6 +170,7 @@ type Mutation {
@cacheControl(scope: PRIVATE)
@withSender
@auditAccess
@checkUserAccess
}

type UserImpersonation {
99 changes: 59 additions & 40 deletions node/directives/checkUserAccess.ts
Original file line number Diff line number Diff line change
@@ -3,6 +3,63 @@ import type { GraphQLField } from 'graphql'
import { defaultFieldResolver } from 'graphql'
import { SchemaDirectiveVisitor } from 'graphql-tools'

export async function checkUserOrAdminTokenAccess(
ctx: Context,
operation?: string
) {
const {
vtex: { adminUserAuthToken, storeUserAuthToken, logger },
clients: { identity, vtexId },
} = ctx

if (!adminUserAuthToken && !storeUserAuthToken) {
logger.warn({
message: `CheckUserAccess: No admin or store token was provided for ${operation}`,
operation,
})
throw new AuthenticationError('No admin or store token was provided')
}

if (adminUserAuthToken) {
try {
await identity.validateToken({ token: adminUserAuthToken })
} catch (err) {
logger.warn({
error: err,
message: `CheckUserAccess: Invalid admin token for ${operation}`,
operation,
token: adminUserAuthToken,
})
throw new ForbiddenError('Unauthorized Access')
}
} else if (storeUserAuthToken) {
let authUser = null

try {
authUser = await vtexId.getAuthenticatedUser(storeUserAuthToken)
if (!authUser?.user) {
logger.warn({
message: `CheckUserAccess: No valid user found by store user token for ${operation}`,
operation,
})
authUser = null
}
} catch (err) {
logger.warn({
error: err,
message: `CheckUserAccess: Invalid store user token for ${operation}`,
operation,
token: adminUserAuthToken,
})
authUser = null
}

if (!authUser) {
throw new ForbiddenError('Unauthorized Access')
}
}
}

export class CheckUserAccess extends SchemaDirectiveVisitor {
public visitFieldDefinition(field: GraphQLField<any, any>) {
const { resolve = defaultFieldResolver } = field
@@ -13,47 +70,9 @@ export class CheckUserAccess extends SchemaDirectiveVisitor {
context: Context,
info: any
) => {
const {
vtex: { adminUserAuthToken, storeUserAuthToken, logger },
clients: { identity, vtexId },
} = context
const operationName = field.astNode?.name?.value

if (!adminUserAuthToken && !storeUserAuthToken) {
throw new AuthenticationError('No admin or store token was provided')
}

if (adminUserAuthToken) {
try {
await identity.validateToken({ token: adminUserAuthToken })
} catch (err) {
logger.warn({
error: err,
message: 'CheckUserAccess: Invalid admin token',
token: adminUserAuthToken,
})
throw new ForbiddenError('Unauthorized Access')
}
} else if (storeUserAuthToken) {
let authUser = null

try {
authUser = await vtexId.getAuthenticatedUser(storeUserAuthToken)
if (!authUser?.user) {
authUser = null
}
} catch (err) {
logger.warn({
error: err,
message: 'CheckUserAccess: Invalid store user token',
token: adminUserAuthToken,
})
authUser = null
}

if (!authUser) {
throw new ForbiddenError('Unauthorized Access')
}
}
await checkUserOrAdminTokenAccess(context, operationName)

return resolve(root, args, context, info)
}