Skip to main content

Go SDK

Protect your application in ten lines. The Go SDK wraps your existing http.Handler so AuthSec owns authentication, scope-level authorization, and tool inventory — your Go code keeps doing what it already does.

Install

go get github.com/authsec-ai/sdk-authsec/packages/go-sdk

Requires Go 1.24+. The package lives in the sdk-authsec monorepo on GitHub.

Drop in the wrapper

import (
"log"
"net/http"
"os"

authsec "github.com/authsec-ai/sdk-authsec/packages/go-sdk"
)

func main() {
cfg := authsec.Config{
Issuer: "https://api.authsec.dev",
ResourceServerID: "<id from Applications screen>",
IntrospectionClientID: "<same id>",
IntrospectionClientSecret: os.Getenv("AUTHSEC_INTROSPECTION_CLIENT_SECRET"),
ResourceURI: "https://your-app.example.com/mcp",
PublishManifest: true,
}

mux := http.NewServeMux()
if err := authsec.MountMCP(mux, "/mcp", yourExistingHandler, cfg); err != nil {
log.Fatal(err)
}
log.Fatal(http.ListenAndServe(":8000", mux))
}

That's it. MountMCP registers two routes on your mux:

  • /mcp — your handler, wrapped: every request has its bearer token validated and tools/call checked against the scope policy you configured in AuthSec.
  • /.well-known/oauth-protected-resource/mcp — the RFC 9728 metadata that lets OAuth clients discover where to authenticate.

The package exports Config, MountMCP, WrapMCPHTTP, NewRuntime, Runtime, Principal, PrincipalFromContext, BuildResourceMetadataPath, ProtectedResourceHandler, the PolicyMode* and ValidationMode* constants, and the error sentinels.

The five-step launch flow

The SDK is step 2 of a five-step product flow. The rest happens in the Applications screen of the admin UI.

1. Register the application

Open Applications → Install protection. Enter a name, your Public Base URL, and the Protected Path (defaults to /mcp). AuthSec generates a one-time introspection secret — copy it, you only see it once. The application's id becomes ResourceServerID and IntrospectionClientID in your Config.

More: Register an application.

2. Install protection (this page)

Add the snippet above. Two values from step 1 land in Config:

  • ResourceServerID — the application UUID
  • IntrospectionClientSecret — read from a secret store, never hard-coded

Start your service. The SDK validates the config, fetches the scope policy from AuthSec, and (with PublishManifest: true) pushes your tool list in the background.

3. Publish the tool manifest

A tool manifest is the list of tools your application exposes, with each tool's required scopes. PublishManifest: true does this for you on every startup. The SDK runs initialize → notifications/initialized → tools/list against your unwrapped handler and PUTs the result to AuthSec. Your tools show up in the admin UI ready to be mapped.

If your handler can't service a synthetic tools/list (custom auth on initialize, dynamic registration, non-HTTP transport), use the escape hatch:

cfg.PublishManifest = true
cfg.ToolInventoryProvider = func() ([]authsec.ManifestTool, error) {
return []authsec.ManifestTool{
{Name: "search_repositories", Description: "Find repos.", SuggestedScopes: []string{"repos:read"}},
{Name: "create_issue", Description: "Open an issue.", SuggestedScopes: []string{"issues:write"}},
}, nil
}

4. Review tool access

In the admin UI, map each tool to its required scopes. Mark health-check / read-only tools public. Until every tool is mapped (or explicitly public), the application is held at "Needs review."

More: Map tools to scopes.

5. Launch

Hit Activate. From this moment the SDK's scope matrix flips to enforce-mode. Requests with the right scopes get 200s; everything else gets 401 invalid_token or 403 insufficient_scope.

The Applications screen now shows Readiness, Risk, Last Signal, and Next Action for your app.

More: Launch.

Configuration

The required fields for a production deploy:

FieldSourceNotes
IssuerAuthSec base URLe.g. https://api.authsec.dev
ResourceServerIDApplication detail panel, field idUUID
IntrospectionClientIDSame value as ResourceServerIDUsername for Basic auth
IntrospectionClientSecretOne-time secret from registrationStore in a secret manager
ResourceURIApplication detail panel, field resource_urlMust match the aud claim of issued tokens
PublishManifesttrue recommendedPushes tool inventory at startup

