All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.networknt.aws.lambda.ScopeVerifier Maven / Gradle / Ivy

package com.networknt.aws.lambda;

import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import com.networknt.oas.model.Operation;
import com.networknt.oas.model.Path;
import com.networknt.oas.model.SecurityParameter;
import com.networknt.oas.model.SecurityRequirement;
import com.networknt.openapi.ApiNormalisedPath;
import com.networknt.openapi.NormalisedPath;
import com.networknt.openapi.OpenApiHelper;
import com.networknt.utility.Constants;
import com.networknt.utility.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * It is called in the Lambda framework to verify the scopes in the primary and secondary jwt tokens.
 * The scopes are passed by the jwt-authorizer deployed on the API Gateway after verify JWT tokens. The
 * authorizer can not verify the scopes as it doesn't have the knowledge of the OpenAPI specification.
 *
 * Each function will have the openapi.yaml packaged as configuration and this class will use it to
 * verify the scopes again the scopes in the authorizer context enriched by the authorizer.
 *
 * The verifyScope is called by the request-handler that intercepts the request and response in the App.
 *
 * The light-rest-4j now supports multiple OpenApi specifications, however, the Lambda should only use
 * one specification. The default config should do that job as it is configured as single spec.
 *
 * @author Steve Hu
 */
public class ScopeVerifier {
    static final Logger logger = LoggerFactory.getLogger(ScopeVerifier.class);
    static final String STATUS_AUTH_TOKEN_SCOPE_MISMATCH = "ERR10005";
    static final String STATUS_SCOPE_TOKEN_SCOPE_MISMATCH = "ERR10006";
    static final String STATUS_INVALID_REQUEST_PATH = "ERR10007";
    static final String STATUS_METHOD_NOT_ALLOWED = "ERR10008";
    static final String STATUS_MISSING_GATEWAY_AUTHORIZER = "ERR10061";
    static final String STATUS_MISSING_PRIMARY_SCOPES = "ERR10062";

    /**
     * verify the scopes from the primary and optional secondary tokens against the scopes in the
     * openapi.yaml specification.
     *
     * @param requestEvent request event
     * @return responseEvent if error and null if pass.
     */
    public APIGatewayProxyResponseEvent verifyScope(APIGatewayProxyRequestEvent requestEvent) {
        APIGatewayProxyRequestEvent.ProxyRequestContext context = requestEvent.getRequestContext();
        if(context != null) {
            // get the enriched primaryScopes and convert it into a list of string again.
            Map authorizerMap = context.getAuthorizer();
            if(authorizerMap == null) {
                logger.error("Authorizer enriched context is missing");
                return createErrorResponse(401, STATUS_MISSING_GATEWAY_AUTHORIZER);
            }
            String primaryScopesString = (String) authorizerMap.get(Constants.PRIMARY_SCOPES);
            if (primaryScopesString != null) {
                String[] primaryScopes = StringUtils.split(primaryScopesString, ' ');
                // get the openapi.yaml from the resources folder.
                String spec = new Scanner(ScopeVerifier.class.getClassLoader().getResourceAsStream("openapi.yaml"), StandardCharsets.UTF_8).useDelimiter("\\A").next();
                OpenApiHelper openApiHelper = null;
                if (spec != null) {
                    openApiHelper = new OpenApiHelper(spec);
                }
                if (openApiHelper.openApi3 != null) {
                    String path = requestEvent.getPath();
                    final NormalisedPath requestPath = new ApiNormalisedPath(path, openApiHelper.basePath);
                    final Optional maybeApiPath = openApiHelper.findMatchingApiPath(requestPath);
                    if (!maybeApiPath.isPresent()) {
                        logger.error("Invalid request path " + path);
                        return createErrorResponse(404, STATUS_INVALID_REQUEST_PATH);
                    }

                    final NormalisedPath swaggerPathString = maybeApiPath.get();
                    final Path swaggerPath = openApiHelper.openApi3.getPath(swaggerPathString.original());

                    final String httpMethod = requestEvent.getHttpMethod().toLowerCase();
                    Operation operation = swaggerPath.getOperation(httpMethod);
                    if (operation == null) {
                        logger.error("Method " + httpMethod + " is not allowed");
                        return createErrorResponse(405, STATUS_METHOD_NOT_ALLOWED);
                    }
                    // save the endpoint for metric collection
                    context.getAuthorizer().put(Constants.ENDPOINT_STRING, swaggerPathString.normalised() + "@" + httpMethod);

                    // get scope defined in OpenAPI spec for this endpoint.
                    Collection specScopes = null;
                    Collection securityRequirements = operation.getSecurityRequirements();
                    if (securityRequirements != null) {
                        for (SecurityRequirement requirement : securityRequirements) {
                            SecurityParameter securityParameter = null;
                            for (String oauth2Name : openApiHelper.oauth2Names) {
                                securityParameter = requirement.getRequirement(oauth2Name);
                                if (securityParameter != null) break;
                            }
                            if (securityParameter != null) specScopes = securityParameter.getParameters();
                            if (specScopes != null) break;
                        }
                    }

                    // is there a scope token
                    String secondaryScopesString = (String) context.getAuthorizer().get(Constants.SECONDARY_SCOPES);
                    if (secondaryScopesString != null) {
                        String[] secondaryScopes = StringUtils.split(secondaryScopesString, ' ');
                        if (!matchedScopes(secondaryScopes, specScopes)) {
                            logger.error("Scopes " + secondaryScopesString + " in scope token and spec scopes " + specScopes + " are not matched");
                            return createErrorResponse(403, STATUS_SCOPE_TOKEN_SCOPE_MISMATCH);
                        }
                    } else {
                        // no scope token, verify scope from auth token.
                        if (!matchedScopes(primaryScopes, specScopes)) {
                            logger.error("Scopes " + primaryScopesString + " in authorization token and spec scopes " + specScopes + " are not matched");
                            return createErrorResponse(403, STATUS_AUTH_TOKEN_SCOPE_MISMATCH);
                        }
                    }
                }
            } else {
                // not primary scopes available.
                logger.error("Scopes from the JWT token in Authorization header are missing");
                return createErrorResponse(401, STATUS_MISSING_PRIMARY_SCOPES);
            }
        }
        return null;
    }

    private APIGatewayProxyResponseEvent createErrorResponse(int statusCode, String errorCode) {
        Map headers = new HashMap<>();
        headers.put("Content-Type", "application/json");
        String body = "{\"statusCode\":" + statusCode + ",\"code\":\"" + errorCode + "\"}";
        return new APIGatewayProxyResponseEvent()
                .withHeaders(headers)
                .withStatusCode(statusCode)
                .withBody(body);
    }

    private boolean matchedScopes(String[] scopes, Collection specScopes) {
        boolean matched = false;
        if(specScopes != null && specScopes.size() > 0) {
            if(scopes != null && scopes.length > 0) {
                List jwtScopes = Arrays.asList(scopes);
                for(String scope: specScopes) {
                    if(jwtScopes.contains(scope)) {
                        matched = true;
                        break;
                    }
                }
            }
        } else {
            matched = true;
        }
        return matched;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy