Integrate Within a Framework
This section will illustrate how to integrate OpenFGA within a framework, such as Fastify or Fiber.
Before you start
- Node.js
- Go
- Deploy an instance of the OpenFGA server, and have ready the values for your setup: FGA_STORE_ID, FGA_API_URL and, if needed, FGA_API_TOKEN.
- You have installed the OpenFGA SDK.
- You have configured the authorization model and updated the relationship tuples.
- You know how to perform a Check.
- You have loaded
FGA_API_URL
andFGA_STORE_ID
as environment variables.
- Deploy an instance of the OpenFGA server, and have ready the values for your setup: FGA_STORE_ID, FGA_API_URL and, if needed, FGA_API_TOKEN.
- You have installed the OpenFGA SDK.
- You have configured the authorization model and updated the relationship tuples.
- You know how to perform a Check.
- You have loaded
FGA_API_URL
andFGA_STORE_ID
as environment variables.
Step by step
Assume that you want to have a web service for document
s using one of the frameworks mentioned above. The service will authenticate users via JWT tokens, which contain the user ID.
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.
- Node.js
- Go
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 OpenFGA to protect the /read/{document}
route, we need to validate the user's JWT.
- Node.js
- Go
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 OpenFGA check API into the service
- Node.js
- Go
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(403).send(`forbidden`);
}
} 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 theCheck 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.StatusForbidden, "Forbidden to access document")
}
// Go to the next middleware
return c.Next()
}