Optional but useful:

FieldPurpose
JWKSURLOverride JWKS endpoint. Defaults to <Issuer>/oauth/jwks.
IntrospectionURLOverride. Defaults to <Issuer>/oauth/introspect.
AuthorizationServerDefaults to Issuer. The SDK uses it for the sdk-policy / sdk-manifest URLs.
PolicyModePolicyModeRemoteRequired (default), PolicyModeRemoteWithLocalFallback, PolicyModeLocalOnly, PolicyModeOpen.
ValidationModeValidationModeJWTAndIntrospect (default), ValidationModeJWTOnly, ValidationModeIntrospectionOnly, ValidationModeJWTOrIntrospect.
ToolScopesLocal fallback policy. Required if PolicyMode = PolicyModeRemoteWithLocalFallback.
ToolScopeSuggestionsPer-tool scope hints sent in the manifest payload.
SupportedScopesThe OAuth scopes this application advertises.
ScopeMatrixTTLHow long fetched policy is cached. Default 5 min.
LoggerInject your own *slog.Logger. Every validate / authorize / deny emits a structured line.

Common scenarios

Protect a stdlib net/http server

The snippet at the top of this page. MountMCP is the easiest path when you use http.ServeMux.

Protect an application behind chi / gin / gorilla

MountMCP requires *http.ServeMux. For other routers, wrap the handler yourself:

rt, err := authsec.NewRuntime(cfg)
if err != nil {
log.Fatal(err)
}

protected := rt.Wrap(yourMCPHandler)
router.Handle("/mcp", protected)

// Also expose the metadata endpoint at the RFC 9728 path:
router.Handle(authsec.BuildResourceMetadataPath(cfg.ResourceURI), rt.ProtectedResourceHandler())

Omitting the metadata route breaks OAuth discovery for clients.

Run in observe-only mode

Validate tokens but skip scope checks while you finish step 4:

cfg.PolicyMode = authsec.PolicyModeOpen

The SDK attaches the validated Principal to the request context but allows every tool call. Switch back to PolicyModeRemoteRequired once mappings are in place.

Keep serving when AuthSec is briefly unreachable

cfg.PolicyMode = authsec.PolicyModeRemoteWithLocalFallback
cfg.ToolScopes = authsec.ToolScopeMap{
"list_repos": {"github.repo:read"},
"create_issue": {"github.issue:write"},
"health_check": {}, // empty slice = public
}

Local map is consulted only when the remote scope matrix fetch fails. Keep it roughly in sync with the admin UI.

Read the authenticated principal

func myHandler(w http.ResponseWriter, r *http.Request) {
p, ok := authsec.PrincipalFromContext(r.Context())
if !ok {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
log.Printf("subject=%s scopes=%v", p.Subject, p.Scopes)
}

Principal exposes Subject, Issuer, Audience, Scopes, Claims (raw JWT claim map), and Active.

Troubleshooting

SymptomLikely causeFix
MountMCP returns "invalid config"A required field is empty or ResourceURI is missing a schemeCheck the table above. cfg.Validate() returns specific field names.
Startup fails with policy fetch failedPolicyModeRemoteRequired + the application isn't activatedFinish step 5, or use PolicyModeRemoteWithLocalFallback during onboarding.
401 invalid_token with audience mismatchToken's aud doesn't match cfg.ResourceURIOAuth client must request resource=<your ResourceURI>.
403 insufficient_scope on a tool you mappedStale scope matrix or tool name doesn't match the manifestWait ScopeMatrixTTL or restart. Confirm tool names in admin UI.
Metadata path returns 404BuildResourceMetadataPath derives the path from ResourceURI/mcp resource → /.well-known/oauth-protected-resource/mcp. Root resource → /.well-known/oauth-protected-resource.

Behaviour notes

  • Unknown tool — denied when any policy is in effect. Only PolicyModeOpen allows unknown tools.
  • Parse-fail-closed — malformed JSON-RPC bodies return 400 without touching your handler.
  • Batch JSON-RPC — each entry is authorized independently.
  • Streaming tools/list — filtered the same way as the HTTP response.
  • Policy refresh — stale-on-read with a single-flight guard; the first request after the TTL kicks off a fetch.

Reference