caep

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 21, 2026 License: Apache-2.0 Imports: 5 Imported by: 0

README

go-caep

a typed event vocabulary for the OpenID Continuous Access Evaluation Profile.

Implements OpenID CAEP 1.0 (Final Specification, 2025-08-29). Spec: https://openid.net/specs/openid-caep-1_0.html

What this library is and is not

It is the CAEP event vocabulary: the CAEP event-type URIs and a typed Go struct for each event payload — session-revoked, token-claims-change, credential-change, assurance-level-change, device-compliance-change, session-established, session-presented, and risk-level-change — together with decoders registered into the go-secevent event registry. Once registered, a parsed Security Event Token's events decode to typed CAEP values, and CAEP events can be built and encoded back into a SET.

CAEP is the "something changed about an ongoing session or credential" half of the Shared Signals Framework. Each event is a JSON object carried under its event-type URI in a SET's events map, sharing a common envelope (event_timestamp, initiating_entity, reason_admin, reason_user) plus event-specific fields. The subject is named by the SET's sub_id.

The following are deliberately out of scope and belong in dedicated libraries:

  • The SET envelope, parsing, validation, and the event registry — these are go-secevent (RFC 8417). This library plugs its event types into that registry.
  • JWS signature verification / signing and transport — a JOSE/Shared Signals transport layer hands go-secevent the decoded claims-set bytes.
  • RISC events (the account-lifecycle half of Shared Signals) — a sibling vocabulary that registers into the same registry.

Install

go get github.com/hstern/go-caep

Quickstart

Receive: parse a SET into typed CAEP events

A blank import registers every CAEP decoder with go-secevent. Parse takes the already-verified, base64url-decoded claims-set bytes; Events.Typed decodes a known event through the registry.

import (
	caep "github.com/hstern/go-caep"
	secevent "github.com/hstern/go-secevent"
	_ "github.com/hstern/go-caep" // registers the CAEP event decoders
)

set, err := secevent.Parse(payload)
if err != nil {
	return err
}
ev, ok, err := set.Events.Typed(caep.EventCredentialChange)
switch {
case err != nil:
	return err // the payload was malformed for this event type
case ok:
	cc := ev.(caep.CredentialChange)
	fmt.Println(cc.ChangeType, cc.CredentialType)
default:
	// no decoder registered for this URI: it stays raw and round-trips
}
Transmit: build a CAEP event into a SET

Put marshals a typed event into a SET's events map under its event-type URI. The optional Validate enforces the CAEP required-field MUSTs before you send.

e := caep.SessionRevoked{Base: caep.Base{
	EventTimestamp:   secevent.NewNumericDate(time.Now()),
	InitiatingEntity: caep.InitiatingEntityPolicy,
}}
if err := caep.Validate(e); err != nil {
	return err
}

set := &secevent.SET{ /* iss, iat, jti, aud, sub_id … */ Events: secevent.Events{}}
if err := caep.Put(set.Events, e); err != nil {
	return err
}
payload, err := set.Encode() // claims-set bytes for a JOSE signer

Unrecognized members on any event are preserved as json.RawMessage and round-trip byte-stably, so an event from a newer CAEP profile survives a decode/encode cycle unchanged. See the package examples for runnable versions.

Status

v0.1.0 — the first tagged release. As a v0.x series, the public API may still change per SemVer before v1.0.0. Runtime dependencies: the standard library plus go-secevent (which in turn uses go-subjectid).

License

Apache-2.0.

Documentation

Overview

Package caep implements the OpenID Continuous Access Evaluation Profile (CAEP) 1.0 event vocabulary: the CAEP event-type URIs and their typed event payloads, registered into the go-secevent event registry so a Security Event Token's events decode to typed Go values.

CAEP events ride inside an RFC 8417 Security Event Token (the go-secevent envelope). This package owns only the CAEP event payloads — one typed struct per event type, each carried under its event-type URI in a SET's events map. Importing the package for its side effects registers every event type's decoder with go-secevent:

import (
	secevent "github.com/hstern/go-secevent"
	_ "github.com/hstern/go-caep" // registers the CAEP event decoders
)

