Authorization Model

This documentation provides an overview of the authorization model used across the Fusebit platform.

Authorization is handled by the Fusebit HTTP API. The API requires that all HTTP requests include an Authorization header with a bearer access token. Here is an example HTTP request to the API that illustrates the use of the Authorization header:

GET https://api.{region}.fusebit.io/v1/account/acc-9d9341ea356841ed/subscription/sub-9d9341ea356841ed/boundary/dev-john
Authorization: Bearer {token}
Accept: application/json

Both the Fusebit editor and the Fusebit CLI include an access token when sending requests to the HTTP API, but neither makes any authorization decisions directly.

Accessing the Fusebit HTTP API

Access to the Fusebit HTTP API is based on access tokens in the Json Web Token (JWT) format. Generally, a JWT will include a number of claims in the token payload. Two of those claims, the issuer claim and the subject claim are of particular importance and both of these claims are discussed in turn below. For the specifics regarding the JWT format and other requirements of Fusebit API access tokens, see the Access Tokens section below.

Issuers

The issuer is the basis of trust within the system. For a token to be accepted by the Fusebit HTTP API, it must be issued by a trusted issuer. The issuer may be a third-party identity provider like Auth0, an installation of the Fusebit CLI or a backend service that integrates with the Fusebit platform that issues its own access tokens.

The issuer is responsible for authenticating the end-user. For example, when an end-user gets redirected to sign-in with Google, Google authenticates the end-user and issues an access token. With the Fusebit CLI, authentication happens when the end-user logs into the local machine account under which the CLI was installed. The Fusebit platform itself is not involved in the authentication process, only the authorization process.

In addition to authenticating the user, the issuer is responsible for signing the access tokens it issues such that the Fusebit platform can validate the signature. This is done using an asymmetric key pair that the issuer maintains; the private key is used to sign the access tokens and the public key is made available to the Fusebit platform such that the platform can validate the signature. Since the Fusebit platform requires the issuer's public key, an issuer must be registered with the platform. For details on how to register an issuer using the Fusebit HTTP API or CLI, see the Registering an Issuer section below.

Subjects

The subject claim indicates who is trying to perform a given action on the Fusebit platform. The subject in the access token is generally a particular user, but may also be a client. (A client is a non-human entity that requires access, such as a backend service.)

When a user is added to the platform, a user is associated with a particular issuer and subject that identifies him or her. For details on how to add a user to the platform using the API or CLI, see the Adding Users section below.

Accessing Resources of the Fusebit Platform

A valid access token alone is not enough to ensure that a particular API request is authorized. A valid access token only grants access to the Fusebit platform as a whole, not any specific resource within the platform, such as a particular function, boundary or subscription.

Access to specific resources within the platform is managed via per-user access controls. User access to particular resources is given or taken away using the Fusebit HTTP API of the Fusebit CLI. For details on how to give a user access to a resource using the API or CLI, see the Giving Access to Users section below.

Access is determined both by an action term and a resource path. For example, if a user had been granted the action function:deploy on the resource /account/acc-9d9341ea356841ed/subscription/sub-9d9341ea356841ed/boundary/dev-john/function/task-a then the user would be able to deploy the task-a function in the dev-john boundary.

Actions support wildcard segments. For example function:* is a valid action to grant to user, allowing that user to perform all function-related actions. Resources also support implicit wilcard semantics. A resource path of /account/acc-9d9341ea356841ed/subscription/sub-9d9341ea356841ed/ would give a user access to all boundaries and functions within the sub-9d9341ea356841ed subscription.

πŸ“˜

At the moment, the Fusebit CLI only supports granting wildcard actions, specifically only the following actions: user:*, client:*, issuer:* and function:*.

Most users will only have access to function resources, either with a resource path that limits their access to a particular function or a particular boundary.

Only a handful of account admins will have access to other resources. An account admin will most likely register the main issuer that is used by the backend service that integrates with the Fusebit platform. The admin would also create a client for the backend service to execute as, and most likely grant that client access to manage users. The backend service would then be able to add new end-users to the given subscription and grant those end-users access to particular boundaries or functions.

Access Tokens

The Fusebit HTTP API only accepts access tokens in the Json Web Token (JWT) format.

