Vault
Implement secrets for the secrets engine
In the Define roles for the secrets
engine tutorial, you added the
role/* path to your secrets engine.
In this tutorial, you will create the workflows to allow Vault to renew and revoke the HashiCups API token. Then, you can add the HashiCups JSON Web Token (JWT) to the secrets engine's backend. A secret defines the type of secret a secrets engine should store and manage, such as a token, password, username, SSH key, and more.

To do this, you will:
- Set up your development environment.
You will clone the HashiCups secrets engine repository. This contains many of the interfaces and objects you need to create a secrets engine. - Explore the attributes for the secret.
You will examine the attributes defined in the secrets engine for the HashiCups API token. - Define a secret for the backend.
You will define a secret and its fields to store into the secrets engine backend. - Implement a workflow to revoke the secret.
You will create a workflow to invalidate the HashiCups API token and remove it from the secrets engine backend. - Implement a workflow to renew the secret.
You will create a workflow to create the HashiCups API token and renew it in the secrets engine backend. - Add the secret type to the backend.
You will add a new secret for the HashiCups API token to the backend.
Prerequisites
- Golang 1.16+ installed and configured.
- Vault 1.8+ CLI installed locally.
Set up your development environment
Clone the learn-vault-plugin-secrets-hashicups repository.
$ git clone https://github.com/hashicorp-education/learn-vault-plugin-secrets-hashicups
Change into the repository directory.
$ cd vault-plugin-secrets-hashicups
Explore the attributes for the secret
Open hashicups_token.go. The file contains all of the objects
and methods related to creating and deleting the HashiCups API
token.
Examine the constant, which sets the name of the token type for
the Vault API as hashicups_token.
hashicups_token.go
const (
hashiCupsTokenType = "hashicups_token"
)
type hashiCupsToken struct {
UserID int `json:"user_id"`
Username string `json:"username"`
TokenID string `json:"token_id"`
Token string `json:"token"`
}
hashiCupsToken defines the API token issued by the HashiCups API when you
sign in and out. It sets a few attributes, including:
UserID: Generated when a user first signs up for HashiCups.Username: Set by the user when they first sign up for HashiCups.TokenID: A unique identifier for the token. The HashiCups API does not generate a token ID, so you must implement ID generation in the secrets engine properly test token revocation and renewal.Token: JWT for the HashiCups API.
hashicups_token.go
const (
hashiCupsTokenType = "hashicups_token"
)
type hashiCupsToken struct {
UserID int `json:"user_id"`
Username string `json:"username"`
TokenID string `json:"token_id"`
Token string `json:"token"`
}
Define a secret for the backend
Open hashicups_token.go.
Replace the method hashiCupsToken, which extends the secrets engine backend,
with the token type and a schema for the Field. You use a string constant to
define the Type as hashicups_token. You also set a string type for the
token attribute in Fields to store the JWT from HashiCups.
hashicups_token.go
func (b *hashiCupsBackend) hashiCupsToken() *framework.Secret {
return &framework.Secret{
Type: hashiCupsTokenType,
Fields: map[string]*framework.FieldSchema{
"token": {
Type: framework.TypeString,
Description: "HashiCups Token",
},
},
}
}
Implement a workflow to revoke the secret
As you develop the secrets object for your secrets engine, you need to examine your target API to identify how to get a new secret (renew) and invalidate an existing one (revoke).
You need to use two endpoints from the HashiCups products API to renew and revoke JSON Web Tokens (JWTs) from HashiCups.
| PATH | METHOD | DESCRIPTION | HEADER | REQUEST | RESPONSE |
|---|---|---|---|---|---|
| /signin | POST | Sign in an existing user and get a new JWT | {"username": "user", "password": "pass"} | {"UserID":1,"Username":"user","token":"<JWT>"} | |
| /signout | POST | Sign out a user and invalidate a JWT | Authorization:<JWT> | Signed out user |
The HashiCups products client SDK implements the two interfaces for signing in and out of the HashiCups API.
Open hashicups_token.go.
Add the deleteToken method to use the HashiCups Go SDK client to sign out
from the HashiCups API. Signing out from the HashiCups API invalidates the
JWT and prevents someone from using it.
hashicups_token.go
func deleteToken(ctx context.Context, c *hashiCupsClient, token string) error {
c.Client.Token = token
err := c.SignOut()
if err != nil {
return nil
}
return nil
}
Replace the tokenRevoke method with new logic to delete the HashiCups API
token. The HashiCups API invalidates the token that you use when you sign out.
As a result, you pass the value of the token stored at req.Secret.InternalData
to `deleteToken.
hashicups_token.go
func (b *hashiCupsBackend) tokenRevoke(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
client, err := b.getClient(ctx, req.Storage)
if err != nil {
return nil, fmt.Errorf("error getting client: %w", err)
}
token := ""
tokenRaw, ok := req.Secret.InternalData["token"]
if ok {
token, ok = tokenRaw.(string)
if !ok {
return nil, fmt.Errorf("invalid value for token in secret internal data")
}
}
if err := deleteToken(ctx, client, token); err != nil {
return nil, fmt.Errorf("error revoking user token: %w", err)
}
return nil, nil
}
Update the hashiCupsToken method to include a field for Revoke. It should call
back to the function for tokenRevoke.
hashicups_token.go
func (b *hashiCupsBackend) hashiCupsToken() *framework.Secret {
return &framework.Secret{
Type: hashiCupsTokenType,
Fields: map[string]*framework.FieldSchema{
"token": {
Type: framework.TypeString,
Description: "HashiCups Token",
},
},
Revoke: b.tokenRevoke,
}
}
Implement a workflow to renew the secret
Open hashicups_token.go.
Add the createToken method to get a new API token from the HashiCups API.
You get a new JWT each time you sign into the API. However, it does
not generate a token identifier. Instead, the method creates a new token ID for
testing and manual tracking.
Return the hashiCupsToken object with the UserID,
Username, TokenID, and Token from the API response.
hashicups_token.go
func createToken(ctx context.Context, c *hashiCupsClient, username string) (*hashiCupsToken, error) {
response, err := c.SignIn()
if err != nil {
return nil, fmt.Errorf("error creating HashiCups token: %w", err)
}
tokenID := uuid.New().String()
return &hashiCupsToken{
UserID: response.UserID,
Username: username,
TokenID: tokenID,
Token: response.Token,
}, nil
}
Replace the tokenRenew method to verify that a role exists in the secrets
engine backend before HashiCups creates a token. You also pass the secrets
object as a response and reset the time to live (TTL) and maximum TTL for the
role.
hashicups_token.go
func (b *hashiCupsBackend) tokenRenew(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
roleRaw, ok := req.Secret.InternalData["role"]
if !ok {
return nil, fmt.Errorf("secret is missing role internal data")
}
role := roleRaw.(string)
roleEntry, err := b.getRole(ctx, req.Storage, role)
if err != nil {
return nil, fmt.Errorf("error retrieving role: %w", err)
}
if roleEntry == nil {
return nil, errors.New("error retrieving role: role is nil")
}
resp := &logical.Response{Secret: req.Secret}
if roleEntry.TTL > 0 {
resp.Secret.TTL = roleEntry.TTL
}
if roleEntry.MaxTTL > 0 {
resp.Secret.MaxTTL = roleEntry.MaxTTL
}
return resp, nil
}
Update the hashiCupsToken method to include a field for Renew. It should call
back to the function for tokenRenew.
hashicups_token.go
func (b *hashiCupsBackend) hashiCupsToken() *framework.Secret {
return &framework.Secret{
Type: hashiCupsTokenType,
Fields: map[string]*framework.FieldSchema{
"token": {
Type: framework.TypeString,
Description: "HashiCups Token",
},
},
Revoke: b.tokenRevoke,
Renew: b.tokenRenew,
}
}
Add the secret type to the backend
You must add the hashiCupsToken as a Secret to the backend.
Open backend.go and replace backend to add hashiCupsToken to the list
of Secrets.
backend.go
func backend() *hashiCupsBackend {
var b = hashiCupsBackend{}
b.Backend = &framework.Backend{
Help: strings.TrimSpace(backendHelp),
PathsSpecial: &logical.Paths{
LocalStorage: []string{},
SealWrapStorage: []string{
"config",
"role/*",
},
},
Paths: framework.PathAppend(
pathRole(&b),
[]*framework.Path{
pathConfig(&b),
},
),
Secrets: []*framework.Secret{
b.hashiCupsToken(),
},
BackendType: logical.TypeLogical,
Invalidate: b.invalidate,
}
return &b
}
Next steps
Congratulations! You defined a new secret for your secrets engine.
If you are stuck in this tutorial, refer to the
plugins/vault-plugin-secrets-hashicups/solution directory.
- To learn more about Vault plugins, refer to the Vault Plugin System Documentation.
- Define the secrets engine's credentials endpoint in the next tutorial.