Skip to content

Latest commit

 

History

History
535 lines (419 loc) · 14.6 KB

framework.mdx

File metadata and controls

535 lines (419 loc) · 14.6 KB
title sidebar_position slug description
Integrate Within a Framework
5
/getting-started/framework
Integrating FGA within a framework, such as Fastify or Fiber

import { SupportedLanguage, languageLabelMap, DocumentationNotice, ProductConcept, ProductName, ProductNameFormat, RelatedSection, SdkSetupPrerequisite, } from '@components/Docs'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem';

Integrate Within a Framework

This section will illustrate how to integrate within a framework, such as Fastify or Fiber.

Before You Start

  1. You have installed the OpenFGA SDK.
  2. You have configured the authorization model and updated the relationship tuples.
  3. You know how to perform a Check.
  4. You have loaded FGA_API_URL and FGA_STORE_ID as environment variables.
  1. You have installed the OpenFGA SDK.
  2. You have configured the authorization model and updated the relationship tuples.
  3. You know how to perform a Check.
  4. You have loaded FGA_API_URL and FGA_STORE_ID as environment variables.

Step By Step

Assume that you want to have a web service for documents using one of the frameworks mentioned above. The service will authenticate users via JWT tokens, which contain the user ID.

:::caution Note The reader should set up their own login method based on their OpenID connect provider's documentation. :::

Assume that you want to provide a route GET /read/{document} to return documents depending on whether the authenticated user has access to it.

01. Install And Setup Framework

The first step is to install the framework.

For the context of this example, we will use the Fastify framework. For that we need to install the following packages:

  • the fastify package that provides the framework itself
  • the fastify-plugin package that allows integrating plugins with Fastify
  • the fastify-jwt package for processing JWT tokens

Using npm:

npm install fastify fastify-plugin fastify-jwt

Using yarn:

yarn add fastify fastify-plugin fastify-jwt

Next, we setup the web service with the GET /read/{document} route in file app.js.

// Require the framework and instantiate it
const fastify = require('fastify')({ logger: true });

// Declare the route
fastify.get('/read/:document', async (request, reply) => {
  return { read: request.params.document };
});

// Run the server
const start = async () => {
  try {
    await fastify.listen(3000);
  } catch (err) {
    fastify.log.error(err);
    process.exit(1);
  }
};
start();

For the context of this example, we will use the Fiber framework. For that we need to install the following Go packages:

  • the gofiber/fiber package that provides the Fiber framework itself
  • the gofiber/jwt middleware authentication layer for JWT
  • the golang-jwt package that provides Go support for JWT
go get -u github.com/gofiber/fiber/v2 github.com/gofiber/jwt/v3 github.com/golang-jwt/jwt/v4

Next, we setup the web service with the GET /read/{document} route.

package main

import "github.com/gofiber/fiber/v2"

func main() {
  app := fiber.New()

  app.Get("/read/:document", read)

  app.Listen(":3000")
}

func read(c *fiber.Ctx) error {
  return c.SendString(c.Params("document"))
}

02. Authenticate And Get User ID

Before we can call to protect the /read/{document} route, we need to validate the user's JWT.

The fastify-jwt package allows validation of JWT tokens, as well as providing access to the user's identity.

In jwt-authenticate.js:

const fp = require('fastify-plugin');

module.exports = fp(async function (fastify, opts) {
  fastify.register(require('fastify-jwt'), {
    secret: {
      private: readFileSync(`${path.join(__dirname, 'certs')}/private.key`, 'utf8'),
      public: readFileSync(`${path.join(__dirname, 'certs')}/public.key`, 'utf8'),
    },
    sign: { algorithm: 'RS256' },
  });

  fastify.decorate('authenticate', async function (request, reply) {
    try {
      await request.jwtVerify();
    } catch (err) {
      reply.send(err);
    }
  });
});

Then, use the preValidation hook of a route to protect it and access the user information inside the JWT:

In route-read.js:

module.exports = async function (fastify, opts) {
  fastify.get(
    '/read/:document',
    {
      preValidation: [fastify.authenticate],
    },
    async function (request, reply) {
      // the user's id is in request.user
      return { read: request.params.document };
    },
  );
};

Finally, update app.js to register the newly added hooks.

const fastify = require('fastify')({ logger: true });
const jwtAuthenticate = require('./jwt-authenticate');
const routeread = require('./route-read');

fastify.register(jwtAuthenticate);
fastify.register(routeread);

// Run the server!
const start = async () => {
  try {
    await fastify.listen(3000);
  } catch (err) {
    fastify.log.error(err);
    process.exit(1);
  }
}
start();

We will now setup middleware to authenticate the incoming JWTs.

package main

import (
  "crypto/rand"
  "crypto/rsa"
  "log"

  "github.com/gofiber/fiber/v2"

  jwtware "github.com/gofiber/jwt/v3"
  "github.com/golang-jwt/jwt/v4"
)

var (
  // Do not do this in production.
  // In production, you would have the private key and public key pair generated
  // in advance. NEVER add a private key to any GitHub repo.
  privateKey *rsa.PrivateKey
)

func main() {
  app := fiber.New()

  // Just as a demo, generate a new private/public key pair on each run.
  rng := rand.Reader
  var err error
  privateKey, err = rsa.GenerateKey(rng, 2048)
  if err != nil {
    log.Fatalf("rsa.GenerateKey: %v", err)
  }

  // JWT Middleware
  app.Use(jwtware.New(jwtware.Config{
    SigningMethod: "RS256",
    SigningKey:    privateKey.Public(),
  }))

  app.Get("/read/:document", read)

  app.Listen(":3000")
}

func read(c *fiber.Ctx) error {
  user := c.Locals("user").(*jwt.Token)
  claims := user.Claims.(jwt.MapClaims)
  name := claims["name"].(string)
  return c.SendString(name + " read " + c.Params("document"))
}

03. Integrate The Check API Into The Service

First, we will create a decorator preauthorize to parse the incoming HTTP method as well as name of the document, and set the appropriate relation and object that we will call Check on.

In preauthorize.js:

const fp = require('fastify-plugin');

module.exports = fp(async function (fastify, opts) {
  fastify.decorate('preauthorize', async function (request, reply) {
    try {
      switch (request.method) {
        case 'GET':
          request.relation = 'reader';
          break;
        case 'POST':
          request.relation = 'writer';
          break;
        case 'DELETE':
        default:
          request.relation = 'owner';
          break;
      }
      request.object = `document:${request.params.document}`;
    } catch (err) {
      reply.send(err);
    }
  });
});

Next, we will create a decorator called authorize. This decorator will invoke the Check API to see if the user has a relationship with the specified document.

In authorize.js:

const fp = require('fastify-plugin');
const { OpenFgaClient } = require('@openfga/sdk'); // OR import { OpenFgaClient } from '@openfga/sdk';

module.exports = fp(async function (fastify, opts) {
  fastify.decorate('authorize', async function (request, reply) {
    try {
      // configure the openfga api client
      const fgaClient = new OpenFgaClient({
        apiUrl: process.env.FGA_API_URL, // required, e.g. https://api.fga.example
        storeId: process.env.FGA_STORE_ID,
      });
      const { allowed } = await fgaClient.check({
        user: request.user,
        relation: request.relation,
        object: request.object,
      });
      if (!allowed) {
        reply.code(401).send(`Not authenticated`);
      }
    } catch (err) {
      reply.send(err);
    }
  });
});

We can now update the GET /read/{document} route to check for user permissions.

In route-read.js:

module.exports = async function (fastify, opts) {
  fastify.get(
    '/read/:document',
    {
      preValidation: [fastify.authenticate, fastify.preauthorize, fastify.authorize],
    },
    async function (request, reply) {
      // the user's id is in request.user
      return { read: request.params.document };
    },
  );
};

Finally, we will register the new hooks in app.js:

const fastify = require('fastify')({ logger: true });
const jwtAuthenticate = require('./jwt-authenticate');
const preauthorize = require('./preauthorize');
const authorize = require('./authorize');
const routeread = require('./route-read');

fastify.register(jwtAuthenticate);
fastify.register(preauthorize);
fastify.register(authorize);
fastify.register(routeread);

const start = async () => {
  try {
    await fastify.listen(3000);
  } catch (err) {
    fastify.log.error(err);
    process.exit(1);
  }
}
start();

We will create two middlewares:

  • preauthorize will parse the user's JWT and prepare variables needed to call Check API.
  • checkAuthorization will call the Check API to see if the user has a relationship with the specified document.
package main

import (
  "context"
  "crypto/rand"
  "crypto/rsa"
  "log"
  "os"

  "github.com/gofiber/fiber/v2"

  jwtware "github.com/gofiber/jwt/v3"
  "github.com/golang-jwt/jwt/v4"
  . "github.com/openfga/go-sdk/client"
)

var (
  // Do not do this in production.
  // In production, you would have the private key and public key pair generated
  // in advance. NEVER add a private key to any GitHub repo.
  privateKey *rsa.PrivateKey
)

func main() {
  app := fiber.New()

  // Just as a demo, generate a new private/public key pair on each run.
  rng := rand.Reader
  var err error
  privateKey, err = rsa.GenerateKey(rng, 2048)
  if err != nil {
    log.Fatalf("rsa.GenerateKey: %v", err)
  }

  // JWT Middleware
  app.Use(jwtware.New(jwtware.Config{
    SigningMethod: "RS256",
    SigningKey:    privateKey.Public(),
  }))

  app.Use("/read/:document", preauthorize)

  app.Use(checkAuthorization)

  app.Get("/read/:document", read)

  app.Listen(":3000")
}

func read(c *fiber.Ctx) error {
  user := c.Locals("user").(*jwt.Token)
  claims := user.Claims.(jwt.MapClaims)
  name := claims["name"].(string)
  return c.SendString(name + " read " + c.Params("document"))
}

func preauthorize(c *fiber.Ctx) error {
  // get the user name from JWT
  user := c.Locals("user").(*jwt.Token)
  claims := user.Claims.(jwt.MapClaims)
  name := claims["name"].(string)
  c.Locals("username", name)

  // parse the HTTP method
  switch (c.Method()) {
    case "GET":
      c.Locals("relation", "reader")
    case "POST":
      c.Locals("relation", "writer")
    case "DELETE":
      c.Locals("relation", "owner")
    default:
      c.Locals("relation", "owner")
  }

  // get the object name and prepend with type name "document:"
  c.Locals("object", "document:" + c.Params("document"))
  return c.Next()
}

// Middleware to check whether user is authorized to access document
func checkAuthorization(c *fiber.Ctx) error {
  fgaClient, err := NewSdkClient(&ClientConfiguration{
    ApiUrl:               os.Getenv("FGA_API_URL"), // required, e.g. https://api.fga.example
    StoreId:        os.Getenv("FGA_STORE_ID"), // optional, not needed for \`CreateStore\` and \`ListStores\`, required before calling for all other methods
    AuthorizationModelId: os.Getenv("FGA_MODEL_ID"),  // optional, can be overridden per request
  })

  if err != nil {
    return fiber.NewError(fiber.StatusServiceUnavailable, "Unable to build OpenFGA client")
  }

  body := ClientCheckRequest{
    User: c.Locals("username").(string),
    Relation: c.Locals("relation").(string),
    Object: c.Locals("object").(string),
  }
  data, err := fgaClient.Check(context.Background()).Body(body).Execute()

  if err != nil {
    return fiber.NewError(fiber.StatusServiceUnavailable, "Unable to check for authorization")
  }

  if !(*data.Allowed) {
    return fiber.NewError(fiber.StatusUnauthorized, "Unauthorized to access document")
  }

  // Go to the next middleware
  return c.Next()
}

Related Sections

<RelatedSection description="Take a look at the following sections for examples that you can try when integrating with SDK." relatedLinks={[ { title: 'Entitlements', description: 'Modeling Entitlements for a System in {ProductName}.', link: '../modeling/advanced/entitlements', }, { title: 'IoT', description: 'Modeling Fine Grained Authorization for an IoT Security Camera System with {ProductName}.', link: '../modeling/advanced/iot', }, { title: 'Slack', description: 'Modeling Authorization for Slack with {ProductName}.', link: '../modeling/advanced/slack', }, ]} />