To be considered valid, a JWT access token must also meet the following requirements:

  • The JWT header must include the following header parameters:
    • A typ (Type) header parameter with a value of 'JWT'
    • An alg (Algorithm) header parameter. Accepted signature algorithms include: RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384 and ES512
    • A kid (Key id) header parameter that identifies the asymmetric key pair used to sign the JWT
  • The JWT payload must include the following registered claims:
    • An iss (Issuer) claim for an issuer registered with the Fusebit platform
    • An aud (Audience) claim with a value of 'https://api.{region}.fusebit.io', or the base URL of your deployment (in case of Private Deployments)
    • A sub (Subject) claim for a known user on the Fusebit platform
    • An exp (Expiration Time) claim with an expiration time of the token
  • The JWT payload may include the following custom claims:
    • An https://fusebit.io/permissions claim that specifies inline permissions. Read more about it in the Inline Permissions section.
  • The JWT must include a signature generated using an asymmetric key algorithm as given by the alg header parameter with an asymmetric key pair identified by the kid header parameter.

An example decoded access token in the JWT format is given below:

// Header
{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "0ba34ef2-438a-4635-8f8b-86edec6aefdf"
}
// Payload
{
   "iss": "https://sales-anchor.auth0.com/",
   "aud": "https://api.{region}.fusebit.io",
   "sub": "google-oauth2|700634445110388888322",
   "exp": "1552631342"
}

Inline Permissions

The Fusebit platform defines a custom JWT claim https://fusebit.io/permissions which allows the specification of inline permissions within the access token itself. This advanced mechanism is useful if the caller wants to restrict its own permissions to a less powerful set in support of the principle of least privilege.

An example of an access token that specifies inline permissions is shown below:

// Header
{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "0ba34ef2-438a-4635-8f8b-86edec6aefdf"
}
// Payload
{
   "iss": "60575b22.clt-e6557755e1244ac3.api.us-west-2.fusebit.io",
   "aud": "https://api.us-west-2.fusebit.io",
   "sub": "cli-8a8c66ce",
   "exp": "1552631342",
   "https://fusebit.io/permissions": {
     "allow": [
       {
         "action": "function:execute",
         "resource": "/account/acc-9d9341ea356841ed/subscription/sub-356841ed9d9341ea/boundary/dev-john/"
       }
     ]
   }
}

The system uses the inline permissions of the access token instead of the permissions of the client or user whose access token it is when performing authorization checks. At the same time, the system enforces that the inline permissions are not more powerful than the permissions of the client or user represented by the access token. If the inline permissions are more powerful, the request is rejected with an HTTP 403 status code.

You can read more about creating an access token with inline permissions programmatically at Restricting Access Token Permissions.

Registering an Issuer

Registering an issuer requires providing the Fusebit platform with both the identifier for the issuer and a means of acquiring the public key that can be used to validate the signature of access tokens from the issuer.

The issuer id is usually a domain name or URL that uniquely identifies the issuer. For example, Google's OAuth 2.0 OpenId Connect issuer identifier is 'accounts.google.com'. Auth0 assigns each Auth0 customer their own issuer id. The issuer id will be of the form: 'https://{customer-tenant-name}.auth0.com/'.

As for a means of acquiring the public key, the Fusebit platform supports two different mechanisms:

  • Uploading a Public Key: One or more public keys for the issuer can be directly uploaded to the Fusebit platform.

  • Hosting a Public Key: A public URL can be provided, at which a JSON file that contains one or more public keys for the issuer should be hosted. The JSON file can be either in the JSON Web Key Specification format (RFC 7517) or it may contain just a single JSON object with key ids as the object property names and the corresponding public key data as the property values. See Google's OAuth keys file as an example.

Which mechanism one uses will depend on the issuer and the requirements one has regarding integrating with the Fusebit platform. It is advisable to rotate asymmetric key pairs on a regular basis, so if the public keys are being directly uploaded to the Fusebit platform, the new keys will need to be uploaded each time the keys are rotated. In this scenario, hosting the public keys is preferable, as the Fusebit platform will download the hosted public keys from the public URL and automatically discover any new public keys.

On the other hand, not all scenarios support hosting the public keys. For example, with each installation of the Fusebit CLI, an asymmetric key pair is generated and the public key is directly uploaded to the Fusebit platform.

If using Auth0 as an identity provider, it is convenient to use the hosted public keys mechanism because Auth0 is already hosting the keys file. By default, the hosted keys file can be found at 'https://{customer-tenant-name}.auth0.com/.well-known/jwks.json', but this is configurable per tenant.

Registering an issuer with the Fusebit platform requires having the issuer:* action with an account resource.

Using the Fusebit API to Register an Issuer

To register an issuer, use the Add a new issuer to an account endpoint.

Below is an example of directly uploading a public key to the Fusebit platform. The account id is acc-9d9341ea356841ed and the issuer id is auth.self-issued.sales-anchor.com.

