In this Part 2 of the tutorial series, we will learn about Golang Biscuit Authorization. Here, we will learn how to create, validate, attenuate, seal, and revoke the biscuit tokens. Let’s see how this works
The tutorial series covers
- Part 1: This tutorial covered the basics of Biscuits, creating a biscuit with a Command Line interface (CLI), and checking the authority of the token. We also covered the attenuation of the Biscuit tokens. Check out the tutorial here.
- Part 2: You are here. This tutorial covers how to use Biscuits with Golang.
The complete code for this tutorial can be found on this Github link
Importing the Biscuit library
In this section, we will import the biscuit library into our Golang project. Let’s begin by running the following commands
mkdir golang-biscuit
cd golang-biscuit
go mod init golang-biscuit
go get github.com/biscuit-auth/biscuit-go/v2
The commands above create golang-biscuit directory along with creating a go.mod file containing project dependencies. We also imported the biscuit-go library to our project. As of writing, the current version of the library is v2.1.0. The biscuit-go library has an internal dependency on Google Protocol Buffer library which is also imported. This is a cross-platform open-source library to serialize structured data. Now let’s dive into the actual code
Creating Biscuit Token
Biscuit authorization needs the ED25519 library, public key cryptography, to generate and verify the tokens. We will use the library to generate our private and public keys. The private key is required to generate the token where as the validation requires a public key. The following code shows how to generate our keys
publicKey, privateKey, _ := ed25519.GenerateKey(rand.Reader)
Now that we have our keys, let’s see how we can create our biscuit token.
builder := biscuit.NewBuilder(privateKey)
// Create facts for the biscuit
fact1, err := parser.FromStringFact(`user("admin")`)
if err != nil {
log.Println(err.Error())
}
// Add fact to the builder
err = builder.AddAuthorityFact(fact1)
if err != nil {
log.Println(err.Error())
}
// Create new biscuit
b, err := builder.Build()
if err != nil {
log.Println(err.Error())
}
// Generate slice of byte
biscuitToken, err = b.Serialize()
if err != nil {
log.Println(err.Error())
}
// To use the token across multiple programming languages
// use base64 encoding like this
fmt.Println(base64.URLEncoding.EncodeToString(biscuitToken))
In line 1, we created a new biscuit builder using the biscuit.NewBuilder(privateKey)
method which implements a Builder interface.
Next, we created a new Fact from a string using parser.FromStringFact(`user("admin")`)
where we instructed our program to create a new fact for the biscuit, i.e user(“admin”). Then we added the fact to our biscuit builder using builder.AddAuthorityFact(fact1)
. If you want to add multiple facts to the biscuit token, you can do it using the following
fact1, _:= parser.FromStringFact(`FACTS("ANY_FACT_1")`)
fact2, _:= parser.FromStringFact(`FACTS("ANY_FACT_2")`)
builder.AddAuthorityFact(fact1)
builder.AddAuthorityFact(fact2)
After the facts are added, we actually created our biscuit using builder.Build()
. This generated our first biscuit. We converted it to a slice of bytes to make it shareable across our program using b.Serialize()
.
Important: Biscuit token compatibility across programming languages
It is important to remember that if a biscuit token is to be shared to other platforms using different programming languages, share a token that is base64 encoded. Otherwise, it won’t be recognized.
// share base64 encoded serialized biscuit token
base64.URLEncoding.EncodeToString(biscuitToken)
Validate a Biscuit token
In this section of Golang Biscuit Authorization, we will learn how to validate a biscuit token. This step includes creating an Authorizer which contains a list of Facts from the system and checks to allow or deny the incoming biscuit. The facts and rules are validated against the authorizer before it is allowed to access the requested resource. Here is a sample validator code
b, err := biscuit.Unmarshal(biscuitToken)
if err != nil {
log.Println(err.Error())
}
// Creating the Authorizer
// Authorizer checks the public key provided
authorizer, err := b.Authorizer(publicKey)
if err != nil {
log.Println(err.Error())
}
// Bring in authorizer facts from any source.
// In our case, we are hard coding it
fact1, err := parser.FromStringFact(`user("admin")`)
if err != nil {
log.Println(err.Error())
}
// Second hardcoded rule
fact2, err := parser.FromStringFact(`operation("create")`)
if err != nil {
log.Println(err.Error())
}
authorizer.AddFact(fact1)
authorizer.AddFact(fact2)
authorizer.AddPolicy(biscuit.DefaultAllowPolicy)
if err := authorizer.Authorize(); err != nil {
log.Println(err.Error())
}
The first line in the code above unmarshals the []byte biscuit token to *biscuit.Biscuit using biscuit.Unmarshal(biscuitToken)
.Remember, our biscuit token was serialized when it was created.
Then we created our Authorizer with the public key, obtained above, using b.Authorizer(publicKey)
. We then list all our Facts from the system (a database or any other source) and create biscuit facts for Authorizer. In our case, we hardcoded two biscuit facts for the sake of this code after which the facts are passed to the Authorizer like this
// Create biscuit facts
fact1, _:= parser.FromStringFact(`user("admin")`)
fact2, _:= parser.FromStringFact(`operation("create")`)
// Add facts to Authorizer
authorizer.AddFact(fact1)
authorizer.AddFact(fact2)
The next step involves adding a policy to the authorizer to allow or deny an incoming biscuit when the facts are matched. This is done with authorizer.AddPolicy(biscuit.DefaultAllowPolicy)
or authorizer.AddPolicy(biscuit.DefaultDenyPolicy)
. In our case, we allowed the incoming biscuits that match the Authorizer facts.
Finally authorizer.Authorize()
loops across all the facts and checks in the authorizer and matches against the same in the biscuit. If the checks are matched, the biscuits are allowed to access the required resource.
Attenuating a Biscuit
Attenuation is the process of generating a new biscuit token from an existing token by adding new checks. The new attenuated biscuit token will always have fewer access rights than the original biscuit token. Here is a sample code for attenuation
b, err := biscuit.Unmarshal(biscuitToken)
if err != nil {
log.Println(err.Error())
}
check1, err := parser.FromStringCheck(`check if operation("create")`)
if err != nil {
log.Println(err.Error())
}
// Attenuate the biscuit by appending a new block to it
blockBuilder := b.CreateBlock()
blockBuilder.AddCheck(check1)
// Generate the attenuated token
attenuatedToken, err := b.Append(rand.Reader, blockBuilder.Build())
if err != nil {
log.Println(err.Error())
}
serializedAttenuatedToken, err := attenuatedToken.Serialize()
if err != nil {
log.Println(err.Error())
}
Again the first step is to obtain a *biscuit.Biscuit token from the passed serialized value which is done using biscuit.Unmarshal(biscuitToken)
After that, we added a new rule (check) in our existing biscuit to attenuate it using parser.FromStringCheck(`check if operation("create")`)
. This tells the attenuated biscuit token to check if the Authorized contains a fact named operation(“create”).
We know from Part 1 of the tutorial that the attenuated tokens contain extra blocks which hold the information on new rules. This new block is apart from the default Authority block inside every token. So this new block is added using blockBuilder := b.CreateBlock()
and the new check is attached to the block using blockBuilder.AddCheck(check1)
.
Lastly, we added the new block to the existing biscuit block using b.Append(rand.Reader, blockBuilder.Build())
and generated a new attenuated biscuit token. The attenuated token was serialized to be passed on to others.
Sealing Biscuits
Sealing is used to stop further attenuation of biscuit tokens. This is fairly easy to do with the code below
b, err := biscuit.Unmarshal(biscuitToken)
if err != nil {
log.Println(err.Error())
}
b.Seal(rand.Reader)
In the above code, after obtaining the Biscuit, we sealed the token using b.Seal(rand.Reader)
. This process again generated a new sealed Biscuit token.
Revoking Biscuit Authorization Tokens
Sometimes we need to revoke a token from the system untimely if a token is compromised or a user is kicked out of the system. Then revocation comes into play. The Golang Biscuit
The biscuit library provides us with a mechanism to list the revocation Ids in a token. But, writing code to validate biscuits and maintaining a list of revoked tokens is the responsibility of the project. The list of revocation Ids to be maintained can be stored in a database, environment variable, or any other object storage medium and fetched dynamically in the Authorizer.
We will use the same code from “Creating Biscuit Token” to generate a new token. Then we will fetch the revocation Id from the token and save it in an environment variable. Later on, we will use the same revocation Id to invalidate a token using our custom code.
The revocation Id of a biscuit token can be obtained and saved to the environment variable as follows
// Here `b` is a biscuit token
// Returns a slice of []byte. Revocation Id for each block
revokationIds := b.RevocationIds();
// Save to environment variable
os.Setenv("revocationId", base64.URLEncoding.EncodeToString(revokationIds[0]))
The line revokationIds := b.RevocationIds();
generates [][]byte. This is because an attenuated token can have multiple revocation Ids equivalent to the number of blocks. We will revoke a simple biscuit token and hence, we are interested in the first revocation Id of the token only. That can be obtained using revokationIds[0]
The next step will be to add a new code block to our Validator described in “Validate a Biscuit token” just after obtaining our biscuit token. Add the following
// Obtain biscuit token
b, err := biscuit.Unmarshal(biscuitToken)
if err != nil {
return err
}
// Check revoked token
if os.Getenv("revocationId") == base64.URLEncoding.EncodeToString(b.RevocationIds()[0]){
return errors.New("Token already revoked")
}
The above code checks if revocationId set in the environment variable matches the incoming biscuit’s revocation id. If that matches, then the code throws an error.
Conclusion
In this tutorial, you have learned about Golang Biscuit Authorization. I have tried to cover all the basics of validations, checks, and facts. To learn more about these topics, you can visit this link.