set, _ := secevent.Parse(payload)
if ev, ok, _ := set.Events.Typed(caep.EventSessionRevoked); ok {
	revoked := ev.(caep.SessionRevoked)
	_ = revoked.InitiatingEntity
}

Spec: https://openid.net/specs/openid-caep-1_0.html

Example (Decode)

Example_decode shows the receiver side: parse an already-verified SET claims set and recover the typed CAEP event. Importing go-caep for its side effects (the blank import in this package's other files) registers the decoders.

package main

import (
	"fmt"

	caep "github.com/hstern/go-caep"
	secevent "github.com/hstern/go-secevent"
)

func main() {
	payload := []byte(`{
		"iss": "https://idp.example.com/",
		"iat": 1615305600,
		"jti": "set-0001",
		"aud": "https://rp.example.com/",
		"events": {
			"https://schemas.openid.net/secevent/caep/event-type/credential-change": {
				"credential_type": "fido2-roaming",
				"change_type": "create",
				"friendly_name": "YubiKey 5C"
			}
		}
	}`)

	set, err := secevent.Parse(payload)
	if err != nil {
		fmt.Println("parse:", err)
		return
	}

	ev, ok, err := set.Events.Typed(caep.EventCredentialChange)
	if err != nil || !ok {
		fmt.Println("not a recognized credential-change event")
		return
	}
	cc := ev.(caep.CredentialChange)
	fmt.Printf("%s %s (%s)\n", cc.ChangeType, cc.CredentialType, cc.FriendlyName)

}
Output:
create fido2-roaming (YubiKey 5C)
Example (Encode)

Example_encode shows the transmitter side: build a typed CAEP event and place it into a SET's events map under its event-type URI with Put.

package main

import (
	"fmt"

	caep "github.com/hstern/go-caep"
	secevent "github.com/hstern/go-secevent"
)

func main() {
	e := caep.SessionRevoked{Base: caep.Base{
		InitiatingEntity: caep.InitiatingEntityPolicy,
		ReasonAdmin:      map[string]string{"en": "policy revoked the session"},
	}}

	events := secevent.Events{}
	if err := caep.Put(events, e); err != nil {
		fmt.Println("put:", err)
		return
	}
	fmt.Printf("%s\n", events[caep.EventSessionRevoked])

}
Output:
{"initiating_entity":"policy","reason_admin":{"en":"policy revoked the session"}}

Index

Examples

Constants

View Source
const (
	// EventSessionRevoked identifies a Session Revoked event (CAEP 1.0 §3.2).
	EventSessionRevoked = eventTypeBase + "session-revoked"
	// EventTokenClaimsChange identifies a Token Claims Change event (§3.3).
	EventTokenClaimsChange = eventTypeBase + "token-claims-change"
	// EventCredentialChange identifies a Credential Change event (§3.4).
	EventCredentialChange = eventTypeBase + "credential-change"
	// EventAssuranceLevelChange identifies an Assurance Level Change event (§3.5).
	EventAssuranceLevelChange = eventTypeBase + "assurance-level-change"
	// EventSessionEstablished identifies a Session Established event (§3.6).
	EventSessionEstablished = eventTypeBase + "session-established"
	// EventSessionPresented identifies a Session Presented event (§3.7).
	EventSessionPresented = eventTypeBase + "session-presented"
	// EventDeviceComplianceChange identifies a Device Compliance Change event (§3.8).
	EventDeviceComplianceChange = eventTypeBase + "device-compliance-change"
	// EventRiskLevelChange identifies a Risk Level Change event (§3.9).
	EventRiskLevelChange = eventTypeBase + "risk-level-change"
)

CAEP 1.0 event-type URIs. Each is the key under which an event of the corresponding type appears in a SET's events claim, and the string its payload's EventTypeURI method returns.

View Source
const SpecVersion = "CAEP 1.0"

SpecVersion is the OpenID CAEP specification version this build implements.

Variables

View Source
var (
	ErrMissingClaims         = errors.New(`caep: token-claims-change: missing required field "claims"`)
	ErrMissingCredentialType = errors.New(`caep: credential-change: missing required field "credential_type"`)
	ErrMissingChangeType     = errors.New(`caep: credential-change: missing required field "change_type"`)
	ErrMissingNamespace      = errors.New(`caep: assurance-level-change: missing required field "namespace"`)
	ErrMissingCurrentLevel   = errors.New(`caep: missing required field "current_level"`)
	ErrMissingPreviousStatus = errors.New(`caep: device-compliance-change: missing required field "previous_status"`)
	ErrMissingCurrentStatus  = errors.New(`caep: device-compliance-change: missing required field "current_status"`)
	ErrMissingPrincipal      = errors.New(`caep: risk-level-change: missing required field "principal"`)
)

Missing-required-field errors returned by Validate. They identify the CAEP required-field MUSTs; match them with errors.Is. When an event is missing more than one required field, Validate joins the corresponding errors.

Functions

func Put

func Put(events secevent.Events, e secevent.Event) error

Put marshals e and stores it in events under e.EventTypeURI(), the producer- side counterpart to go-secevent's Events.Typed. The events map must be non-nil. It is a thin convenience over json.Marshal plus a map assignment.

func Validate

func Validate(e secevent.Event) error

Validate reports whether e carries the fields CAEP 1.0 requires for its event type. It returns nil when e is complete, or the missing-field sentinel errors joined together (inspect with errors.Is) when it is not.

Validation is opt-in and wire-shape-bound only: decoding never calls it, and it never asserts anything about an event's meaning. Event types with no required event-specific fields — session-revoked, session-established, session-presented, and any non-CAEP event — always validate.

Types

type AssuranceLevelChange

type AssuranceLevelChange struct {
	Base
	// Namespace is the assurance framework CurrentLevel is expressed in (required).
	Namespace AssuranceNamespace `json:"namespace,omitempty"`
	// CurrentLevel is the new assurance level, per Namespace (required).
	CurrentLevel string `json:"current_level,omitempty"`
	// PreviousLevel is the prior assurance level; omitted means unknown.
	PreviousLevel string `json:"previous_level,omitempty"`
	// ChangeDirection reports whether the level increased or decreased.
	ChangeDirection ChangeDirection `json:"change_direction,omitempty"`
	// contains filtered or unexported fields
}

AssuranceLevelChange is an Assurance Level Change event (CAEP 1.0 §3.5), signaling that the subject's identity-assurance level changed. Namespace and CurrentLevel are required (enforced by Validate); an omitted PreviousLevel means the previous level is unknown.

func (AssuranceLevelChange) EventTypeURI

func (AssuranceLevelChange) EventTypeURI() string

EventTypeURI reports the CAEP Assurance Level Change event-type URI.

func (AssuranceLevelChange) MarshalJSON

func (e AssuranceLevelChange) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler, re-emitting any open-extension members.

func (*AssuranceLevelChange) UnmarshalJSON

func (e *AssuranceLevelChange) UnmarshalJSON(b []byte) error

UnmarshalJSON implements json.Unmarshaler, capturing unrecognized members.

type AssuranceNamespace

type AssuranceNamespace string

AssuranceNamespace names the framework an assurance level is expressed in (CAEP 1.0 §3.5). The spec permits custom values, so decoding is lenient.

const (
	AssuranceNamespaceRFC8176     AssuranceNamespace = "RFC8176"
	AssuranceNamespaceRFC6711     AssuranceNamespace = "RFC6711"
	AssuranceNamespaceISOIEC29115 AssuranceNamespace = "ISO-IEC-29115"
	AssuranceNamespaceNISTIAL     AssuranceNamespace = "NIST-IAL"
	AssuranceNamespaceNISTAAL     AssuranceNamespace = "NIST-AAL"
	AssuranceNamespaceNISTFAL     AssuranceNamespace = "NIST-FAL"
)

The AssuranceNamespace values listed by CAEP 1.0 §3.5.

type Base

type Base struct {
	// EventTimestamp is when the event occurred, as an RFC 7519 NumericDate
	// (seconds since the Unix epoch). Reuses go-secevent's NumericDate so the
	// wire form matches the SET envelope's iat/toe claims.
	EventTimestamp *secevent.NumericDate `json:"event_timestamp,omitempty"`
	// InitiatingEntity is what caused the event (admin, user, policy, system).
	InitiatingEntity InitiatingEntity `json:"initiating_entity,omitempty"`
	// ReasonAdmin is an administrative message keyed by BCP47 language tag.
	ReasonAdmin map[string]string `json:"reason_admin,omitempty"`
	// ReasonUser is an end-user message keyed by BCP47 language tag.
	ReasonUser map[string]string `json:"reason_user,omitempty"`
}

Base is the common envelope every CAEP event may carry (CAEP 1.0 §3.1). It is embedded by each event type; all four fields are optional. The subject of a CAEP event is the enclosing SET's sub_id, not a field here.

type ChangeDirection

type ChangeDirection string

ChangeDirection reports whether an assurance level rose or fell (§3.5).

const (
	ChangeDirectionIncrease ChangeDirection = "increase"
	ChangeDirectionDecrease ChangeDirection = "decrease"
)

The ChangeDirection values defined by CAEP 1.0 §3.5.

type ComplianceStatus

type ComplianceStatus string

ComplianceStatus is a device compliance status (CAEP 1.0 §3.8).

const (
	ComplianceStatusCompliant    ComplianceStatus = "compliant"
	ComplianceStatusNotCompliant ComplianceStatus = "not-compliant"
)

The ComplianceStatus values defined by CAEP 1.0 §3.8.

type CredentialChange

type CredentialChange struct {
	Base
	// CredentialType is the kind of credential that changed (required).
	CredentialType CredentialType `json:"credential_type,omitempty"`
	// ChangeType is the kind of change (required).
	ChangeType CredentialChangeType `json:"change_type,omitempty"`
	// FriendlyName is a human-readable credential identifier.
	FriendlyName string `json:"friendly_name,omitempty"`
	// X509Issuer is the issuer of an x509 credential.
	X509Issuer string `json:"x509_issuer,omitempty"`
	// X509Serial is the serial number of an x509 credential.
	X509Serial string `json:"x509_serial,omitempty"`
	// FIDO2AAGUID is the FIDO2 Authenticator Attestation GUID.
	FIDO2AAGUID string `json:"fido2_aaguid,omitempty"`
	// contains filtered or unexported fields
}

CredentialChange is a Credential Change event (CAEP 1.0 §3.4), signaling that a credential was created, revoked, updated, or deleted for the subject. CredentialType and ChangeType are required (enforced by Validate).

func (CredentialChange) EventTypeURI

func (CredentialChange) EventTypeURI() string

EventTypeURI reports the CAEP Credential Change event-type URI.

func (CredentialChange) MarshalJSON

func (e CredentialChange) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler, re-emitting any open-extension members.

func (*CredentialChange) UnmarshalJSON

func (e *CredentialChange) UnmarshalJSON(b []byte) error

UnmarshalJSON implements json.Unmarshaler, capturing unrecognized members.

type CredentialChangeType

type CredentialChangeType string

CredentialChangeType is the kind of change in a CredentialChange (§3.4).

const (
	CredentialChangeCreate CredentialChangeType = "create"
	CredentialChangeRevoke CredentialChangeType = "revoke"
	CredentialChangeUpdate CredentialChangeType = "update"
	CredentialChangeDelete CredentialChangeType = "delete"
)

The CredentialChangeType values defined by CAEP 1.0 §3.4.

type CredentialType

type CredentialType string

CredentialType identifies the kind of credential in a CredentialChange (CAEP 1.0 §3.4). Decoding is lenient: an unrecognized value is preserved.

const (
	CredentialTypePassword             CredentialType = "password"
	CredentialTypePIN                  CredentialType = "pin"
	CredentialTypeX509                 CredentialType = "x509"
	CredentialTypeFIDO2Platform        CredentialType = "fido2-platform"
	CredentialTypeFIDO2Roaming         CredentialType = "fido2-roaming"
	CredentialTypeFIDOU2F              CredentialType = "fido-u2f"
	CredentialTypeVerifiableCredential CredentialType = "verifiable-credential"
	CredentialTypePhoneVoice           CredentialType = "phone-voice"
	CredentialTypePhoneSMS             CredentialType = "phone-sms"
	CredentialTypeApp                  CredentialType = "app"
)

The CredentialType values listed by CAEP 1.0 §3.4.

type DeviceComplianceChange

type DeviceComplianceChange struct {
	Base
	// PreviousStatus is the device's prior compliance status (required).
	PreviousStatus ComplianceStatus `json:"previous_status,omitempty"`
	// CurrentStatus is the device's new compliance status (required).
	CurrentStatus ComplianceStatus `json:"current_status,omitempty"`
	// contains filtered or unexported fields
}

DeviceComplianceChange is a Device Compliance Change event (CAEP 1.0 §3.8), signaling that a device's compliance status changed. Both statuses are required (enforced by Validate).

func (DeviceComplianceChange) EventTypeURI

func (DeviceComplianceChange) EventTypeURI() string

EventTypeURI reports the CAEP Device Compliance Change event-type URI.

func (DeviceComplianceChange) MarshalJSON

func (e DeviceComplianceChange) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler, re-emitting any open-extension members.

func (*DeviceComplianceChange) UnmarshalJSON

func (e *DeviceComplianceChange) UnmarshalJSON(b []byte) error

UnmarshalJSON implements json.Unmarshaler, capturing unrecognized members.

type InitiatingEntity

type InitiatingEntity string

InitiatingEntity identifies what caused a CAEP event (CAEP 1.0 §3.1). The spec defines four values; decoding is lenient, so an unrecognized value is preserved verbatim rather than rejected.

const (
	InitiatingEntityAdmin  InitiatingEntity = "admin"
	InitiatingEntityUser   InitiatingEntity = "user"
	InitiatingEntityPolicy InitiatingEntity = "policy"
	InitiatingEntitySystem InitiatingEntity = "system"
)

The InitiatingEntity values defined by CAEP 1.0 §3.1.

type RiskLevel

type RiskLevel string

RiskLevel is a risk rating (CAEP 1.0 §3.9).

const (
	RiskLevelLow    RiskLevel = "LOW"
	RiskLevelMedium RiskLevel = "MEDIUM"
	RiskLevelHigh   RiskLevel = "HIGH"
)

The RiskLevel values defined by CAEP 1.0 §3.9.

type RiskLevelChange

type RiskLevelChange struct {
	Base
	// Principal is the entity the risk assessment is about (required).
	Principal RiskPrincipal `json:"principal,omitempty"`
	// CurrentLevel is the present risk rating (required).
	CurrentLevel RiskLevel `json:"current_level,omitempty"`
	// PreviousLevel is the prior risk rating; omitted means unknown.
	PreviousLevel RiskLevel `json:"previous_level,omitempty"`
	// RiskReason explains the risk change (recommended).
	RiskReason string `json:"risk_reason,omitempty"`
	// contains filtered or unexported fields
}

RiskLevelChange is a Risk Level Change event (CAEP 1.0 §3.9), signaling that the assessed risk for a principal changed. Principal and CurrentLevel are required (enforced by Validate); RiskReason is recommended, and an omitted PreviousLevel means the previous level is unknown.

func (RiskLevelChange) EventTypeURI

func (RiskLevelChange) EventTypeURI() string

EventTypeURI reports the CAEP Risk Level Change event-type URI.

func (RiskLevelChange) MarshalJSON

func (e RiskLevelChange) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler, re-emitting any open-extension members.

func (*RiskLevelChange) UnmarshalJSON

func (e *RiskLevelChange) UnmarshalJSON(b []byte) error

UnmarshalJSON implements json.Unmarshaler, capturing unrecognized members.

type RiskPrincipal

type RiskPrincipal string

RiskPrincipal is the kind of entity a RiskLevelChange is about (CAEP 1.0 §3.9). The spec permits other defined entities, so decoding is lenient.

const (
	RiskPrincipalUser    RiskPrincipal = "USER"
	RiskPrincipalDevice  RiskPrincipal = "DEVICE"
	RiskPrincipalSession RiskPrincipal = "SESSION"
	RiskPrincipalTenant  RiskPrincipal = "TENANT"
	RiskPrincipalOrgUnit RiskPrincipal = "ORG_UNIT"
	RiskPrincipalGroup   RiskPrincipal = "GROUP"
)

The RiskPrincipal values listed by CAEP 1.0 §3.9.

type SessionEstablished

type SessionEstablished struct {
	Base
	// FingerprintUA is a fingerprint of the session's user agent (fp_ua).
	FingerprintUA string `json:"fp_ua,omitempty"`
	// ACR is the Authentication Context Class Reference of the session.
	ACR string `json:"acr,omitempty"`
	// AMR is the set of Authentication Methods References for the session.
	AMR []string `json:"amr,omitempty"`
	// ExtID is an external session identifier for correlation.
	ExtID string `json:"ext_id,omitempty"`
	// contains filtered or unexported fields
}

SessionEstablished is a Session Established event (CAEP 1.0 §3.6), signaling that a new session was created for the subject. All fields are optional.

func (SessionEstablished) EventTypeURI

func (SessionEstablished) EventTypeURI() string

EventTypeURI reports the CAEP Session Established event-type URI.

func (SessionEstablished) MarshalJSON

func (e SessionEstablished) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler, re-emitting any open-extension members.

func (*SessionEstablished) UnmarshalJSON

func (e *SessionEstablished) UnmarshalJSON(b []byte) error

UnmarshalJSON implements json.Unmarshaler, capturing unrecognized members.

type SessionPresented

type SessionPresented struct {
	Base
	// FingerprintUA is a fingerprint of the session's user agent (fp_ua).
	FingerprintUA string `json:"fp_ua,omitempty"`
	// ExtID is an external session identifier for correlation.
	ExtID string `json:"ext_id,omitempty"`
	// contains filtered or unexported fields
}

SessionPresented is a Session Presented event (CAEP 1.0 §3.7), signaling that an existing session was presented to a receiver. All fields are optional.

func (SessionPresented) EventTypeURI

func (SessionPresented) EventTypeURI() string

EventTypeURI reports the CAEP Session Presented event-type URI.

func (SessionPresented) MarshalJSON

func (e SessionPresented) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler, re-emitting any open-extension members.

func (*SessionPresented) UnmarshalJSON

func (e *SessionPresented) UnmarshalJSON(b []byte) error

UnmarshalJSON implements json.Unmarshaler, capturing unrecognized members.

type SessionRevoked

type SessionRevoked struct {
	Base
	// contains filtered or unexported fields
}

SessionRevoked is a Session Revoked event (CAEP 1.0 §3.2). It has no event-specific fields; it signals that the sessions of the SET's subject are revoked, so tokens issued before EventTimestamp should no longer be honored.

func (SessionRevoked) EventTypeURI

func (SessionRevoked) EventTypeURI() string

EventTypeURI reports the CAEP Session Revoked event-type URI.

func (SessionRevoked) MarshalJSON

func (e SessionRevoked) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler, re-emitting any open-extension members.

func (*SessionRevoked) UnmarshalJSON

func (e *SessionRevoked) UnmarshalJSON(b []byte) error

UnmarshalJSON implements json.Unmarshaler, capturing unrecognized members.

type TokenClaimsChange

type TokenClaimsChange struct {
	Base
	// Claims maps each changed claim name to its new value (required).
	Claims map[string]json.RawMessage `json:"claims,omitempty"`
	// contains filtered or unexported fields
}

TokenClaimsChange is a Token Claims Change event (CAEP 1.0 §3.3), signaling that one or more token claims for the subject changed value. Claims is required (enforced by Validate).

Each Claims value is kept as a json.RawMessage: the changed claims are arbitrary JSON, so their bytes round-trip unchanged rather than being coerced into a Go type.

func (TokenClaimsChange) EventTypeURI

func (TokenClaimsChange) EventTypeURI() string

EventTypeURI reports the CAEP Token Claims Change event-type URI.

func (TokenClaimsChange) MarshalJSON

func (e TokenClaimsChange) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler, re-emitting any open-extension members.

func (*TokenClaimsChange) UnmarshalJSON

func (e *TokenClaimsChange) UnmarshalJSON(b []byte) error

UnmarshalJSON implements json.Unmarshaler, capturing unrecognized members.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL