Accessing AWS AppSync from Batch Programs with JWT Authentication

Implementing JWT Token Authentication

When building batch processing systems that need to interact with AWS AppSync GraphQL APIs protected by Amazon Cognito, you’ll need to handle authentication differently than typical client applications. This article explores the patterns, authentication mechanisms, and implementation strategies for batch programs accessing Cognito-protected AppSync APIs.

Understanding the Challenge

Batch programs operate without user interaction and need programmatic access to GraphQL operations that are typically protected by Cognito User Pool authentication. Unlike Lambda functions that can use IAM roles, batch programs must authenticate as either:

  1. Service accounts (Cognito users with programmatic credentials)
  2. Machine-to-machine authentication using JWT tokens
  3. Admin-level access with elevated permissions

Authentication Architecture Overview

Cognito User Pool Authentication Flow

When AppSync operations are protected with @auth(rules: [{ allow: owner }]) or similar Cognito-based rules, the API expects:

  • Authorization Header: Authorization: Bearer <JWT_TOKEN>
  • Valid JWT Token: Signed by Cognito User Pool
  • User Context: Token must contain valid user claims and groups

Batch Program Authentication Options

Option 1: Service Account Authentication Create dedicated Cognito users for batch operations with appropriate permissions.

Option 2: Admin Authentication Use Cognito Admin APIs to generate tokens for system-level access.

Option 3: Custom Authorizer Implement custom authentication logic for machine-to-machine scenarios.

Implementation Strategies

1. Service Account with Username/Password Authentication

Create a dedicated Cognito user for your batch program:

javascript

const AWS = require(‘aws-sdk’);

const AmazonCognitoIdentity = require(‘amazon-cognito-identity-js’);

const fetch = require(‘node-fetch’);

class AppSyncBatchClient {

  constructor(config) {

    this.userPoolId = config.userPoolId;

    this.clientId = config.clientId;

    this.username = config.serviceAccountUsername;

    this.password = config.serviceAccountPassword;

    this.graphqlEndpoint = config.graphqlEndpoint;

    this.region = config.region;

  }

  async authenticate() {

    const userPool = new AmazonCognitoIdentity.CognitoUserPool({

      UserPoolId: this.userPoolId,

      ClientId: this.clientId

    });

    const user = new AmazonCognitoIdentity.CognitoUser({

      Username: this.username,

      Pool: userPool

    });

    const authDetails = new AmazonCognitoIdentity.AuthenticationDetails({

      Username: this.username,

      Password: this.password

    });

    return new Promise((resolve, reject) => {

      user.authenticateUser(authDetails, {

        onSuccess: (result) => {

          const accessToken = result.getAccessToken().getJwtToken();

          const idToken = result.getIdToken().getJwtToken();

          resolve({ accessToken, idToken });

        },

        onFailure: (err) => reject(err),

        newPasswordRequired: () => reject(new Error(‘Password change required’))

      });

    });

  }

  async executeGraphQL(query, variables = {}) {

    const tokens = await this.authenticate();

    const response = await fetch(this.graphqlEndpoint, {

      method: ‘POST’,

      headers: {

        ‘Content-Type’: ‘application/json’,

        ‘Authorization’: `Bearer ${tokens.idToken}`, // Use ID token for Cognito auth

        ‘x-api-key’: this.apiKey // If API key is also required

      },

      body: JSON.stringify({

        query: query,

        variables: variables

      })

    });

    if (!response.ok) {

      throw new Error(`GraphQL request failed: ${response.status} ${response.statusText}`);

    }

    const result = await response.json();

    if (result.errors) {

      throw new Error(`GraphQL errors: ${JSON.stringify(result.errors)}`);

    }

    return result.data;

  }

}

// Usage example

const batchClient = new AppSyncBatchClient({

  userPoolId: ‘us-east-1_ABC123’,

  clientId: ‘your-app-client-id’,

  serviceAccountUsername: ‘batch-service-account’,

  serviceAccountPassword: ‘secure-password’,

  graphqlEndpoint: ‘https://your-api.appsync-api.us-east-1.amazonaws.com/graphql’,

  region: ‘us-east-1’

});

