Security Guide

This document provides comprehensive security guidelines for deploying and operating the Wallet Service in production environments.

Overview

The Wallet Service handles sensitive member data and integrates with multiple external services. Proper security configuration is critical for:


Current Security Architecture

Authentication Mechanisms

Component Current Method Security Level
Apple Wallet API Bearer token in Authorization header ✅ Good
Pass Send Endpoint API key in query parameter ⚠️ Needs improvement
Salesforce Username + Password + Security Token ⚠️ Needs improvement
Google Wallet Service Account (key.json) ✅ Good
SMTP Username + App Password ✅ Acceptable

Application Security

1. Environment Variables

Never commit secrets to version control. Use environment variables for all sensitive data.

# Required secrets (never hardcode these)
AUTH_TOKEN=<secure-random-uuid>
CERT_PASSPHRASE=<certificate-passphrase>
SF_PASSWD=<salesforce-password>
SF_SECURITY_TOKEN=<salesforce-token>
SF_CONSUMER_SECRET=<oauth-secret>
SMTP_PASSWD=<smtp-password>

Best Practices:

2. AUTH_TOKEN Configuration

The AUTH_TOKEN authenticates Apple Wallet device requests.

Requirements:

Generate a secure token:

# Using Node.js
node -e "console.log(require('crypto').randomUUID())"

# Using OpenSSL
openssl rand -hex 32

# Using uuidgen
uuidgen

Current Implementation:

AUTH_TOKEN=ebee936c-46fe-42e6-9d1c-8d619cf1c53a

3. HTTPS Configuration

Always use HTTPS in production.

HTTPS=true
WEBSERVICE_URL=passes.usvjena.de

TLS Requirements:

Railway/Platform Configuration: Most platforms (Railway, Heroku, Fly.io) provide automatic TLS termination. Ensure:

4. Rate Limiting

Production rate limiting is automatically enabled:

// Current configuration
await this._instance.register(rateLimit, {
  max: 100,           // requests
  timeWindow: 2000,   // per 2 seconds
  whitelist: [env.HOST, 'localhost', '0.0.0.0', '127.0.0.1', env.WEBSERVICE_URL],
});

Recommendations:

5. Security Headers (Helmet)

The application uses Fastify Helmet for security headers:

await this._instance.register(helmet, {
  contentSecurityPolicy: {
    directives: {
      defaultSrc: [`'self'`],
      styleSrc: [`*`, 'https://fonts.googleapis.com/*', `'unsafe-inline'`],
      imgSrc: [`'self'`, 'data:', 'validator.swagger.io'],
      scriptSrc: [`'self'`, `https: 'unsafe-inline'`, `http: 'unsafe-inline'`],
      'form-action': ["'self'"],
    },
  },
});

Headers Enabled:

6. CORS Configuration

await this._instance.register(cors, {
  origin: '*',  // ⚠️ Should be restricted in production
  methods: ['GET', 'POST', 'DELETE', 'HEAD'],
  allowedHeaders: ['Content-Type', 'Authorization', 'Accept'],
});

Production Recommendation:

origin: ['https://usvjena.lightning.force.com', 'https://passes.usvjena.de'],

7. Input Validation

All endpoints use JSON Schema validation via Fastify:

// Example schema
const updatePassSchema = {
  params: {
    type: 'object',
    required: ['serialNumber'],
    properties: {
      serialNumber: { type: 'string', minLength: 1 }
    }
  },
  querystring: {
    type: 'object',
    required: ['type'],
    properties: {
      type: { type: 'string', enum: ['apple', 'google'] }
    }
  }
};

Salesforce Security

Current Authentication (Username-Password Flow)

The current implementation uses direct username/password authentication:

SF_USERNAME=c.simon@dotsource.de.usvjena
SF_PASSWD=AyvTa99Pka4KARHJ!
SF_SECURITY_TOKEN=htqruSUO0vVyBroqkWIGnA8HO

⚠️ Security Concerns:

  1. User credentials stored in environment variables
  2. Password changes require service redeployment
  3. Security token rotates on password change
  4. Tied to a specific user account (single point of failure)
  5. No token refresh - relies on long-lived sessions
  6. User account could be locked/disabled, breaking integration

Recommended: OAuth 2.0 JWT Bearer Flow

Why JWT Bearer Flow?

Implementation Steps

1. Create a Connected App in Salesforce

Navigate to: Setup → App Manager → New Connected App

Configure:

2. Generate X.509 Certificate

# Generate private key
openssl genrsa -out salesforce.key 2048

# Generate certificate (valid for 1 year)
openssl req -new -x509 -key salesforce.key -out salesforce.crt -days 365 \
  -subj "/CN=WalletService/O=USV Jena/C=DE"

# Upload salesforce.crt to Connected App in Salesforce

3. Pre-authorize the Connected App

In Salesforce Setup:

  1. Go to Manage Connected Apps
  2. Find your Connected App
  3. Click Edit Policies
  4. Set Permitted Users to "Admin approved users are pre-authorized"
  5. Add a Permission Set or Profile that allows access

4. Update Environment Configuration

# Remove these (no longer needed)
# SF_PASSWD=...
# SF_SECURITY_TOKEN=...

# Add these
SF_CONSUMER_KEY=<connected-app-consumer-key>
SF_PRIVATE_KEY_PATH=/app/certs/salesforce.key
SF_USERNAME=integration@usvjena.org  # Service account or integration user
SF_LOGIN_URL=https://login.salesforce.com
SF_AUDIENCE=https://login.salesforce.com  # or https://test.salesforce.com for sandbox

5. Implement JWT Bearer Flow

// src/lib/modules/salesforce-oauth.ts
import * as jwt from 'jsonwebtoken';
import { readFileSync } from 'node:fs';
import { environment as env } from '../../app';

interface SalesforceTokenResponse {
  access_token: string;
  instance_url: string;
  token_type: string;
}

export class SalesforceAuth {
  private accessToken: string | null = null;
  private instanceUrl: string | null = null;
  private tokenExpiry: number = 0;

  async getAccessToken(): Promise<string> {
    // Return cached token if still valid (with 5 min buffer)
    if (this.accessToken && Date.now() < this.tokenExpiry - 300000) {
      return this.accessToken;
    }

    return this.refreshToken();
  }

  private async refreshToken(): Promise<string> {
    const privateKey = readFileSync(env.SF_PRIVATE_KEY_PATH, 'utf8');
    
    const now = Math.floor(Date.now() / 1000);
    const payload = {
      iss: env.SF_CONSUMER_KEY,
      sub: env.SF_USERNAME,
      aud: env.SF_AUDIENCE,
      exp: now + 300, // 5 minutes
    };

    const assertion = jwt.sign(payload, privateKey, { algorithm: 'RS256' });

    const response = await fetch(`${env.SF_LOGIN_URL}/services/oauth2/token`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: new URLSearchParams({
        grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
        assertion,
      }),
    });

    if (!response.ok) {
      const error = await response.text();
      throw new Error(`Salesforce OAuth failed: ${error}`);
    }

    const data: SalesforceTokenResponse = await response.json();
    
    this.accessToken = data.access_token;
    this.instanceUrl = data.instance_url;
    this.tokenExpiry = Date.now() + 7200000; // 2 hours (SF default)

    return this.accessToken;
  }

  getInstanceUrl(): string {
    if (!this.instanceUrl) {
      throw new Error('Not authenticated. Call getAccessToken first.');
    }
    return this.instanceUrl;
  }
}

// Singleton instance
export const salesforceAuth = new SalesforceAuth();

6. Update Salesforce API Calls

// Before (username-password flow)
const connection = new jsforce.Connection({
  loginUrl: env.SF_LOGIN_URL,
});
await connection.login(env.SF_USERNAME, env.SF_PASSWD + env.SF_SECURITY_TOKEN);

// After (JWT Bearer flow)
import { salesforceAuth } from './salesforce-oauth';

const accessToken = await salesforceAuth.getAccessToken();
const instanceUrl = salesforceAuth.getInstanceUrl();

const connection = new jsforce.Connection({
  instanceUrl,
  accessToken,
});

Alternative: OAuth 2.0 Client Credentials Flow

For Salesforce orgs with API version 52.0+ (Winter '22):

SF_CONSUMER_KEY=<connected-app-consumer-key>
SF_CONSUMER_SECRET=<connected-app-consumer-secret>
SF_TOKEN_URL=https://login.salesforce.com/services/oauth2/token
const response = await fetch(env.SF_TOKEN_URL, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
  },
  body: new URLSearchParams({
    grant_type: 'client_credentials',
    client_id: env.SF_CONSUMER_KEY,
    client_secret: env.SF_CONSUMER_SECRET,
  }),
});

Note: Client Credentials flow requires additional Salesforce configuration and may not be available in all orgs.


Certificate Security

Apple Wallet Certificates

Store certificates securely:

src/lib/certs/
├── pass.pem          # Pass Type ID certificate
├── pass.key          # Private key (NEVER commit)
└── wwdr.pem          # Apple WWDR certificate

Best Practices:

Google Wallet Service Account

src/lib/assets/
└── key.json          # Service account credentials (NEVER commit)

Best Practices:


API Endpoint Security

Current Vulnerabilities

1. /passes/send uses query parameter for API key:

GET /passes/send?salesforceId=xxx&key=g4Rf1elD1337&type=apple

Issues:

Recommended Fix:

// Use Authorization header instead
@GET('/send')
async send(request: FastifyRequest<SendMailRequest>, reply: FastifyReply) {
  const authHeader = request.headers.authorization;
  
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return reply.status(401).send({ code: 401, message: 'Unauthorized' });
  }

  const apiKey = authHeader.slice(7);
  if (apiKey !== env.API_KEY) {
    return reply.status(401).send({ code: 401, message: 'Unauthorized' });
  }
  
  // ... rest of handler
}

2. Add API Key to Environment:

API_KEY=<secure-random-key>

Monitoring & Logging

Error Tracking (Sentry)

SENTRY_DSN=https://xxx@sentry.io/xxx

Configuration:

Security Event Logging

Log these events for security monitoring:


Security Checklist

Pre-Deployment

Post-Deployment

Regular Maintenance


Future Improvements

Short-term

  1. Move API key from query string to Authorization header
  2. Implement Salesforce OAuth 2.0 JWT Bearer flow
  3. Add request signing for critical endpoints

Medium-term

  1. Implement API key management system
  2. Add webhook signature verification
  3. Implement audit logging

Long-term

  1. Consider mTLS for service-to-service auth
  2. Implement zero-trust architecture
  3. Add security scanning to CI/CD pipeline