Skip to content

Service Accounts

Non-human identities in SystemForge are service principals: a Principal of type service with a ServicePrincipal extension. A service principal can hold credentials such as API keys and public keys, scoped to the work it does.

JWT Bearer grant (RFC 7523) is not implemented

SystemAuth does not currently accept the urn:ietf:params:oauth:grant-type:jwt-bearer grant: it is not registered with the OAuth server. Machine-to-machine clients should use the client credentials grant with an OAuth client today. The building blocks described below (service principals and keypair credentials) are what a future JWT Bearer grant will verify assertions against.

Service Principals

A service principal is created through the identity/principal service:

import (
    "github.com/plexusone/systemforge/identity/principal"
)

svc := principal.NewService(client)

desc := "Deploys builds to staging and production"
sp, err := svc.CreateService(ctx, principal.CreateServiceInput{
    Identifier:     "cicd@myorg",
    DisplayName:    "CI/CD Pipeline",
    ServiceType:    "worker",
    Description:    &desc,
    OrganizationID: &orgID,
    CreatedBy:      &adminPrincipalID,
    AllowedScopes:  []string{"deploy:staging", "deploy:production"},
    AllowedIPs:     []string{"10.0.0.0/8"},
})
if err != nil {
    return err
}

CreateService writes the sf_principals row (type service) and its sf_service_principals extension in one transaction. Identifier is the principal's unique lookup key.

Service principals are updated with UpdateService and can be suspended and restored with Deactivate and Reactivate.

Keypair Credentials

Public keys are stored as credentials of type keypair in sf_credentials, owned by the principal. The private key never reaches SystemForge: generate the key pair where the service runs and register only the public key.

import (
    "crypto/rand"
    "crypto/rsa"
    "crypto/x509"
    "encoding/pem"

    "github.com/plexusone/systemforge/identity/credential"
)

privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
    return err
}
pubBytes, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey)
if err != nil {
    return err
}
pubPEM := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubBytes})

creds := credential.NewService(client)
expires := time.Now().Add(365 * 24 * time.Hour)
kp, err := creds.CreateKeypair(ctx, credential.CreateKeypairInput{
    PrincipalID:  sp.ID,
    Name:         "deploy key",
    KeyAlgorithm: "RS256",
    PublicKeyPEM: string(pubPEM),
    Scopes:       []string{"deploy:staging"},
    ExpiresAt:    &expires,
})
if err != nil {
    return err
}

// kp.KeyID is generated by SystemForge; use it as the JWT "kid" header.

Keys are looked up and managed through the same service:

Method Purpose
CreateKeypair Register a public key for a principal; returns the generated key ID
GetKeypairByKeyID Resolve a key by its ID (the kid a verifier receives)
ListKeypairs List a principal's keys, for rotation and audit
Revoke Revoke a credential by ID with a reason

Choosing a Mechanism

Need Use
A backend calling SystemForge-protected APIs today An OAuth client with the client credentials grant
A long-lived scoped secret for an integration An API key credential on a service principal
Public-key authentication without shared secrets Keypair credentials, once the JWT Bearer grant is implemented