nlang / serverless-offline-local-authorizers-plugin

Plugin for adding local authorizers when developing locally with serverless-offline
MIT License
16 stars 22 forks source link

Hard-coded runtime of nodejs12.x not recognised in Serverless 3.32.0 and above #44

Open evanjd opened 1 year ago

evanjd commented 1 year ago

In Serverless 3.32.0, support for the nodejs12.x runtime was dropped.

This causes this plugin to become inoperable, as the runtime of nodejs12.x is hard-coded into the latest npm release of this plugin. I've noticed that the hard-coded runtime was removed in this PR. However, it has not been released to npm.

jmferreiratech commented 1 year ago

@evanjd, if you still need it:

yarn add -D jmferreiratech/serverless-offline-local-authorizers-plugin#v1.3.0
dmitriy-baltak commented 9 months ago

For anyone having issues with this plugin since it's not actively maintained you can disable authorizers on local for serverless-offline itself as an alternative for using this plugin:

custom:
  stage: ${opt:stage, self:provider.stage}
  region: ${opt:region, self:provider.region}
  ...
  serverless-offline:
    noAuth: true # <-- this
visrut-at-handldigital commented 3 months ago

If you want to just override for the sake of development purposes and want to get a feedback loop locally, don't use this plugin.

I am using serverless@3.33.0 and serverless-offline@14.0.0 what I did is create one typescript file at the root level of the project named: localAuthorizer.ts and then exported one function like the below, note that I have implemented cognito locally

//localAuthorizer.ts
require('dotenv').config();
const jwt = require('jsonwebtoken');
const axios = require('axios');
const jwkToPem = require('jwk-to-pem');

const region = process.env.AWS_DEFAULT_REGION;
const userPoolId = process.env.COGNITO_USER_POOL_ID;
const appClientId = process.env.COGNITO_APP_CLIENT_ID;

const getPublicKeys = async () => {
  const url = `https://cognito-idp.${region}.amazonaws.com/${userPoolId}/.well-known/jwks.json`;
  const response = await axios.get(url);

  return response.data.keys as {
    alg: string;
    kty: string;
    kid: string;
    x5c: string;
  }[];
};

const verifyToken = async (token: string) => {
  try {
    const decodedToken = jwt.decode(token, { complete: true });
    const kid = decodedToken.header.kid;

    const keys = await getPublicKeys();
    const key = keys.find((k) => k.kid === kid);

    if (!key) {
      throw new Error('Public key not found.');
    }

    let publicKey;
    if (key.x5c && key.x5c.length > 0) {
      publicKey = `-----BEGIN CERTIFICATE-----\n${key.x5c[0]}\n-----END CERTIFICATE-----`;
    } else {
      // Convert JWK to PEM format if x5c is not available
      publicKey = jwkToPem(key);
    }

    const verified = jwt.verify(token, publicKey, {
      audience: appClientId,
      issuer: `https://cognito-idp.${region}.amazonaws.com/${userPoolId}`,
    });

    return verified;
  } catch (error) {
    console.error('Token verification error:', error);
    throw new Error('Unauthorized');
  }
};

const mylocalAuthProxyFn = async (event, context) => {
  const token = event.headers.authorization || event.headers.Authorization;

  if (!token) {
    throw new Error('Unauthorized');
  }

  try {
    const verified = await verifyToken(token.replace('Bearer ', ''));
    return {
      principalId: verified.sub,
      policyDocument: {
        Version: '2012-10-17',
        Statement: [
          {
            Action: 'execute-api:Invoke',
            Effect: 'Allow',
            Resource: '*',
          },
        ],
      },
    };
  } catch (error) {
    throw new Error('Unauthorized');
  }
};

module.exports = { mylocalAuthProxyFn };

then in my serverless.yml I did configure like below

# serverless.yml
service: <service-name>

plugins:
  - serverless-offline

custom:
  serverless-offline:
    httpPort: 4000
    prefix: 'http'
    http:
      stage: $default
      basePath: http

provider:
  name: aws
  runtime: nodejs16.x
  memorySize: 1024
  region: ${env:AWS_DEFAULT_REGION}
  httpApi:
    authorizers:
      serviceAuthorizer:
        type: request
        identitySource: $request.header.Authorization
        issuerUrl: https://cognito-idp.${env:AWS_DEFAULT_REGION}.amazonaws.com/${env:COGNITO_USER_POOL_ID}
        audience: ${env:COGNITO_APP_CLIENT_ID}

functions:
  lambda1:
    handler: functions/lambda1.handler
    events:
      - httpApi:
          path: /endpoint1
          method: post

  serviceAuthorizer:
    handler: localAuthorizer.mylocalAuthProxyFn

and I am running npx serverless offline only, now this is working for me earlier I was getting an error like below

Error: Serverless Offline only supports retrieving JWT from the headers (undefined)

so I changed the serviceAuthorizer type to request then I was getting an error below

Error:
Function "serviceAuthorizer" doesn't exist in this Service

so I have added the below lines in the Function scope

  serviceAuthorizer:
    handler: localAuthorizer.mylocalAuthProxyFn

then it is working for me, I mean it's not the perfect way but the only thing I needed was to get the feedback loop locally so I can test my lambdas faster after the change. I will not commit this code because I think it would mess up with the deployment I guess, but I want to know if anyone has a work around for this as well.

Thanks