CMSgov/dpc-app

View on GitHub
dpc-api/src/main/java/gov/cms/dpc/api/auth/MacaroonHelpers.java

Summary

Maintainability
A
0 mins
Test Coverage
B
85%
package gov.cms.dpc.api.auth;

import com.github.nitram509.jmacaroons.Macaroon;
import gov.cms.dpc.macaroons.CaveatSupplier;
import gov.cms.dpc.macaroons.MacaroonBakery;
import gov.cms.dpc.macaroons.MacaroonCaveat;
import gov.cms.dpc.macaroons.MacaroonCondition;
import gov.cms.dpc.macaroons.caveats.ExpirationCaveatSupplier;
import gov.cms.dpc.macaroons.caveats.VersionCaveatSupplier;
import org.apache.http.HttpHeaders;

import javax.annotation.Nullable;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

public class MacaroonHelpers {

    private MacaroonHelpers() {
        // Not used
    }

    public static final String BEARER_PREFIX = "Bearer";
    public static final String ORGANIZATION_CAVEAT_KEY = "organization_id";
    static final String TOKEN_URI_PARAM = "token";

    /**
     * Extracts a Macaroon from the given {@link ContainerRequestContext}
     * First tries to get the value from the {@link HttpHeaders#AUTHORIZATION} header, if that fails (returns null) tries for the {@link MacaroonHelpers#TOKEN_URI_PARAM} query param.
     * Note: This does not validate that the Macaroon is actually valid, it simply pulls whatever {@link String} value it finds.
     *
     * @param requestContext       - {@link ContainerRequestContext} to extract Macaroon from
     * @param unauthorizedResponse - {@link Response} handler to use when extraction fails
     * @return - {@link String} Macaroon value from request
     */
    public static String extractMacaroonFromRequest(ContainerRequestContext requestContext, Response unauthorizedResponse) {
        // Try to get the Macaroon from the request
        String macaroon = getMacaroon(requestContext.getHeaders().getFirst(HttpHeaders.AUTHORIZATION));

        final UriInfo uriInfo = requestContext.getUriInfo();
        if (macaroon == null) {
            macaroon = uriInfo.getQueryParameters().getFirst(TOKEN_URI_PARAM);
        }

        if (macaroon == null) {
            throw new WebApplicationException(unauthorizedResponse);
        }

        return macaroon;
    }

    /**
     * Generate caveats for the given token Version
     *
     * @param tokenVersion   - {@link Integer} token version to generate caveats for
     * @param organizationID - {@link UUID} organization ID to restrict caveat to
     * @param tokenLifetime  - {@link Duration} lifetime of token
     * @return - {@link List} of {@link CaveatSupplier} to use for generating token
     */
    public static List<CaveatSupplier> generateCaveatsForToken(int tokenVersion, UUID organizationID, Duration tokenLifetime) {
        switch (tokenVersion) {
            case 1: {
                return generateV1Caveats(tokenLifetime, organizationID);
            }
            case 2: {
                return generateV2Caveats(tokenLifetime, organizationID);
            }
            default: {
                throw new IllegalArgumentException(String.format("Cannot created token with version: %s", tokenVersion));
            }
        }
    }

    public static Optional<UUID> extractOrgIDFromCaveats(List<Macaroon> macaroons) {
        final Macaroon rootMacaroon = macaroons.get(0);
            // Find the org_id caveat and extract the value
            return MacaroonBakery
                    .getCaveats(rootMacaroon)
                    .stream()
                    .map(MacaroonCaveat::getCondition)
                    .filter(condition -> condition.getKey().equals(ORGANIZATION_CAVEAT_KEY))
                    .map(condition -> UUID.fromString(condition.getValue()))
                    .findAny();
    }

    private static List<CaveatSupplier> generateV1Caveats(Duration tokenLifetime, UUID organizationID) {
        return generateDefaultCaveats(1, tokenLifetime, organizationID);
    }

    private static List<CaveatSupplier> generateV2Caveats(Duration tokenLifetime, UUID organizationID) {
        final List<CaveatSupplier> caveatSuppliers = new ArrayList<>(generateDefaultCaveats(2, tokenLifetime, organizationID));
        // The body of this local caveat is intentionally duplicated. We don't actually check it, we just need something in order to serialize correctly.
        // In the future, we might add specific caveats that the discharger can check before discharging.
        caveatSuppliers.add(() -> new MacaroonCaveat("local", new MacaroonCondition(ORGANIZATION_CAVEAT_KEY, MacaroonCondition.Operator.EQ, organizationID.toString())));
        return caveatSuppliers;
    }

    private static List<CaveatSupplier> generateDefaultCaveats(int tokenVersion, Duration tokenLifetime, UUID organizationID) {
        return List.of(
                new VersionCaveatSupplier(tokenVersion),
                new ExpirationCaveatSupplier(tokenLifetime),
                () -> new MacaroonCaveat("", new MacaroonCondition(ORGANIZATION_CAVEAT_KEY, MacaroonCondition.Operator.EQ, organizationID.toString())));
    }

    @Nullable
    private static String getMacaroon(String header) {
        if (header == null) {
            return null;
        }

        final int space = header.indexOf(' ');
        if (space <= 0) {
            return null;
        }

        final String method = header.substring(0, space);
        if (!BEARER_PREFIX.equalsIgnoreCase(method)) {
            return null;
        }

        return header.substring(space + 1);
    }
}