URL Minter

Context

The URL minter Cloud Function provides a service to generate signed URLs that can be passed to third-party systems for reading or writing data in Cloud Storage without issuing those third-parties with GCP credentials.

A private key is required to sign the URLs and, as this must not be exposed to clients, the signing must be done server-side. Although Google Cloud provides a similar iam.signBlob method, this is only recommended for short-lived access to resources so not suitable for our purposes.

Input

The function expects a JSON payload containing an array of URL signing requests. For example:

[
    {
        "Bucket": "maximum-egret.appspot.com",
        "Path": "/avatar/shared/aaa/test.png",
        "Method": "GET",
        "ContentType": "",
        "TTL": "15m"
    }
]

The fields Bucket, Path, and Method are required; Method must be either GET or PUT.

The ContentType field is optional (and only relevant for PUT requests), but if specified the client doing the upload must set the same value in the Content-Type header.

The TTL (time to live) must be a duration string that can be parsed by go’s time.ParseDuration. The maximum allowed expiry for signed URLs is 168h (7 days) - this is a limit imposed by GCP.

Invocation

The URL minter Cloud Function implements the https.onCall protocol supported by the Firebase client SDKs.

The client SDK will take care of authentication and error handling. See UrlMinterDisplay.jsx for an example of calling this using the Firebase Javascript SDK.

Actions

The function verifies the bearer token passed in the Authorization header, which must be a valid Firebase id token issued in the same project the Cloud Function is configured.

It then goes on to check that the caller is permitted to read (or write) each of the requested URLs, following the rules described in ADR 21 Storage Path Scheme. At the time of writing, it is assumed that administrators will use standard tools to upload to public, shared, and internal assets and will not be calling this function, so the function will deny PUT requests for anything outside of /{assetType]/user/{userID}/.

If access is denied to any one of the requested URLs, the entire request will fail with HTTP status 403 (Forbidden).

Output

On successful invocation, the function returns HTTP status 200 and a JSON payload that mirrors the input with a signed URL added to each element; for example:

{
    "result": [
        {
            "Bucket": "maximum-egret.appspot.com",
            "Path": "/avatar/shared/aaa/test.png",
            "Method": "GET",
            "ContentType": "",
            "TTL": "15m0s",
            "URL": "https://storage.googleapis.com/maximum-egret.appspot.com/avatar/shared/aaa/test.png?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=url-minter%40maximum-egret.iam.gserviceaccount.com%2F20211116%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20211116T142607Z&X-Goog-Expires=899&X-Goog-Signature=XXX&X-Goog-SignedHeaders=host"
        }
    ]
}

If an error occurs and the function is unable to complete the request, the function will return an HTTP error status and a JSON payload that contains an error key rather than result. For example, if a request is made with no bearer token, the HTTP status will be 401 and response payload:

{"error": {"status": "UNAUTHENTICATED", "message": "Missing bearer token"}}

See the protocol specification for more details. If you are using one of the Firebase SDKs, the library will take care of deserializing the result or throwing an error.

Source code

See url-minter.

Deployment

This function can be deployed to a Firebase project using the url-minter Terraform module.