POST https://api.{region}.fusebit.io/v1/account/acc-9d9341ea356841ed/issuer/auth.self-issued.sales-anchor.com
Authorization: Bearer {token}
{
  "displayName": "Sales Anchor Self-Issued Auth Server",
  "publicKeys": [
    {
      "keyId": "a074dd8bea6da5b19b5fe2d541a4282b7ae3628f",
      "publicKey": "-----BEGIN CERTIFICATE-----\nMIIDJjCCA...[omitted for brevity]...XgxEVcYD\n-----END CERTIFICATE-----\n"
    }
  ]
}

As an alternative, below is an example of using the hosted public keys mechanism. The account id is acc-9d9341ea356841ed and the issuer id is https://sales-anchor.auth0.com/. Note that the issuer id must be URL encoded.

POST https://api.{region}.fusebit.io/v1/account/acc-9d9341ea356841ed/issuer/https%3A%2F%2Fsales-anchor.auth0.com%2F
Authorization: Bearer {token}
{
  "displayName": "Sales Anchor Auth0 Server",
  "jsonKeyUri": "https://sales-anchor.auth0.com/.well-known/jwks.json"
}

Using the Fusebit CLI to Register an Issuer

To register a issuer, use the fuse issuer add command.

Below is an example of directly uploading a public key to the Fusebit platform. The account id is acc-9d9341ea356841ed and the issuer id is auth.self-issued.sales-anchor.com. This example assumes that the default profile used with the Fusebit CLI specifies the account id.

fuse issuer add --issuer auth.self-issued.sales-anchor.com \
               --publicKey ./self-issued-fusebit.pub \
               --keyId 0ba34ef2-438a-4635-8f8b-86edec6aefdf \
               --name "Sales Anchor Self-Issued Auth Server"

The './self-issued-fusebit.pub' has the following content:

-----BEGIN CERTIFICATE-----
MIIDJjCCA...
[omitted for brevity]
...XgxEVcYD
-----END CERTIFICATE-----

And for completeness, below is an example of using the hosted public keys mechanism. The account id is acc-9d9341ea356841ed and the issuer id is https://sales-anchor.auth0.com/. This example assumes that the default profile used with the Fusebit CLI specifies the account id.

fuse issuer add --issuer https://sales-anchor.auth0.com/ \
               --jsonKeysUrl https://sales-anchor.auth0.com/.well-known/jwks.json \
               --name "Sales Anchor Auth0 Server"

Adding Users

After an issuer has been registered with the Fusebit platform, adding a user involves associating that user with a specific 'sub' claim value from the issuer. For example, the user with the email [email protected], when signing in with Google, might be identified by access tokens that have a 'sub' claim value of google-oauth2|700634445110388888322.

Adding users to the Fusebit platform requires having the user:* action with an account resource.

Using the Fusebit API to Add a User

To add a user, use the Add a user endpoint.

Below is an example of adding the user John Doe, who is identified by the 'sub' claim value google-oauth2|700634445110388888322.

POST https://api.{region}.fusebit.io/v1/user
Authorization: Bearer {token}
{
  "firstName": "John",
  "lastName": "Doe",
  "primaryEmail": "[email protected]",
  "identities": [
    {
      "iss": "https://sales-anchor.auth0.com",
      "sub": "google-oauth2|700634445110388888322"
    }
  ]
}

Using the Fusebit CLI to Add a User

To add a user, use the fuse user add and fuse user identity add commands.

Below is an example of adding the user John Doe, who is identified by the 'sub' claim value google-oauth2|700634445110388888322.

fuse user add John Doe [email protected]

> User added with user id: usr-341ea341ed9d9568

fuse user identity add usr-341ea341ed9d9568 \
  --issuer https://sales-anchor.auth0.com \
  --subject google-oauth2|700634445110388888322 \

Giving Access to Users

After a user has been added to the platform, the user will not initially have any access.

Using the Fusebit API to Give Access to a User

Using the Fusebit HTTP API, a user can be given access in the same HTTP request that is used to add the user to the platform, that is, by using the Add a user endpoint. Below is the same example HTTP request used in the Adding Users section, except updated to include giving the user access.

POST https://api.{region}.fusebit.io/v1/user
Authorization: Bearer {token}
{
  "firstName": "John",
  "lastName": "Doe",
  "primaryEmail": "[email protected]",
  "identities": [
    {
      "iss": "https://sales-anchor.auth0.com",
      "sub": "google-oauth2|700634445110388888322"
    }
  ],
  "access": {
    "allow": [
      {
        "action": "function:*",
        "resource": "/account/acc-9d9341ea356841ed/subscription/sub-356841ed9d9341ea/boundary/dev-john"
      }
    ]
  }
}

