Web3Auth with AWS API Gateways
Handling ES256 JWT Authentication with AWS API Gateway and Web3Auth: Overcoming Limitations Without REST APIs
Introduction
Integrating third-party authentication providers like Web3Auth into your AWS serverless application can be challenging, especially when dealing with JWT tokens signed with the ES256 algorithm. AWS API Gateway’s limitations necessitate creative solutions to ensure secure and efficient authentication. This blog post delves into these challenges and presents a solution that works within the constraints of AWS HTTP APIs and Lambda functions, without resorting to REST APIs.
The Challenge with ES256 and AWS API Gateway
AWS API Gateway provides two types of APIs: HTTP APIs and REST APIs. While both offer mechanisms for authentication and authorization, they have different capabilities and limitations.
AWS API Gateway’s Native JWT Authorizers
- Unsupported Algorithms: AWS API Gateway’s native JWT authorizers do not support the ES256 algorithm used by Web3Auth. They primarily support RS256 and RS512 algorithms.
- Limitation: This means you cannot use the native JWT authorizer feature of API Gateway to validate tokens issued by Web3Auth.
Using a Custom Lambda Authorizer
To overcome the algorithm limitation, you can implement a custom Lambda authorizer that verifies JWT tokens using the ES256 algorithm.
Constraints with HTTP APIs and Lambda Authorizers
- IAM Policy Response Mode: When using a Lambda authorizer with an HTTP API in IAM Policy response mode, the context data (including
principalId
) is not passed to the backend Lambda function. - Payload Format Version 2.0: HTTP APIs use Payload Format Version 2.0, which provides a different event structure and capabilities compared to version 1.0.
Why Not Use REST APIs?
- Payload Format Version 2.0 Unavailable: REST APIs do not support Payload Format Version 2.0. If your backend Lambda functions rely on the event structure provided by Payload Format Version 2.0, switching to REST APIs is not viable.
- Different Event Structure: REST APIs use a different event format (version 1.0), which may require significant changes to your backend Lambda functions.
- Increased Complexity and Cost: REST APIs are generally more complex to configure and can be more expensive than HTTP APIs.
The Solution: Verifying JWT Tokens in the Backend Lambda Function
Given these constraints, the practical solution is to verify the JWT token within your backend Lambda function. This approach allows you to:
- Work Within AWS Limitations: Continue using HTTP APIs with Lambda authorizers in IAM Policy response mode.
- Maintain Payload Format Version 2.0: Keep using Payload Format Version 2.0 in your backend Lambda functions.
- Access User Information: Extract the
principalId
or other user information from the token.
Implementation Steps
-
Extract the JWT Token: Retrieve the token from the
Authorization
header in the incoming request. -
Verify the Token: Use libraries like
jsonwebtoken
andjwks-rsa
to verify the token’s signature using the ES256 algorithm and fetch the signing key from Web3Auth’s JWKS endpoint. -
Extract User Information: Obtain the
principalId
(e.g.,verifierId
orsub
) from the token’s payload. -
Proceed with Application Logic: Use the extracted user information for authentication and authorization in your application.
Sample Code Snippet
Here’s how you can implement the token verification in your backend Lambda function:
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
const issuer = 'https://api-auth.web3auth.io';
const jwks = jwksClient({
jwksUri: `${issuer}/.well-known/jwks.json`,
});
function getKey(header, callback) {
jwks.getSigningKey(header.kid, function (err, key) {
if (err) {
callback(err);
} else {
const signingKey = key.publicKey || key.rsaPublicKey;
callback(null, signingKey);
}
});
}
exports.handler = async (event) => {
// Extract the token from the Authorization header
const authHeader = event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
body: JSON.stringify({ message: 'Unauthorized: No Authorization header' }),
};
}
const tokenParts = authHeader.split(' ');
const token = tokenParts.length > 1 ? tokenParts[1] : tokenParts[0];
// Verify the token
try {
const decoded = await new Promise((resolve, reject) => {
jwt.verify(token, getKey, { algorithms: ['ES256'], issuer }, (err, decoded) => {
if (err) reject(err);
else resolve(decoded);
});
});
const user = decoded.verifierId || decoded.sub;
if (!user) {
throw new Error('User not found in token');
}
// Proceed with your application logic using the user information
// ...
return {
statusCode: 200,
body: JSON.stringify({ message: 'Success', user }),
};
} catch (err) {
return {
statusCode: 401,
body: JSON.stringify({ message: 'Unauthorized: Invalid token' }),
};
}
};
Advantages of This Approach
- Compatibility: Works with HTTP APIs using Payload Format Version 2.0.
- Security: Ensures that only authenticated requests are processed.
- Flexibility: Allows access to all necessary user information from the token.
Considerations
- Duplicate Token Verification: The token is verified in both the Lambda authorizer and the backend Lambda function, which may introduce slight latency.
- Code Maintenance: Ensure consistency between the token verification logic in both functions.
- Performance Impact: Monitor the performance to ensure the additional verification doesn’t adversely affect your application.
Why Not Switch to REST APIs?
While REST APIs with Lambda authorizers can pass context data (including principalId
) to the backend, they are not suitable in this scenario due to several reasons:
- Lack of Payload Format Version 2.0 Support:
- REST APIs use Payload Format Version 1.0, which has a different event structure.
- Switching would require significant refactoring of your backend Lambda functions to accommodate the different event format.
- Feature Limitations:
- Payload Format Version 2.0 includes features and improvements not available in version 1.0, such as improved HTTP request and response representations.
- Your application may rely on these features, making it impractical to downgrade.
- Increased Complexity and Cost:
- REST APIs are generally more complex to set up and manage.
- They can be more expensive due to higher per-request costs.
- No Native Support for ES256:
- Even with REST APIs, the native JWT authorizer does not support the ES256 algorithm.
- You would still need to use a custom Lambda authorizer, facing similar limitations.
Alternative Options
- Using Simple Response Mode:
- Changing the Lambda authorizer’s response mode to Simple allows passing context data to the backend.
- However, this mode does not support fine-grained access control with IAM policies.
- If your application requires IAM policies for authorization, Simple response mode may not be sufficient.
- Custom Middleware:
- Implement custom middleware within your Lambda functions to handle authentication and authorization.
- This can provide more control but requires additional development and maintenance effort.
Conclusion
Integrating Web3Auth with AWS API Gateway and Lambda functions involves navigating several limitations. By verifying JWT tokens within your backend Lambda function, you can securely extract user information while continuing to use HTTP APIs with Payload Format Version 2.0. Although this approach requires duplicate token verification, it allows you to work within AWS’s constraints and maintain the features and event structures your application depends on.
Recommendations
- Monitor Performance: Keep an eye on the latency introduced by the additional token verification to ensure it meets your application’s performance requirements.
- Shared Verification Logic: Consider abstracting the token verification logic into a shared module or library to maintain consistency and reduce code duplication.
- Stay Updated: AWS services evolve over time. Keep an eye on AWS announcements for any updates that might introduce support for ES256 in native JWT authorizers or other features that could simplify this integration.