// Execute batch operations

async function runBatchJob() {

  try {

    const query = `

      query ListItems {

        listItems {

          items {

            id

            name

            status

          }

        }

      }

    `;

    const result = await batchClient.executeGraphQL(query);

    console.log(‘Batch job results:’, result);

    // Process results and perform mutations

    for (const item of result.listItems.items) {

      if (item.status === ‘PENDING’) {

        await processItem(item.id);

      }

    }

  } catch (error) {

    console.error(‘Batch job failed:’, error);

  }

}

2. Admin API Authentication (Server-Side Only)

For server-side batch programs with elevated privileges:

javascript

const AWS = require(‘aws-sdk’);

class AdminAppSyncClient {

  constructor(config) {

    this.cognitoIdentityServiceProvider = new AWS.CognitoIdentityServiceProvider({

      region: config.region

    });

    this.userPoolId = config.userPoolId;

    this.username = config.adminUsername;

    this.graphqlEndpoint = config.graphqlEndpoint;

  }

  async getAdminAccessToken() {

    const params = {

      UserPoolId: this.userPoolId,

      Username: this.username,

      AuthFlow: ‘ADMIN_NO_SRP_AUTH’,

      AuthParameters: {

        USERNAME: this.username,

        PASSWORD: process.env.ADMIN_PASSWORD

      }

    };

    try {

      const result = await this.cognitoIdentityServiceProvider.adminInitiateAuth(params).promise();

      return result.AuthenticationResult.IdToken;

    } catch (error) {

      console.error(‘Admin authentication failed:’, error);

      throw error;

    }

  }

  async executeAdminGraphQL(query, variables = {}) {

    const token = await this.getAdminAccessToken();

    const response = await fetch(this.graphqlEndpoint, {

      method: ‘POST’,

      headers: {

        ‘Content-Type’: ‘application/json’,

        ‘Authorization’: `Bearer ${token}`,

      },

      body: JSON.stringify({

        query: query,

        variables: variables

      })

    });

    const result = await response.json();

    if (result.errors) {

      throw new Error(`Admin GraphQL errors: ${JSON.stringify(result.errors)}`);

    }

    return result.data;

  }

}

3. Token Management and Caching

Implement token caching to avoid repeated authentication:

javascript

class TokenManager {

  constructor() {

    this.tokenCache = new Map();

  }

  isTokenValid(token) {

    if (!token) return false;

    try {

      const payload = JSON.parse(Buffer.from(token.split(‘.’)[1], ‘base64’).toString());

      const exp = payload.exp * 1000; // Convert to milliseconds

      return Date.now() < (exp – 60000); // 1 minute buffer

    } catch (error) {

      return false;

    }

  }

  async getValidToken(authClient) {

    const cacheKey = `${authClient.username}-${authClient.userPoolId}`;

    const cachedToken = this.tokenCache.get(cacheKey);

    if (this.isTokenValid(cachedToken)) {

      return cachedToken;

    }

    // Token expired or doesn’t exist, get new one

    const tokens = await authClient.authenticate();

    const idToken = tokens.idToken;

    this.tokenCache.set(cacheKey, idToken);

    return idToken;

  }

}

GraphQL Schema Considerations

Permission Models for Batch Operations

Design your GraphQL schema to accommodate batch operations:

graphql

type Item @model @auth(rules: [

  { allow: owner },

  { allow: groups, groups: [“BatchProcessor”] },

  { allow: groups, groups: [“Admin”] }

]) {

  id: ID!

  name: String!

  status: ItemStatus!

  owner: String

  batchProcessed: Boolean

  lastProcessedAt: AWSDateTime

}

type BatchOperation @model @auth(rules: [

  { allow: groups, groups: [“BatchProcessor”, “Admin”] }

]) {

  id: ID!

  operationType: String!

  itemsProcessed: Int

  startTime: AWSDateTime

  endTime: AWSDateTime

  status: BatchStatus!

}

enum ItemStatus {

  PENDING

  PROCESSING

  COMPLETED

  FAILED

}

enum BatchStatus {

  RUNNING

  COMPLETED

  FAILED

}

Batch-Friendly Mutations

Create mutations optimized for batch processing:

graphql

type Mutation {

  processBatch(input: ProcessBatchInput!): ProcessBatchOutput

    @auth(rules: [{ allow: groups, groups: [“BatchProcessor”] }])

  updateItemsBatch(items: [UpdateItemInput!]!): [Item]

    @auth(rules: [{ allow: groups, groups: [“BatchProcessor”] }])

}

input ProcessBatchInput {

  batchId: ID!

  itemIds: [ID!]!

  processingType: String!

}

type ProcessBatchOutput {

  batchId: ID!

  processedCount: Int!

  failedCount: Int!

  errors: [BatchError]

}

type BatchError {

  itemId: ID!

  errorMessage: String!

  errorCode: String

}

Security Best Practices

1. Service Account Security

Dedicated User Pool Groups

javascript

// Create dedicated groups for batch operations

const createBatchGroup = {

  GroupName: ‘BatchProcessor’,

  UserPoolId: userPoolId,

  Description: ‘Group for automated batch processing systems’,

  Precedence: 1

};

Least Privilege Access

graphql

type Query {

  # Only allow batch processors to access batch-specific queries

  listPendingItems: [Item] 

    @auth(rules: [{ allow: groups, groups: [“BatchProcessor”] }])

  getBatchStatus(batchId: ID!): BatchOperation

    @auth(rules: [{ allow: groups, groups: [“BatchProcessor”] }])

}

2. Token Security

Environment Variable Management

javascript

// Use environment variables for sensitive data

const config = {

  userPoolId: process.env.COGNITO_USER_POOL_ID,

  clientId: process.env.COGNITO_CLIENT_ID,

  serviceAccountUsername: process.env.BATCH_SERVICE_USERNAME,

  serviceAccountPassword: process.env.BATCH_SERVICE_PASSWORD,

  graphqlEndpoint: process.env.APPSYNC_GRAPHQL_ENDPOINT

};

Token Rotation

javascript

class SecureTokenManager extends TokenManager {

  async rotateServiceAccountPassword() {

    // Implement password rotation logic

    const newPassword = this.generateSecurePassword();

    await this.cognitoClient.adminSetUserPassword({

      UserPoolId: this.userPoolId,

      Username: this.serviceUsername,

      Password: newPassword,

      Permanent: true

    }).promise();

    // Update environment/secrets manager

    await this.updateStoredPassword(newPassword);

  }

}

3. Error Handling and Monitoring

Comprehensive Error Handling

javascript

class BatchProcessor {

  async processWithRetry(operation, maxRetries = 3) {

    for (let attempt = 1; attempt <= maxRetries; attempt++) {

      try {

        return await operation();

      } catch (error) {

        console.error(`Attempt ${attempt} failed:`, error.message);

        if (error.message.includes(‘UnauthorizedException’)) {

          // Clear cached tokens and re-authenticate

          this.tokenManager.clearCache();

        }

        if (attempt === maxRetries) {

          throw new Error(`Operation failed after ${maxRetries} attempts: ${error.message}`);

        }

        // Exponential backoff

        await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));

      }

    }

  }

}

Monitoring and Logging

javascript

class BatchMonitor {

  async logBatchOperation(operation, result, duration) {

    const logEntry = {

      timestamp: new Date().toISOString(),

      operation: operation,

      success: !result.errors,

      duration: duration,

      itemsProcessed: result.itemsProcessed || 0,

      errors: result.errors || []

    };

    // Send to CloudWatch, DataDog, etc.

    await this.sendToMonitoringService(logEntry);

  }

}

Performance Optimization

1. Batch Query Optimization

GraphQL Query Batching

javascript

// Instead of multiple individual queries

const optimizedBatchQuery = `

  query BatchQuery($itemIds: [ID!]!) {

    items: getItemsBatch(ids: $itemIds) {

      id

      name

      status

      metadata

    }

    pendingBatches: listBatchOperations(

      filter: { status: { eq: RUNNING } }

    ) {

      items {

        id

        startTime

        itemsProcessed

      }

    }

  }

`;

Connection Pooling

javascript

class AppSyncConnectionPool {

  constructor(config) {

    this.config = config;

    this.connectionPool = [];

    this.maxConnections = config.maxConnections || 10;

  }

  async getConnection() {

    if (this.connectionPool.length > 0) {

      return this.connectionPool.pop();

    }

    return new AppSyncBatchClient(this.config);

  }

  releaseConnection(client) {

    if (this.connectionPool.length < this.maxConnections) {

      this.connectionPool.push(client);

    }

  }

}

2. Parallel Processing

Concurrent Batch Processing

javascript

async function processBatchConcurrently(items, batchSize = 50, concurrency = 5) {

  const batches = [];

  for (let i = 0; i < items.length; i += batchSize) {

    batches.push(items.slice(i, i + batchSize));

  }

  const semaphore = new Semaphore(concurrency);

  const results = await Promise.allSettled(

    batches.map(async (batch) => {

      await semaphore.acquire();

      try {

        return await processBatch(batch);

      } finally {

        semaphore.release();

      }

    })

  );

  return results;

}

Deployment Considerations

Container/ECS Deployment

Dockerfile Example

dockerfile

FROM node:16-alpine

WORKDIR /app

COPY package*.json ./

RUN npm ci –only=production

COPY . .

# Use non-root user for security

USER node

CMD [“node”, “batch-processor.js”]

Environment Configuration

yaml

# docker-compose.yml or ECS task definition

environment:

  – COGNITO_USER_POOL_ID=us-east-1_ABC123

  – COGNITO_CLIENT_ID=your-client-id

  – BATCH_SERVICE_USERNAME=batch-processor

  – BATCH_SERVICE_PASSWORD_ARN=arn:aws:secretsmanager:us-east-1:123456789:secret:batch-password

  – APPSYNC_GRAPHQL_ENDPOINT=https://api.appsync-api.us-east-1.amazonaws.com/graphql

  – LOG_LEVEL=info

Lambda Batch Processing

For Lambda-based batch processing:

javascript

// Lambda function for batch processing

exports.handler = async (event) => {

  const batchClient = new AppSyncBatchClient({

    // Configuration from environment variables

  });

  try {

    // Process items from SQS, S3, or scheduled event

    const items = event.Records || event.items;

    const results = await Promise.allSettled(

      items.map(item => processItem(item, batchClient))

    );

    return {

      statusCode: 200,

      body: JSON.stringify({

        processed: results.filter(r => r.status === ‘fulfilled’).length,

        failed: results.filter(r => r.status === ‘rejected’).length

      })

    };

  } catch (error) {

    console.error(‘Batch processing failed:’, error);

    throw error;

  }

};

Conclusion

Accessing AWS AppSync from batch programs with JWT authentication requires careful consideration of authentication patterns, security practices, and performance optimization. Key takeaways include:

  1. Use dedicated service accounts with appropriate Cognito User Pool groups for batch operations
  2. Implement proper token management with caching and rotation strategies
  3. Design GraphQL schemas that accommodate batch processing patterns
  4. Follow security best practices including least privilege access and comprehensive error handling
  5. Optimize for performance with connection pooling, batching, and parallel processing

By following these patterns and best practices, you can build robust batch processing systems that securely and efficiently interact with Cognito-protected AppSync GraphQL APIs.

Remember to monitor your batch operations, implement proper error handling, and regularly review security configurations to ensure your system remains secure and performant as it scales.