Authsome

Creating Custom Plugins

Step-by-step guide to building custom Authsome plugins with strategies, hooks, migrations, and routes.

This guide walks through building a custom Authsome plugin from scratch. By the end, you will have a plugin that implements the core interface, hooks into the authentication lifecycle, contributes database migrations, and registers HTTP routes.

Step 1: Implement the Plugin interface

Every plugin must implement plugin.Plugin with a Name() method:

package auditlog

import "github.com/xraph/authsome/plugin"

// Compile-time interface check.
var _ plugin.Plugin = (*Plugin)(nil)

type Plugin struct {
    store AuditStore
}

func New(store AuditStore) *Plugin {
    return &Plugin{store: store}
}

func (p *Plugin) Name() string { return "auditlog" }

The name must be unique across all registered plugins. Use a short, lowercase identifier.

Step 2: Capture engine references with OnInit

Most plugins need access to the engine's store, bridges, or configuration. Implement plugin.OnInit to capture these during startup:

var _ plugin.OnInit = (*Plugin)(nil)

func (p *Plugin) OnInit(_ context.Context, engine any) error {
    // Use duck-typing to avoid import cycles with the engine package.
    type storeGetter interface {
        Store() store.Store
    }
    if sg, ok := engine.(storeGetter); ok {
        p.coreStore = sg.Store()
    }

    type loggerGetter interface {
        Logger() log.Logger
    }
    if lg, ok := engine.(loggerGetter); ok {
        p.logger = lg.Logger()
    }

    return nil
}

The engine parameter is typed as any to avoid circular imports. Use small interface assertions to extract what you need.

Step 3: Subscribe to lifecycle hooks

Implement any combination of before/after hook interfaces to react to authentication events:

var _ plugin.AfterSignIn = (*Plugin)(nil)

func (p *Plugin) OnAfterSignIn(ctx context.Context, u *user.User, s *session.Session) error {
    return p.store.RecordLogin(ctx, &AuditEntry{
        UserID:    u.ID,
        SessionID: s.ID,
        Action:    "signin",
        Timestamp: time.Now(),
    })
}

Before-hooks vs after-hooks

Before-hooks run synchronously before the operation. They can abort the operation by returning an error:

var _ plugin.BeforeSignUp = (*Plugin)(nil)

func (p *Plugin) OnBeforeSignUp(ctx context.Context, req *account.SignUpRequest) error {
    if p.isBlockedDomain(req.Email) {
        return fmt.Errorf("registration from this domain is not allowed")
    }
    return nil
}

After-hooks run after the operation completes. Errors are logged but never propagated -- they must not block the pipeline.

Step 4: Add database migrations

If your plugin needs its own tables, implement plugin.MigrationProvider:

import "github.com/xraph/grove/migrate"

var _ plugin.MigrationProvider = (*Plugin)(nil)

var PostgresMigrations = &migrate.Group{
    Name: "auditlog",
    Migrations: []*migrate.Migration{
        {
            Version: 1,
            Up: `CREATE TABLE IF NOT EXISTS audit_log (
                id TEXT PRIMARY KEY,
                user_id TEXT NOT NULL,
                session_id TEXT,
                action TEXT NOT NULL,
                metadata JSONB,
                created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
            );
            CREATE INDEX idx_audit_log_user ON audit_log(user_id);`,
            Down: `DROP TABLE IF EXISTS audit_log;`,
        },
    },
}

func (p *Plugin) MigrationGroups(driverName string) []*migrate.Group {
    switch driverName {
    case "pg":
        return []*migrate.Group{PostgresMigrations}
    default:
        return nil
    }
}

The engine collects migration groups from all plugins and runs them alongside core schema migrations during engine.Start().

Step 5: Register HTTP routes

If your plugin exposes an API, implement plugin.RouteProvider:

import "github.com/xraph/forge"

var _ plugin.RouteProvider = (*Plugin)(nil)

func (p *Plugin) RegisterRoutes(r any) error {
    router, ok := r.(forge.Router)
    if !ok {
        return fmt.Errorf("auditlog: expected forge.Router, got %T", r)
    }

    g := router.Group("/v1/auth/audit", forge.WithGroupTags("Audit Log"))

    return g.GET("/events", p.handleListEvents,
        forge.WithSummary("List audit events"),
        forge.WithOperationID("listAuditEvents"),
        forge.WithErrorResponses(),
    )
}

Routes are registered after OnInit, so you can safely use engine references in your handlers.

Step 6: Contribute an authentication strategy

If your plugin provides a new way to authenticate requests, implement plugin.StrategyProvider:

var _ plugin.StrategyProvider = (*Plugin)(nil)

func (p *Plugin) Strategy() strategy.Strategy {
    return &customStrategy{store: p.store}
}

func (p *Plugin) StrategyPriority() int {
    return 200 // Higher numbers = evaluated later
}

The strategy registry evaluates strategies in priority order. Session-based auth typically has priority 0, API key auth 100.

Step 7: Support GDPR data export

If your plugin stores user-specific data, implement plugin.DataExportContributor:

var _ plugin.DataExportContributor = (*Plugin)(nil)

func (p *Plugin) ExportUserData(ctx context.Context, userID id.UserID) (string, any, error) {
    entries, err := p.store.GetUserEntries(ctx, userID)
    if err != nil {
        return "", nil, err
    }
    return "audit_log", entries, nil
}

Step 8: Register the plugin

Add your plugin to the engine during construction:

eng, err := authsome.NewEngine(
    authsome.WithStore(store),
    authsome.WithPlugin(password.New()),
    authsome.WithPlugin(auditlog.New(auditStore)),
)

Testing plugins

Test plugins in isolation by calling their hook methods directly:

func TestAuditLogPlugin(t *testing.T) {
    mockStore := &MockAuditStore{}
    p := auditlog.New(mockStore)

    u := &user.User{ID: id.NewUserID(), Email: "test@example.com"}
    s := &session.Session{ID: id.NewSessionID()}

    err := p.OnAfterSignIn(context.Background(), u, s)
    require.NoError(t, err)

    entries := mockStore.Entries()
    require.Len(t, entries, 1)
    assert.Equal(t, "signin", entries[0].Action)
}

For integration tests, create a full engine with your plugin and exercise it through the API:

func TestAuditLogIntegration(t *testing.T) {
    eng, err := authsome.NewEngine(
        authsome.WithStore(memoryStore),
        authsome.WithPlugin(auditlog.New(memoryAuditStore)),
    )
    require.NoError(t, err)
    require.NoError(t, eng.Start(context.Background()))

    // Sign in and verify audit entries were created
    sess, err := eng.SignIn(ctx, &account.SignInRequest{
        Email:    "alice@example.com",
        Password: "password123",
    })
    require.NoError(t, err)

    entries := memoryAuditStore.Entries()
    require.NotEmpty(t, entries)
}

Complete example

Here is the full plugin in one file:

package auditlog

import (
    "context"
    "fmt"
    "time"

    "github.com/xraph/authsome/plugin"
    "github.com/xraph/authsome/session"
    "github.com/xraph/authsome/user"
    log "github.com/xraph/go-utils/log"
)

var (
    _ plugin.Plugin      = (*Plugin)(nil)
    _ plugin.OnInit      = (*Plugin)(nil)
    _ plugin.AfterSignIn = (*Plugin)(nil)
    _ plugin.AfterSignUp = (*Plugin)(nil)
)

type Plugin struct {
    store  AuditStore
    logger log.Logger
}

func New(store AuditStore) *Plugin {
    return &Plugin{store: store}
}

func (p *Plugin) Name() string { return "auditlog" }

func (p *Plugin) OnInit(_ context.Context, engine any) error {
    type loggerGetter interface{ Logger() log.Logger }
    if lg, ok := engine.(loggerGetter); ok {
        p.logger = lg.Logger()
    }
    return nil
}

func (p *Plugin) OnAfterSignIn(ctx context.Context, u *user.User, s *session.Session) error {
    return p.record(ctx, u.ID.String(), s.ID.String(), "signin")
}

func (p *Plugin) OnAfterSignUp(ctx context.Context, u *user.User, s *session.Session) error {
    return p.record(ctx, u.ID.String(), s.ID.String(), "signup")
}

func (p *Plugin) record(ctx context.Context, userID, sessionID, action string) error {
    if err := p.store.RecordLogin(ctx, userID, sessionID, action, time.Now()); err != nil {
        if p.logger != nil {
            p.logger.Warn("auditlog: failed to record",
                log.String("action", action),
                log.String("error", err.Error()),
            )
        }
    }
    return nil
}

On this page