This example above gives the user John Doe access to manage all functions within the dev-john boundary of the sub-356841ed9d9341ea subscription. Since a particular function is not specified in the resource path, John has access to all functions within the boundary. The same is true for the "boundary" and "subscription" segments of the resource path; if they are not specified, the user has access to all sub resources under the given path.

If a user has already been added to the platform, additional access can be given or taken away from the user also using the Add a user endpoint.

Below is an example HTTP request in which John Doe is given additional access to the production-east-coast boundary:

POST https://api.{region}.fusebit.io/v1/user/usr-341ea341ed9d9568
Authorization: Bearer {token}
{
  "firstName": "John",
  "lastName": "Doe",
  "primaryEmail": "[email protected]",
  "identities": [
    {
      "iss": "https://sales-anchor.auth0.com",
      "sub": "google-oauth2|700634445110388888322"
    }
  ],
  "access": {
    "allow": [
      {
        "action": "function:*",
        "resource": "/account/acc-9d9341ea356841ed/subscription/sub-356841ed9d9341ea/boundary/dev-john"
      },
      {
        "action": "function:*",
        "resource": "/account/acc-9d9341ea356841ed/subscription/sub-356841ed9d9341ea/boundary/production-east-coast"
      }
    ]
  }
}

Using the Fusebit Cli to Give Access to a User

To give access to a user, use the fuse user access add command.

Below is an example of giving the user John Doe access to all functions in the dev-john boundary. This example assumes that the default profile used with the Fusebit CLI specifies the account id and subscription id.

fuse user access add usr-341ea341ed9d9568 \
  --action function:* \
  --boundary dev-john \

Securing Functions

User-created Fusebit functions can be secured using the same authentication and authorization mechanisms Fusebit employs to protect the Fusebit HTTP API.

Authentication

A Fusebit function can require the caller to present a valid access token. Before executing the function, Fusebit platform will validate the caller's access token using the same mechanisms that are used for authenticating access to the Fusebit HTTP API.

To create a Fusebit function that authenticates the caller, specify the authentication setting when creating or updating the function with a call to the Initiate a new build and deployment of a function endpoint, as shown in the example below.

PUT https://api.{region}.fusebit.io/v1/account/acc-9d9341ea356841ed/subscription/sub-9d9341ea356841ed/boundary/dev-john/function/hello
Authorization: Bearer {token}
{
  "nodejs": {
    "files": {
      "index.js": "module.exports = async (ctx) => { return { body: 'Hello, world!' } }"
    }
  },
  "security": {
    "authentication": "required"
  }
}

The behavior of the system depends on the value of the authentication setting as follows:

  • none (the default) - no authentication check is performed and the function accepts unauthenticated callers.
  • required - every call to the function must contain an access token that is trusted by the Fusebit platform specified in the Authorization: Bearer {accessToken} request header. Requests that do not contain a trusted token are rejected with an HTTP 403 response.
  • optional - function always executes, regardless if the caller presents a trusted access token or not. If the request contains the Authorization: Bearer {accessToken} and the access token is trusted by the Fusebit platform, the access token and the permissions of the caller are propagated to the function code in the ctx.caller.permissions and ctx.caller.accessToken values. This mode is useful in more advanced cases where some calls to the Fusebit function require authentication using an access token trusted by Fusebit, while other calls are either unauthenticated or use a custom authentication mechanism.

If the caller was authenticated, the code of the Fusebit function has access to the caller's access token and the permissions the caller was granted in the system, and can make its own decisions based on this information:

module.exports = async (ctx) => {
  if (ctx.fusebit.permissions) {
    // Caller is authenticated:
    // - ctx.fusebit.permissions contains caller's permissions in the system
    // - ctx.fusebit.callerAccessToken contains their access token
  } else {
    // Caller is not authenticated
  }
};

Authorization

In addition to requiring the caller to authenticate, a Fusebit function can also authorize the request by enforcing that the caller has a minimum set of permissions.

To create a Fusebit function that authorizes the caller, specify the authorization setting in addition to the authentication setting when creating or updating the function with a call to the Initiate a new build and deployment of a function endpoint, as shown in the example below.

PUT https://api.{region}.fusebit.io/v1/account/acc-9d9341ea356841ed/subscription/sub-9d9341ea356841ed/boundary/dev-john/function/hello
Authorization: Bearer {token}
{
  "nodejs": {
    "files": {
      "index.js": "module.exports = async (ctx) => { return { body: 'Hello, world!' } }"
    }
  },
  "security": {
    "authentication": "required",
    "authorization": [
      {
        "action": "function:execute",
        "resource": "/account/{{accountId}}/subscription/{{subscriptionId}}/boundary/{{boundaryId}}/function/{{functionId}}/"
      }
    ]
  }
}

The authorization setting specifies a list of permissions the caller must have for the Fusebit function execution to proceed. If the caller does not have sufficient permissions, the call will be rejected with HTTP 403 status code.

Specifying the authorization setting requires authentication to be set to either required or optional. If authentication is set to optional, and the caller is not authenticated, the authorization check is skipped. In other words, the authorization check is only performed if the caller was authenticated.

The action and resource values in the authorization setting can have a user-defined structure. Enforcement of permissions follows the same rules as described in the Accessing Resources of the Fusebit Platform section. By convention, the permission to execute a function is defined using the function:execute action on the resource that fully describes the function identity.

The specification of the resource can utilize placeholder values of {{accountId}}, {{subscriptionId}}, {{boundaryId}}, and {{functionId}}. Those placeholders are replaced with actual values at the time of the authorization check. Using the placeholder values is useful when you are storing your Fusebit function definition in source control and want to be able to reuse it across different Fusebit accounts, subscriptions, boundaries, and function names without modification.

Giving Access to Functions

Some Fusebit functions need to call the Fusebit HTTP API to access Fusebit storage or other resources, or call Fusebit functions that require authentication or authorization. To make such calls, the Fusebit function must be in possession of an access token with sufficient permissions. Two common ways of obtaining that access token are:

  1. Using the access token provided by the caller of the function.
  2. Using the access token provided by the Fusebit platform with permissions specified at function creation.

Using Access Token of the Caller

When a Fusebit function is authenticated, the caller's access token is available to the function in ctx.fusebit.callerAccessToken. In addition, the effective permissions of that access token can be inspected at ctx.caller.permissions.

The function can use the caller's access token to issue a request to another API on behalf of the caller:

const Superagent = require('superagent');

module.exports = async (ctx) => {
  const url; // endpoint that requires Fusebit permissions
  const response = await Superagent.get(url).set('Authorization', `Bearer ${ctx.fusebit.callerAccessToken}`);

  return { body: response.body };
};

Using Access Token Provided by the Fusebit Platform

You can specify the permissions a Fusebit function requires when creating the function. The Fusebit platform will automatically provide the function with the appropriate access token during execution. The access token can then be used to issue a request to another API that requires authorization.

To provide a Fusebit function with an access token, specify the functionPermissions setting when creating or updating the function with a call to the Initiate a new build and deployment of a function endpoint, as shown in the example below.

PUT https://api.{region}.fusebit.io/v1/account/acc-9d9341ea356841ed/subscription/sub-9d9341ea356841ed/boundary/dev-john/function/hello
Authorization: Bearer {token}
{
  "nodejs": {
    "files": {
      "index.js": "module.exports = async (ctx) => { return { body: 'Hello, world!' } }"
    }
  },
  "security": {
    "functionPermissions": {
      "allow": [
        {
          "action": "storage:*",
          "resource": "/account/{{accountId}}/subscription/{{subscriptionId}}/storage/boundary/{{boundaryId}}/function/{{functionId}}/"
        }
      ]
    }
  }
}

The functionPermissions setting specifies a list of permissions that will be associated with an access token the Fusebit platform supplies to the function at the time of execution. The requested list of function permissions cannot exceed the scope of permissions of the caller making the request above. In the above example, the function is granted permissions to access all nodes of the storage hierarchy at or below the node specified by the resource property.

The specification of the resource can utilize placeholder values of {{accountId}}, {{subscriptionId}}, {{boundaryId}}, and {{functionId}}. Those placeholders are replaced with actual values before the permissions are granted.

If a function was granted permissions at creation, the code of the function is provided with an access token within the ctx.fusebit.functionAccessToken property. It can use the access token to make calls to Fusebit HTTP API or a Fusebit function that requires authorization:

const Superagent = require('superagent');

module.exports = async (ctx) => {
  const url; // endpoint that requires Fusebit permissions
  const response = await Superagent.get(url).set('Authorization', `Bearer ${ctx.fusebit.functionAccessToken}`);

  return { body: response.body };
};

The access token has a short expiry time and should not be cached or used beyond the lifetime of the function execution.


Did this page help you?