---
title: "theauth-go: Go auth library with MCP authorization"
description: "theauth-go is an MIT Go library for human sign-in, OAuth 2.1 and MCP authorization with DPoP, CIBA, PAR and JAR. Install with go get."
canonical: https://theauth.dev/go/
lastmod: 2026-10-31
---

Go library

# theauth-go: auth and an MCP authorization server for Go

Mount it into your own net/http or chi server. Humans sign in with passkeys, magic links or OAuth. Agents and MCP clients get scoped OAuth 2.1 tokens from the same process. MIT licensed, no hosted service.

`go get github.com/glincker/theauth-go/v2`

Go 1.25 or newer. Keep the /v2: the bare path is frozen at v1.0.0.

[Go quick start](https://docs.theauth.dev/go/getting-started/quick-start) [Build an MCP authorization server](https://theauth.dev/guides/mcp-authorization-server-go/)[pkg.go.dev](https://pkg.go.dev/github.com/glincker/theauth-go/v2)[GitHub](https://github.com/glincker/theauth-go)

## A sign-in server in about 20 lines.

Memory storage, magic links and one protected route on the standard library mux. Built and run against v2.13.0 on 2026-10-31: GET /me returns 401 until you sign in. Sign-in links print in the server log. Add EmailSender for real mail, and swap memory for SQLite or Postgres to keep data.

main.go

```go
package main

import (
	"log"
	"net/http"

	"github.com/glincker/theauth-go/v2"
	"github.com/glincker/theauth-go/v2/storage/memory"
)

func main() {
	a, err := theauth.New(theauth.Config{Storage: memory.New(), BaseURL: "http://localhost:8080"})
	if err != nil {
		log.Fatal(err)
	}
	defer a.Close()

	mux := http.NewServeMux()
	mux.Handle("/auth/", a.Handler())
	mux.Handle("GET /me", a.Authn()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if u, ok := theauth.UserFromContext(r.Context()); ok {
			_, _ = w.Write([]byte("hello, " + u.Email))
			return
		}
		http.Error(w, "sign in first", http.StatusUnauthorized)
	})))
	log.Fatal(http.ListenAndServe(":8080", mux))
}
```

Run go mod init example.com/app && go mod tidy && go run .. Next: the [Go examples](https://theauth.dev/examples/#go) (chi, Gin, Echo, SQLite, passkeys, device login) or the [full quick start](https://docs.theauth.dev/go/getting-started/quick-start).

## Where the project stands.

Latest release v2.13.0 (2026-10-31), MIT, Go 1.25 or newer. The first commit was on 2026-06-20, so the project is months old, not years. It is maintained by GLINCKER, and one person makes nearly all commits (GitHub contributors API, read 2026-10-31). Test coverage on [Codecov](https://app.codecov.io/gh/glincker/theauth-go) was 45.7% on the same date, so read the experimental labels on the [stability page](https://theauth.dev/open/stability/) before relying on a package.

If you need a conformance-certified OpenID Provider or a long track record, see [when not to use theAuth](https://theauth.dev/when-not-to-use/).

Release data from the GitHub API, read 2026-10-31.

What it offers

## One library for the human side and the agent side.

Agent-grade OAuth

## DPoP, CIBA, PAR and JAR each sit behind their own config field.

The authorization server implements the specs that matter when the client is an autonomous agent rather than a browser tab. Each one is opt-in through its own config field.

Standards implemented by the theauth-go authorization server

| Standard | What it gives an agent deployment |
| --- | --- |
| RFC 9449 DPoP | Access tokens bound to a client key, so a leaked token is useless without the private key. |
| RFC 9509 CIBA | Backchannel authentication: an agent asks, a human approves out of band. Needs memory or Postgres storage. |
| RFC 9126 PAR | Pushed authorization requests, so request parameters never travel through the browser. |
| RFC 9101 JAR | Signed authorization request objects. |
| RFC 8693 token exchange | Delegation and impersonation with a capped actor chain (`MaxActorChainDepth`). |
| RFC 9728 and RFC 8414 | Protected resource and authorization server metadata, so MCP clients discover everything from the resource URL. |
| CIMD (MCP 2025-11-25) | Client ID Metadata Documents. Off by default and fail-closed: you choose which hosts may be fetched. |
| RFC 7591 dynamic registration | Closed unless you issue registration tokens or explicitly allow anonymous registration. |

Source: the [theauth-go llms.txt](https://github.com/glincker/theauth-go/blob/main/llms.txt) and [README](https://github.com/glincker/theauth-go/blob/main/README.md). TypeScript parity differs: the TypeScript library covers PKCE S256, RFC 9728, 8707, 8414 and 7591. See the [MCP OAuth page](https://theauth.dev/mcp-oauth/).

MCP quickstart

## An authorization server and a protected MCP route in one file.

This is the program from the [MCP authorization server guide](https://theauth.dev/guides/mcp-authorization-server-go/). It was built and run against v2.13.0 and mcpresource v0.2.0 on 2026-10-31: the metadata endpoint returns 200 and /mcp returns 401 without a token. Sign-in emails print to the terminal.

main.go

```go
package main

import (
	"context"
	"crypto/rand"
	"encoding/json"
	"fmt"
	"log"
	"net/http"

	"github.com/glincker/theauth-go/mcpresource"
	"github.com/glincker/theauth-go/v2"
	"github.com/glincker/theauth-go/v2/storage/memory"
	"github.com/go-chi/chi/v5"
)

const (
	baseURL  = "http://localhost:8080"
	resource = baseURL + "/mcp"
)

// logSender prints each email instead of sending it, so you can copy the
// magic link out of the terminal.
type logSender struct{}

func (logSender) Send(_ context.Context, to, subject, body string) error {
	fmt.Printf("\n--- email to %s: %s ---\n%s\n\n", to, subject, body)
	return nil
}

func main() {
	key := make([]byte, 32)
	if _, err := rand.Read(key); err != nil {
		log.Fatal(err)
	}

	a, err := theauth.New(theauth.Config{
		Storage:       memory.New(), // swap for storage/sqlite or Postgres to keep data
		BaseURL:       baseURL,
		EmailSender:   logSender{},
		EncryptionKey: key,
		AuthorizationServer: &theauth.AuthorizationServerConfig{
			Issuer: baseURL,
			Resources: []theauth.ProtectedResource{
				{Identifier: resource, Scopes: []string{"tools.read"}},
			},
			// Dynamic client registration is closed unless you open it. This token is
			// the "initial access token" an MCP client presents to register itself.
			RegistrationTokens: []string{"dev-registration-token"},
		},
	})
	if err != nil {
		log.Fatal(err)
	}
	defer a.Close()

	r := chi.NewRouter()
	a.Mount(r) // /auth/*, /oauth/*, /.well-known/*

	// The MCP resource server: validates JWTs issued by the server above.
	v := mcpresource.New(resource, mcpresource.WithJWKS(baseURL+"/oauth/jwks"))
	r.Route("/mcp", func(r chi.Router) {
		r.Use(v.Middleware)
		r.Get("/", func(w http.ResponseWriter, req *http.Request) {
			p, _ := v.Principal(req.Context())
			_ = json.NewEncoder(w).Encode(map[string]any{"subject": p.Subject, "scope": p.Scope})
		})
	})

	log.Println("listening on", baseURL)
	log.Fatal(http.ListenAndServe(":8080", r))
}
```

Run go mod init example.com/mcp-as && go mod tidy && go run ., then open /.well-known/oauth-authorization-server. For a human-only server with SQLite, start from [examples/single-binary-sqlite](https://github.com/glincker/theauth-go/tree/main/examples/single-binary-sqlite).

Storage

## Pick the database by the features you need.

New fails at startup if you enable a feature your storage backend cannot hold, so you find out before deploy.

Feature support by storage backend

| Capability | memory | sqlite | postgres | mysql |
| --- | --- | --- | --- | --- |
| Users, sessions, magic links, passwords | Yes | Yes | Yes | Yes |
| OAuth accounts, passkeys, TOTP, audit log | Yes | Yes | Yes | Yes |
| Scoped API tokens, device grant | Yes | Yes | Yes | Yes |
| Organizations, SAML, SCIM, RBAC | Yes | No | Yes | Yes |
| OAuth 2.1 server, agent identity | Yes | No | Yes | Yes |
| CIBA backchannel auth | Yes | No | Yes | No |

From the theauth-go README. Postgres and MySQL support for the newer token, device and session capabilities is recent and passes the new storagetest suites; the README says to treat it as newly added.

In the authorization server

## Attacks it already blocks

Behavior of the merged Go authorization server, shown as before and after. Nothing here is a certification or a pen test result.

SSRF

### Internal fetch stopped

Client JWKS and metadata URLs come from outside parties. The Go server fetches them with an SSRF-guarded client that refuses loopback, private and link-local (cloud metadata) addresses, follows no redirects and caps the body at 512 KB.

Refresh reuse

### Old token revokes the family

Refresh tokens rotate on every use. Presenting one that was already rotated is treated as theft, and the whole rotation family is revoked (RFC 9700, section 4.14).

Code replay

### Second use undoes the first

Authorization codes are stored hashed and live 60 seconds by default. Redeeming one a second time revokes the tokens already issued from it and writes an oauth.code_replay_revoked audit event.

Supply chain and speed

## Signed releases, and hot paths with numbers you can rerun.

Release artifacts ship with an SBOM and Sigstore signatures. The verification steps are in the [releases and verification](https://docs.theauth.dev/go/security/releases) doc. We do not claim a third-party audit or certification.

Twelve Go benchmarks gate every release against regressions. Argon2id runs at the production work factor, so password hashing is slow on purpose. Everything else on the hot path is measured in nanoseconds to microseconds, in memory storage. Full table, machine and commit are on the [benchmarks page](https://theauth.dev/benchmarks/).

Apple M4 Max, Go go1.26.5, snapshot 2026-10-07. Per operation, median of five runs.

## theauth-go: common questions

**Which import path do I use?**

Use `github.com/glincker/theauth-go/v2`. The path without `/v2` is frozen at v1.0.0, so a plain `go get github.com/glincker/theauth-go` installs the old release. The `mcpresource` and `storage/sqlite` modules are separate modules without the `/v2` suffix.

**Is it a hosted service?**

No. It is a library you mount into your own `net/http` or `chi` server, under the MIT license. Data stays in your database: memory, SQLite, Postgres or MySQL.

**Which Go versions does it need?**

The root module declares Go 1.25. The separate `storage/sqlite` module needs Go 1.26.

**Does it work without Postgres?**

Yes. SQLite gives you one static binary with one database file. SQLite has no organizations, SAML, SCIM, RBAC or OAuth authorization server, so MCP authorization needs memory, Postgres or MySQL.

**How stable is the API?**

Packages that were stable before v2.6 keep their SemVer guarantees. The agent identity APIs, `policy`, `clientauth` and `storage/sqlite` are marked experimental and may change in a minor release. The stability page lists the exact split.

[Source on GitHub](https://github.com/glincker/theauth-go) [Releases](https://github.com/glincker/theauth-go/releases) [Go examples](https://theauth.dev/examples/#go) [TypeScript library](https://theauth.dev/typescript/) [DPoP and device flow](https://theauth.dev/agent-oauth/) [Go documentation](https://docs.theauth.dev/go) [pkg.go.dev reference](https://pkg.go.dev/github.com/glincker/theauth-go/v2) [Get started in TypeScript or Go](https://theauth.dev/get-started/) [MCP OAuth 2.1](https://theauth.dev/mcp-oauth/) [Changelog](https://theauth.dev/changelog/)
