Skip to main content

Custom Auth Providers

vibeD authenticates every request to its MCP, /api, /v1, and internal-source endpoints with a bearer token verifier. The core ships three modes — apikey, oauth, and oidc — but the verifier is pluggable: you can register your own auth mode (SAML SP, a bespoke JWT issuer, an internal token service) from a separate Go module without touching the core.

You do this through the public extension surface in pkg/plugin. It re-exports the internal auth types as aliases, so a value that satisfies plugin.TokenVerifier also satisfies the internal interface — no adapter needed.

This page assumes you have read Authentication & HTTPS, which covers the built-in modes and config.

What an auth mode contributes

An auth mode is a factory that returns a Provider:

// package github.com/vibed-project/vibeD/pkg/plugin

type Provider struct {
Verifier TokenVerifier // required: validates the bearer token on every request
Routes []Route // optional: PUBLIC login routes mounted OUTSIDE auth
}
FieldRequiredPurpose
VerifieryesChecked on every authenticated request. A nil verifier is a startup error.
RoutesnoPublic HTTP endpoints your login flow needs (e.g. an SSO metadata / callback endpoint).

A bearer-only mode (like the built-in apikey/oauth/oidc) leaves Routes empty.

The verifier

TokenVerifier and TokenInfo are the MCP SDK types the core wraps in auth.RequireBearerToken:

type TokenVerifier func(ctx context.Context, token string, req *http.Request) (*TokenInfo, error)

Return a *TokenInfo on success — its UserID becomes the authenticated identity for the rest of the request. Return mcpauth.ErrInvalidToken (from github.com/modelcontextprotocol/go-sdk/auth) on a bad token; the middleware turns that into a 401.

TokenInfo carries at least:

FieldMeaning
UserIDStable identity string. Drives ownership, role lookup, audit.
ScopesOptional granted scopes.
ExpirationWhen the token is no longer valid.

The factory

AuthProviderFactory builds the Provider for one mode. It does its own config validation and returns a descriptive error:

type AuthProviderFactory func(cfg AuthConfig, userStore UserStore, logger *slog.Logger) (*Provider, error)
  • cfg is the whole auth: config section. Read your mode's settings from cfg.Options (a map[string]string) — the core modes ignore it, so you never have to add a field to the config struct.
  • userStore may be nil. When present you can auto-provision user records on first login and look users up.
  • Return a descriptive error (e.g. "metadata URL not configured") rather than panicking; the process fails to start with your message.

Registering a mode

Register from an init() in your provider package:

func RegisterAuthProvider(mode string, f AuthProviderFactory)
func AuthProviders() []string // registered mode names, sorted

RegisterAuthProvider panics on a nil factory or a duplicate mode — both are programmer errors surfaced at process start. The mode name is what operators put in auth.mode.

Selecting your mode

vibeD picks the mode from the auth.mode config key and hands your factory the settings under auth.options:

auth:
enabled: true
mode: "saml" # your registered mode name
options:
metadataURL: "https://idp.example.com/metadata"
acsPath: "/saml/acs"
entityID: "https://vibed.example.com"

Your factory reads those with cfg.Options["metadataURL"], etc.

Config validation currently allow-lists the built-in modes

config.Validate() rejects any auth.mode outside apikey, oauth, and oidc, so a freshly registered custom mode will fail validation before the registry is consulted. To ship a custom mode today you must widen that allow-list in your fork/build (or run with validation adjusted). The registry itself imposes no such restriction — an empty auth.mode defaults to apikey.

Public login routes

Bearer verification answers "is this request already authenticated?" — but an SSO flow needs endpoints a user can reach before they have a session: an IdP-redirect, an assertion-consumer / callback endpoint, a metadata document. Those are Provider.Routes.

type Route struct {
Pattern string // net/http ServeMux pattern, e.g. "POST /saml/acs"
Handler http.Handler
}

Provider routes are mounted outside the bearer-auth middleware — they are how a user obtains a session in the first place, so they cannot themselves require one. Keep them on public paths, clear of the authenticated prefixes (/api, /v1, /mcp, /internal/sources).

Suspended-user check

The core wraps your verifier with one extra check it enforces at auth time, so you do not implement it yourself:

  • After your verifier returns a UserID, if a userStore is present the core loads that user and rejects the request with 401 account suspended when user.Status == "suspended".

Your verifier's job is just to authenticate the token and yield the right UserID; suspension, role resolution, and audit are layered on by the core.

Skeleton provider

A minimal mode with a verifier and one public callback route:

package myauth

import (
"context"
"fmt"
"log/slog"
"net/http"

mcpauth "github.com/modelcontextprotocol/go-sdk/auth"
"github.com/vibed-project/vibeD/pkg/plugin"
)

func init() {
plugin.RegisterAuthProvider("mymode", newProvider)
}

func newProvider(cfg plugin.AuthConfig, users plugin.UserStore, logger *slog.Logger) (*plugin.Provider, error) {
issuer := cfg.Options["issuer"]
if issuer == "" {
return nil, fmt.Errorf("auth.options.issuer is required for mode 'mymode'")
}

return &plugin.Provider{
Verifier: verify(issuer, users, logger),
Routes: []plugin.Route{
// PUBLIC: mounted outside the bearer-auth middleware.
{Pattern: "GET /mymode/callback", Handler: callback(users, logger)},
},
}, nil
}

func verify(issuer string, users plugin.UserStore, logger *slog.Logger) plugin.TokenVerifier {
return func(ctx context.Context, token string, req *http.Request) (*plugin.TokenInfo, error) {
userID, ok := validate(ctx, issuer, token) // your token check
if !ok {
logger.Debug("mymode: token rejected", "path", req.URL.Path)
return nil, mcpauth.ErrInvalidToken // -> 401
}
// Optionally auto-provision on first login when users != nil.
return &plugin.TokenInfo{UserID: userID}, nil
}
}

func callback(users plugin.UserStore, logger *slog.Logger) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Complete the SSO handshake, mint/return a bearer token the client
// then sends as `Authorization: Bearer ...` to authenticated endpoints.
w.WriteHeader(http.StatusOK)
})
}

Wiring it into a binary

Go forbids importing another module's internal/, which is why pkg/plugin exists: it re-exports the registry interfaces as type aliases, so your out-of-tree type satisfies the core interface directly. To ship a custom binary, blank-import your provider package (its init() calls RegisterAuthProvider) and call the standard server entrypoint:

package main

import (
_ "example.com/vibed-providers/myauth" // init() registers "mymode"

"github.com/vibed-project/vibeD/pkg/server"
)

func main() {
server.Main()
}

Build it with the same toolchain settings as the core:

GOTOOLCHAIN=auto GO111MODULE=on go build ./...

Set auth.mode: mymode in your config and the server resolves your factory at startup.

See also