huudis

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Apr 19, 2026 License: MIT Imports: 21 Imported by: 0

README

huudis (Go)

Official Go SDK for Huudis.

Install

go get github.com/hachimi-cat/huudis-go

We plan to mirror this package to a dedicated github.com/hachimi-cat/huudis-go repo for shorter import paths in v0.2.

Quickstart

Env vars (or pass them to NewClient):

HUUDIS_ISSUER=https://huudis.com
HUUDIS_AUDIENCE=oc_your_client_id
HUUDIS_CLIENT_ID=oc_your_client_id
HUUDIS_CLIENT_SECRET=cs_...       # omit for public clients (PKCE)
Verify an access token
package main

import (
    "encoding/json"
    "net/http"

    huudis "github.com/hachimi-cat/huudis-go"
)

func main() {
    http.HandleFunc("/me", func(w http.ResponseWriter, r *http.Request) {
        claims, err := huudis.VerifyAccessToken(
            r.Context(),
            r.Header.Get("Authorization"),
            huudis.VerifyOptions{},
        )
        if err != nil {
            http.Error(w, err.Error(), http.StatusUnauthorized)
            return
        }
        _ = json.NewEncoder(w).Encode(map[string]string{
            "userId": claims.Sub,
            "email":  claims.Email,
        })
    })
    _ = http.ListenAndServe(":8080", nil)
}
OIDC sign-in flow
client, err := huudis.NewClient(huudis.ClientOptions{})
if err != nil { /* ... */ }

// Step 1: redirect the user
http.Redirect(w, r, client.AuthorizationURL(huudis.AuthorizationURLOptions{
    RedirectURI:   "https://yourapp.com/callback",
    State:         session.State,
    CodeChallenge: session.PKCEChallenge,
}), http.StatusFound)

// Step 2: exchange the code
tokens, err := client.ExchangeCode(r.Context(),
    r.URL.Query().Get("code"),
    "https://yourapp.com/callback",
    session.PKCEVerifier)
if err != nil { /* ... */ }

info, _ := client.UserInfo(r.Context(), tokens.AccessToken)
Authorization check
result, err := client.AuthzCheck(r.Context(), accessToken, huudis.AuthzCheckInput{
    Principal: struct {
        Type        string `json:"type"`
        ID          string `json:"id"`
        AccountID   string `json:"accountId"`
        MfaVerified bool   `json:"mfaVerified,omitempty"`
    }{Type: "user", ID: claims.Sub, AccountID: claims.AccountID},
    Action:   "plugipay:DeleteInvoice",
    Resource: "forjio:plugipay::acc_.../invoice/inv_9F8",
})
if err != nil || !result.Allow {
    http.Error(w, "forbidden: "+result.Reason, http.StatusForbidden)
    return
}

What's in the box

Symbol Purpose
VerifyAccessToken(ctx, hdr, VerifyOptions{}) Package-level — reads HUUDIS_ISSUER / HUUDIS_AUDIENCE from env.
Client / NewClient(ClientOptions{}) Full surface — OIDC code flow, refresh, userinfo, authz check.
Claims Typed view over a Huudis JWT payload.
*Error Single error type; branch on .Code.

JWKS keys are fetched per issuer and cached for one hour (auto-refreshed on unknown kid to handle rotation).

Docs

License

MIT

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func VerifyWebhookSignature added in v0.2.0

func VerifyWebhookSignature(
	rawBody []byte,
	signatureHeader string,
	secret string,
	options *VerifyWebhookSignatureOptions,
) bool

VerifyWebhookSignature returns true iff the X-Huudis-Signature header over the given raw request body validates against the provided secret within the tolerance window.

The signature header looks like "t=<unix>,v1=<hex>", where <hex> is HMAC-SHA256(secret, fmt.Sprintf("%d.%s", unix, rawBody)). Mount your webhook handler such that you can see the raw bytes — if you let a framework parse JSON first and re-stringify, whitespace drift will break the signature.

net/http example:

func huudisWebhook(w http.ResponseWriter, r *http.Request) {
    body, err := io.ReadAll(r.Body)
    if err != nil { http.Error(w, "bad body", 400); return }
    sig := r.Header.Get("X-Huudis-Signature")
    if !huudis.VerifyWebhookSignature(body, sig, os.Getenv("HUUDIS_WEBHOOK_SECRET"), nil) {
        http.Error(w, "bad signature", 400); return
    }
    // unmarshal body, handle event, reply 204.
}

Types

type AuthorizationURLOptions

type AuthorizationURLOptions struct {
	RedirectURI         string
	State               string
	Scope               string // default: "openid profile email"
	CodeChallenge       string // base64url of sha256(verifier). Strongly recommended.
	CodeChallengeMethod string // default: "S256"
	LoginHint           string // pre-fills the login form
}

AuthorizationURLOptions controls the URL built for step 1 of the code flow.

type AuthzCheckInput

type AuthzCheckInput struct {
	Principal struct {
		Type        string `json:"type"` // "user" | "group" | "role" | "service_account"
		ID          string `json:"id"`
		AccountID   string `json:"accountId"`
		MfaVerified bool   `json:"mfaVerified,omitempty"`
	} `json:"principal"`
	Action   string         `json:"action"`
	Resource string         `json:"resource"`
	Context  map[string]any `json:"context,omitempty"`
}

AuthzCheckInput — one request to /authz/check.

type AuthzCheckResult

type AuthzCheckResult struct {
	Decision   string `json:"decision"` // "Allow" | "Deny" | "ImplicitDeny"
	Allow      bool   `json:"allow"`
	Reason     string `json:"reason,omitempty"`
	MatchedSid string `json:"matchedSid,omitempty"`
}

AuthzCheckResult — what Huudis returns.

type Claims

type Claims struct {
	Iss          string `json:"iss"`
	Aud          string `json:"aud"`
	Sub          string `json:"sub"`
	Exp          int64  `json:"exp"`
	Iat          int64  `json:"iat"`
	AccountID    string `json:"accountId"`
	IdentityID   string `json:"identityId"`
	IdentityType string `json:"identityType"` // "user" | "service_account"
	Scope        string `json:"scope"`
	MfaVerified  bool   `json:"mfaVerified,omitempty"`
	Email        string `json:"email,omitempty"`
	EmailVerif   bool   `json:"email_verified,omitempty"`
	Name         string `json:"name,omitempty"`
	// Raw contains every claim exactly as decoded — use this for custom fields.
	Raw map[string]any `json:"-"`
}

Claims is the typed view over a Huudis-issued JWT. Every Huudis token carries the Forjio-specific fields below alongside the standard OIDC claims.

func VerifyAccessToken

func VerifyAccessToken(ctx context.Context, tokenOrHeader string, opts VerifyOptions) (*Claims, error)

VerifyAccessToken verifies a Huudis-issued JWT against the issuer's JWKS. Strips a leading `Bearer ` if present, so you can pass `r.Header.Get("Authorization")` directly.

Reads `HUUDIS_ISSUER` and `HUUDIS_AUDIENCE` from env when the fields on VerifyOptions are empty — same convenience the Node + Python SDKs have.

type Client

type Client struct {
	Issuer       string
	ClientID     string
	ClientSecret string
	Audience     string
	APIBase      string
	HTTP         *http.Client
}

Client is the high-level SDK surface: JWT verification, OIDC code flow, refresh, userinfo, authz check.

func NewClient

func NewClient(opts ClientOptions) (*Client, error)

func (*Client) AuthorizationURL

func (c *Client) AuthorizationURL(opts AuthorizationURLOptions) string

AuthorizationURL builds the /authorize redirect target.

func (*Client) AuthzCheck

func (c *Client) AuthzCheck(ctx context.Context, accessToken string, in AuthzCheckInput) (*AuthzCheckResult, error)

AuthzCheck calls /authz/check. The access token must belong to a principal with iam:AuthzCheck permission.

func (*Client) ExchangeCode

func (c *Client) ExchangeCode(ctx context.Context, code, redirectURI, codeVerifier string) (*TokenResponse, error)

ExchangeCode swaps an authorization code for tokens.

func (*Client) RefreshAccessToken

func (c *Client) RefreshAccessToken(ctx context.Context, refreshToken string) (*TokenResponse, error)

RefreshAccessToken mints a new access token off a refresh token.

func (*Client) UserInfo

func (c *Client) UserInfo(ctx context.Context, accessToken string) (map[string]any, error)

UserInfo fetches the standard OIDC userinfo for a bearer token.

func (*Client) VerifyAccessToken

func (c *Client) VerifyAccessToken(ctx context.Context, tokenOrHeader string, requireMFA bool) (*Claims, error)

VerifyAccessToken — client-scoped convenience wrapper.

type ClientOptions

type ClientOptions struct {
	Issuer       string // defaults to HUUDIS_ISSUER
	ClientID     string // defaults to HUUDIS_CLIENT_ID
	ClientSecret string // defaults to HUUDIS_CLIENT_SECRET; empty for public clients
	Audience     string // defaults to ClientID
	APIBase      string // defaults to Issuer
	HTTP         *http.Client
}

ClientOptions matches the env-var defaults of the Node + Python SDKs.

type Error

type Error struct {
	Code    string
	Message string
}

Error is the single error type returned by every operation in this package. Wraps an opaque code plus a message so callers can branch on well-defined error reasons without matching strings.

func (*Error) Error

func (e *Error) Error() string

type TokenResponse

type TokenResponse struct {
	AccessToken  string `json:"access_token"`
	TokenType    string `json:"token_type"`
	ExpiresIn    int    `json:"expires_in"`
	RefreshToken string `json:"refresh_token,omitempty"`
	IDToken      string `json:"id_token,omitempty"`
	Scope        string `json:"scope"`
}

TokenResponse wraps every /token grant result.

type VerifyOptions

type VerifyOptions struct {
	Issuer     string
	Audience   string
	RequireMFA bool
}

VerifyOptions customizes verification. Zero-value is fine: issuer + audience will be read from HUUDIS_ISSUER / HUUDIS_AUDIENCE env vars.

type VerifyWebhookSignatureOptions added in v0.2.0

type VerifyWebhookSignatureOptions struct {
	// ToleranceSeconds rejects signatures with a timestamp older than this
	// many seconds. Zero or negative means "use the default" (300).
	ToleranceSeconds int64
	// Now is the clock used for the freshness check. Nil means time.Now.
	// Useful in tests.
	Now func() time.Time
}

VerifyWebhookSignatureOptions tunes the webhook verifier. Pass a pointer to a zero-value struct to use the defaults.

Jump to

Keyboard shortcuts

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