Skip to content

Instantly share code, notes, and snippets.

@brhx
Created November 16, 2024 21:02
Show Gist options
  • Save brhx/94319a447af71550ee4e2fa396ee35b5 to your computer and use it in GitHub Desktop.
Save brhx/94319a447af71550ee4e2fa396ee35b5 to your computer and use it in GitHub Desktop.
AppSync events JWT authorizer that works just like Ably
import crypto from "crypto";
/**
* @typedef {Object} RequestContext
* @property {string} apiId - The API Gateway ID
* @property {string} accountId - The AWS account ID
* @property {string} requestId - The unique request ID
* @property {'EVENT_CONNECT'|'EVENT_SUBSCRIBE'|'EVENT_PUBLISH'} operation - The WebSocket operation
* @property {string} channelNamespaceName - The channel namespace
* @property {string} channel - The full channel path
*/
/**
* @typedef {Object} Event
* @property {string} authorizationToken - The authorization token
* @property {RequestContext} requestContext - The request context
* @property {Object.<string,string>} requestHeaders - The request headers
*/
/**
* @typedef {Object} Response
* @property {boolean} isAuthorized - Whether the request is authorized
* @property {Record<string,string>} [handlerContext] - Optional context to pass to the handler
* @property {number} [ttlOverride] - Optional TTL override in seconds
*/
/**
* @param {Event} event
* @returns {Promise<Response>}
*/
export async function handler(event) {
const [authMethod, authToken] = event.authorizationToken.split(" ");
if (authMethod !== "Bearer" || !authToken) return { isAuthorized: false };
if (!process.env.TOKEN) {
console.error("Missing TOKEN env var");
return { isAuthorized: false };
}
// The secret can be provided as admin for non-jwt based auth
if (authToken === process.env.TOKEN) return { isAuthorized: true };
const payload = verifyJWT(authToken, process.env.TOKEN);
if (!payload) return { isAuthorized: false };
switch (event.requestContext.operation) {
case "EVENT_CONNECT":
return { isAuthorized: true };
case "EVENT_SUBSCRIBE":
return {
isAuthorized:
payload.subscribe?.some((scope) =>
matchesScope(scope, event.requestContext.channel)
) ?? false,
};
case "EVENT_PUBLISH":
return {
isAuthorized:
payload.publish?.some((scope) =>
matchesScope(scope, event.requestContext.channel)
) ?? false,
};
}
}
/**
* @typedef {Object} TokenPayload
* @property {string[]} [subscribe] - Array of channel patterns that can be subscribed to
* @property {string[]} [publish] - Array of channel patterns that can be published to
*/
/**
* Verifies a JWT token using HMAC SHA-256
* @param {string} token - The JWT token to verify
* @param {string} secret - The secret key
* @returns {TokenPayload|null} The decoded payload or null if invalid
*/
function verifyJWT(token, secret) {
try {
const [headerB64, payloadB64, signatureB64] = token.split(".");
if (!headerB64 || !payloadB64 || !signatureB64) return null;
// Verify signature
const data = `${headerB64}.${payloadB64}`;
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(data)
.digest("base64url");
if (signatureB64 !== expectedSignature) return null;
// Decode payload
const payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString());
// Check expiration
if (payload.exp && Date.now() >= payload.exp * 1000) return null;
return payload;
} catch {
return null;
}
}
/**
* Checks if a channel scope matches another channel scope, supporting wildcards
* @param {string} scope - The permission scope to check (e.g. /default/test/*)
* @param {string} channel - The channel to check against (e.g. /default/test/123)
* @returns {boolean} Whether the scope matches the channel
*/
function matchesScope(scope, channel) {
const scopeParts = scope.split("/").filter(Boolean);
const channelParts = channel.split("/").filter(Boolean);
for (let i = 0; i < scopeParts.length; i++) {
if (scopeParts[i] === "*") return true;
if (channelParts[i] !== scopeParts[i]) return false;
}
return scopeParts.length === channelParts.length;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment