Hi there, I'm new in sst I'm looking for a way to ...
# help
j
Hi there, I'm new in sst I'm looking for a way to add an api key to an api stack Let say I have this basic example:
Copy code
export default class MyStack extends sst.Stack {
  constructor(scope, id, props) {
    super(scope, id, props);

    // Create an HTTP API
    const api = new sst.Api(this, "Api", {
      routes: {
        "GET /": "src/lambda.handler",
      },
    });

    // Show the endpoint in the output
    this.addOutputs({
      "ApiEndpoint": api.url,
    });
  }
}
Is it possible to implement an api key to this api in the latest version? Thanks!
r
I don’t think you can use API keys on the new HTTP API, they’re only valid for the older REST API unfortunately
d
API Keys are part of the
Usage Plan
feature of REST API Gateways, you are u…thanks Ross.
r
Funny, we were lamenting this ourselves about an hour ago
d
Out of all the features that extra $2.50/M covers, I think this is the easiest to build and get right.
gives you additional flexibility too
j
Ok thanks for the answer I'll take a look at the older rest api
s
@Derek Kershner Funny you should mention this. I inherited an application that implements it's own API Key management instead of leveraging APIGW Usage Plans. That pattern kinda bugged me, since it duplicated functionality offered by APIGW out of the box with no clear value added, but I may have discounted the flexibility you are mentioning. What kind of additional flexibility do you get by creating a custom implementation?
d
If you think of the functionality more like “Rate-Limiting” and less like “Authentication” it may help you picture ways, but for us specifically, we are able to use the same functionality to protect the route where you get tokens (using secrets), that has no API Key associated with it, as we use to protect the routes where you DO have tokens present. (@Seth Geoghegan)
r
I thought about a custom implementation myself, very simple, basically just allowing requests with the correct api key in and rejecting the rest via a custom authoriser. The only issue with this, I think, is that you’re having a lambda called to authenticate each request whereas if the OOTB API Key/Usage Plan was used, it’d never get that far. In a DDOS situation, that could be pretty important, particularly from a cost perspective
d
Manual implementation does have that downside @Ross Coundon, you can store rate limited folks locally, but still inside a lambda. Lambda’s pretty cheap though, even in DDOS, and for DDOS I would want WAF on my side anyway (outside APIG entirely)
Now, lack of WAF support is another argument for REST Api…of course, been waiting on HTTP support for awhile.
r
That’s true. WAF is something I want to implement but last time I looked into it the documentation around rule definition was impenetrable for my simple brain.
Yeah, don’t you need to do something like stick CloudFront in front of the HTTP API to take advantage of WAF?
d
yessir
r
It all starts to seem quite unwieldly
d
agreed, tradeoffs.
l
We're using custom auth lambda (as mentioned above), with straightforward rate limiting and ddos protection by cloudflare
But those endpoints are only used internally
Not much traffic
d
Cloudflare’s WAF has severe limitations surrounding rules, but is good so long as you dont care about them, or can pay the equally severe fees to have them removed (~$3K/mo).
l
Ya, yet again, tradeoffs 😄
Everything's about your usecase
r
@Derek Kershner don’t suppose you have any links to useful docs for CDK+WAF+CloudFront+HTTP API ?
l
I think there's a mid-priority task in SST repo to document an integration like this
d
I don’t, and chose not to go that route and inside just bear the brunt using our own Rate Limiter, and cross fingers AWS gets moving on WAF + HTTP.
l
Lemme check if it was resolved
d
It doesnt seem too bad though, Ross, you just have to implement Cft using the HTTP API endpoint, and deal with…WAF (as you humbly noted above).
Just to round out the lineup above, even AWS Shield Advanced requires Cloudfront to be present (and doesnt cover REST APIs).
@Ross Coundon, I am essentially currently running under the assumption that WAF will protect our frontends and for the APIs, we are just gonna eat a bunch of APIG/lambda costs if we get attacked, and assume that AWS can outscale the attack. This might be wrong, but I figure we would be eating a ton of cost under normal circumstances to prepare…so…balance?
r
Yeah, as lambda is so cheap from an invocation perspective, I think that’s a fair compromise
d
Thinking this through, we will still be vulnerable to the HTTP API throttling taking down the API…AWS needs to implement WAF support, lol. Perhaps Lukasz idea until they do (or cloudfront)
f
@Ross Coundon a side note
having a lambda called to authenticate each request
Setting
resultsCacheTtl
cache should help? https://docs.serverless-stack.com/constructs/v1/Api#resultscachettl
r
@Frank I’ve had weird results with auth caching, I couldn’t get any consistency when I last tried it, often failing auth when requests happen in quick succession. Didn’t delve into it though.
@Derek Kershner @Lukasz K is there a way to implement the api key check in a lambda authorizer? Looking at the types for
APIGatewayTokenAuthorizerEvent
it seems it only receives the authorizationToken and not the headers. If not, are you doing the header checks in the actual lambda using some middleware (or as a first check)?
Should I be using
APIGatewayRequestAuthorizerEventV2
via handler type
APIGatewayRequestSimpleAuthorizerHandlerV2
?
l
For apigw2 I'm using sth like this:
Copy code
const boaAuthorizer = new HttpLambdaAuthorizer("boaTokenVerifier", boaAuthVerifier,
            {
                resultsCacheTtl: Duration.hours(1),
                responseTypes: [HttpLambdaResponseType.SIMPLE],
                authorizerName: 'boaTokenVerifier',
                identitySource: ['$request.header.authToken']
            }
        );
APi def:
Copy code
defaultAuthorizationType: ApiAuthorizationType.CUSTOM,
defaultAuthorizer: boaAuthorizer,
with the actual function being (shorthand):
Copy code
export async function verifyToken(event): Promise<{isAuthorized: boolean, context: unknown}> {
    const response = {
        "isAuthorized": false,
        "context": {}
    };
    try {
        if (!tokenId) {
            <grab actual token from secrets>
        }
        response.isAuthorized = tokenId === event.headers.authToken;
        return response;
    } catch (e) {
        console.log(e);
        return response;
    }
Where my authToken rotates in SecretStore
r
Fantastic, thank you
What type is that event
Copy code
verifyToken(event)
or what interface is the
verifyToken
function implementing?
l
Will respond asap, got a meeting now 😅
r
Of course, no problem!
l
Did a quick glance in between talks and looks like I had an issue with finding the correct types for this one
sadly I left it as is. No aws docs specified the correct types, the only info is a result object: https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-lambda-authorizer.html#http-api-lambda-authorizer.example-code isAuthorized is a required boolean that actually decides whether Api should let you pass, context can have arbritrary set of key:value elements
APIGatewayAuthorizerEvent / APIGatewayAuthorizerResult didn't seem correct for a Simple authorizer
r
Glad it’s not just me 😄 Thank you, I’ll do some tinkering
d
I use JWT Authorizer.
r
What I’m mainly trying to do is to add a custom header check for an API Key on a public endpoint. When you say you’re using a JWT Authorizer, are you able to interrogate headers in that?
j
Finally I've decided to use a
HttpLambdaAuthorizer
this way:
Copy code
import * as sst from "@serverless-stack/resources";
import * as authorizers from "@aws-cdk/aws-apigatewayv2-authorizers";

export default class MyStack extends sst.Stack {
  constructor(scope, id, props) {
    super(scope, id, props);

    const authorizer = new authorizers.HttpLambdaAuthorizer({
      authorizerName: "LambdaAuthorizer",
      identitySource: ["$request.header.API-Key"],
      responseTypes: [authorizers.HttpLambdaResponseType.SIMPLE],
      handler: new sst.Function(this, "Authorizer", {
        handler: "src/authorizer.main",
      }),
    });

    // Create an HTTP API
    const api = new sst.Api(this, "Api", {
      defaultAuthorizationType: sst.ApiAuthorizationType.CUSTOM,
      defaultAuthorizer: authorizer,
      routes: {
        "GET /": "src/lambda.handler",
      },
    });

    // Show the endpoint in the output
    this.addOutputs({
      ApiEndpoint: api.url,
    });
  }
}
and a simple authorizer lambda:
Copy code
export const main = async (event) => {
  const key = event.headers['api-key'];
  return {
    isAuthorized: key === process.env.API_KEY,
  };
};
Not sure if it's acceptable, it works and fits that I was looking for, some straightforward way to protect my endpoints for a demo project Thanks for all your comments
d
We got distracted, glad you were able to solve Jose.
r
Does that work even if there's no JWT?
d
I dunno, I use JWTs.
but probably not, its pretty specific.
r
Right. What I have is an endpoint with no authentication which is effectively SMS'd or emailed to people. Each recipient has a unique token that's sent as a query parameter when they click the link thei receive which is used to retrieve their specific details. Our customer is the service provider for those customers so I'd like to have an API key per service provider which is sent from the front end by each of their end customers' requests.
So I was looking at some kind of custom authorizer that could check that api key. @Lukasz K posted some ideas earlier I'm going to try
r
That's cool. Just thought it was about time I actually explained myself 😄
d
there is really no reason to check an API Key sent by an unauthenticated front end, though. You could probably just base64 encode some ids and call it a “key”.
if you just want a separate lambda to handle that, it will work, though, just not much benefit to the authorizer being separate
r
I dunno. My thinking is it's another way to compartmentalise our customers, we could centralise some usage stats and also prevent randoms from calling the endpoint
d
Maybe just give this a quick read, and then implement if you still agree with yourself (short, great article on authorizers)
r
Will do, thank you.
l
My usecase was pretty simple and straightforward but I see it being extended by @Ross Coundon to incorporate multiple token verifications + usage analysis + call throttling (monthly restrictions mby? Since you mentioned providing tokens for an ext service that feeds it to end user) that's centralized for particular set of endpoints
Sure, you could always just opt for ApiGw MK1 and have it available out of the box for a slightly higher cost per call
r
Yeah, that’s what I’m thinking, it’s not really cost effective to roll my own
l
@Ross Coundon could you share your final choice (and arguments :D) for the API key auth? Looks like I'll be facing similar decision in a moment, where my naive implementation doesn't suffice anymore
r
I haven’t made one yet, it’s on the list 😄
My gut feeling is to use the V1 API with the built in capability for those endpoints but need to validate that thinking