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 11 months ago

evanjd commented 11 months 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 11 months ago

@evanjd, if you still need it:

yarn add -D jmferreiratech/serverless-offline-local-authorizers-plugin#v1.3.0
dmitriy-baltak commented 8 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 2 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