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 andtools/callchecked 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 UUIDIntrospectionClientSecret— 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:
| Field | Source | Notes |
|---|---|---|
Issuer | AuthSec base URL | e.g. https://api.authsec.dev |
ResourceServerID | Application detail panel, field id | UUID |
IntrospectionClientID | Same value as ResourceServerID | Username for Basic auth |
IntrospectionClientSecret | One-time secret from registration | Store in a secret manager |
ResourceURI | Application detail panel, field resource_url | Must match the aud claim of issued tokens |
PublishManifest | true recommended | Pushes tool inventory at startup |
Optional but useful:
| Field | Purpose |
|---|---|
JWKSURL | Override JWKS endpoint. Defaults to <Issuer>/oauth/jwks. |
IntrospectionURL | Override. Defaults to <Issuer>/oauth/introspect. |
AuthorizationServer | Defaults to Issuer. The SDK uses it for the sdk-policy / sdk-manifest URLs. |
PolicyMode | PolicyModeRemoteRequired (default), PolicyModeRemoteWithLocalFallback, PolicyModeLocalOnly, PolicyModeOpen. |
ValidationMode | ValidationModeJWTAndIntrospect (default), ValidationModeJWTOnly, ValidationModeIntrospectionOnly, ValidationModeJWTOrIntrospect. |
ToolScopes | Local fallback policy. Required if PolicyMode = PolicyModeRemoteWithLocalFallback. |
ToolScopeSuggestions | Per-tool scope hints sent in the manifest payload. |
SupportedScopes | The OAuth scopes this application advertises. |
ScopeMatrixTTL | How long fetched policy is cached. Default 5 min. |
Logger | Inject 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
| Symptom | Likely cause | Fix |
|---|---|---|
MountMCP returns "invalid config" | A required field is empty or ResourceURI is missing a scheme | Check the table above. cfg.Validate() returns specific field names. |
Startup fails with policy fetch failed | PolicyModeRemoteRequired + the application isn't activated | Finish step 5, or use PolicyModeRemoteWithLocalFallback during onboarding. |
401 invalid_token with audience mismatch | Token's aud doesn't match cfg.ResourceURI | OAuth client must request resource=<your ResourceURI>. |
403 insufficient_scope on a tool you mapped | Stale scope matrix or tool name doesn't match the manifest | Wait ScopeMatrixTTL or restart. Confirm tool names in admin UI. |
| Metadata path returns 404 | BuildResourceMetadataPath 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
PolicyModeOpenallows 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
- Module:
github.com/authsec-ai/sdk-authsec/packages/go-sdk - Source on GitHub:
sdk-authsec/packages/go-sdk - Quickstart binary:
examples/quickstart/main.go - Firstrun harness (CI-tested against a live backend):
firstrun/ - Full operator README:
README.md - API docs on pkg.go.dev:
github.com/authsec-ai/sdk-authsec/packages/go-sdk
Related
- From zero to launched — the surrounding setup
- Register an application — where the Config values come from
- Map tools to scopes — populating the scope matrix the SDK fetches
- JWT vs token introspection — picking a
ValidationMode - Troubleshoot OAuth tokens — when 401 or 403 doesn't match expectations