Authenticator via oauth2, direct, email and telegram
See the codeThis library provides "social login" with Github, Google, Facebook, Microsoft, Twitter, Yandex, Battle.net, Apple, Patreon, Discord and Telegram as well as custom auth providers and email verification.
dev provider allows local testing and developmentSecretReaderThe v2 version of this library is now the actively developed version, and we recommend using it for new projects. Key differences include:
github.com/golang-jwt/jwt/v5 (from v3)StandardClaims replaced with RegisteredClaimsClaims.ExpiresAt changed from int64 to *jwt.NumericDateClaims.NotBefore changed from int64 to *jwt.NumericDateClaims.IssuedAt changed from int64 to *jwt.NumericDateID field renamed from IdAudience changed from string to []stringGet(key string) (value token.Claims, ok bool)Set(key string, value token.Claims)To migrate from v1 to v2:
/v2 (e.g., github.com/go-pkgz/auth/v2)For a real-world example of migrating a complex project from v1 to v2, see Remark42's migration PR. This migration included:
/v2StandardClaims references to RegisteredClaims*jwt.NumericDateThe PR demonstrates how to handle migration for a production application with extensive auth usage.
go get -u github.com/go-pkgz/auth/v2
Example with routegroup (stdlib-based router):
func main() {
// define options
options := auth.Opts{
SecretReader: token.SecretFunc(func(id string) (string, error) { // secret key for JWT
return "secret", nil
}),
TokenDuration: time.Minute * 5, // token expires in 5 minutes
CookieDuration: time.Hour * 24, // cookie expires in 1 day and will enforce re-login
Issuer: "my-test-app",
URL: "http://127.0.0.1:8080",
AvatarStore: avatar.NewLocalFS("/tmp"),
Validator: token.ValidatorFunc(func(_ string, claims token.Claims) bool {
// allow only dev_* names
return claims.User != nil && strings.HasPrefix(claims.User.Name, "dev_")
}),
}
// create auth service with providers
service := auth.NewService(options)
service.AddProvider("github", "<Client ID>", "<Client Secret>") // add github provider
service.AddProvider("facebook", "<Client ID>", "<Client Secret>") // add facebook provider
// retrieve auth middleware
m := service.Middleware()
// setup http server
router := routegroup.New(http.NewServeMux())
router.HandleFunc("GET /open", openRouteHandler) // open api
router.Group().Route(func(r *routegroup.Bundle) {
r.Use(m.Auth)
r.HandleFunc("GET /private", protectedRouteHandler) // protected api
})
// setup auth routes
authRoutes, avaRoutes := service.Handlers()
router.Handle("/auth/", authRoutes) // add auth handlers
router.Handle("/avatar/", avaRoutes) // add avatar handler
log.Fatal(http.ListenAndServe(":8080", router))
}
func main() {
// define options
options := auth.Opts{
SecretReader: token.SecretFunc(func(id string) (string, error) { // secret key for JWT
return "secret", nil
}),
TokenDuration: time.Minute * 5, // token expires in 5 minutes
CookieDuration: time.Hour * 24, // cookie expires in 1 day and will enforce re-login
Issuer: "my-test-app",
URL: "http://127.0.0.1:8080",
AvatarStore: avatar.NewLocalFS("/tmp"),
Validator: token.ValidatorFunc(func(_ string, claims token.Claims) bool {
// allow only dev_* names
return claims.User != nil && strings.HasPrefix(claims.User.Name, "dev_")
}),
}
// create auth service with providers
service := auth.NewService(options)
service.AddProvider("github", "<Client ID>", "<Client Secret>") // add github provider
service.AddProvider("facebook", "<Client ID>", "<Client Secret>") // add facebook provider
// retrieve auth middleware
m := service.Middleware()
// setup http server
router := chi.NewRouter()
router.Get("/open", openRouteHandler) // open api
router.With(m.Auth).Get("/private", protectedRouteHandler) // protected api
// setup auth routes
authRoutes, avaRoutes := service.Handlers()
router.Mount("/auth", authRoutes) // add auth handlers
router.Mount("/avatar", avaRoutes) // add avatar handler
log.Fatal(http.ListenAndServe(":8080", router))
}
github.com/go-pkgz/auth/middleware provides ready-to-use middleware.
middleware.Auth - requires authenticated usermiddleware.Admin - requires authenticated admin usermiddleware.Trace - doesn't require authenticated user, but adds user info to requestmiddleware.RBAC - requires authenticated user with passed role(s)Also, there is a special middleware middleware.UpdateUser for population and modifying UserInfo in every request. See "Customization" for more details.
Generally, adding support of auth includes a few relatively simple steps:
auth.Opts structure with all parameters. Each of them documented and most of parameters are optional and have sane defaults.auth.Service with provided options.auth.ServiceFor the example above authentication handlers wired as /auth and provides:
/auth/<provider>/login?site=<site_id>&from=<redirect_url> - site_id used as aud claim for the token and can be processed by SecretReader to load/retrieve/define different secrets. redirect_url is the url to redirect after successful login. When Opts.AllowedRedirectHosts is set, only targets whose host matches Opts.URL or appears in the allowlist are honoured; others fall back to the user-info JSON response. With the default (nil) allowlist any non-empty from is honoured — see "Allowed redirect hosts" below for the opt-in details./avatar/<avatar_id> - returns the avatar (image). Links to those pictures added into user info automatically, for details see "Avatar proxy"/auth/<provider>/logout and /auth/logout - invalidate "session" by removing JWT cookie/auth/list - gives a json list of active providers/auth/user - returns token.User (json)/auth/status - returns status of logged in user (json)Middleware populates token.User to request's context. It can be loaded with token.GetUserInfo(r *http.Request) (user User, err error) or token.MustGetUserInfo(r *http.Request) User functions.
token.User object includes all fields retrieved from oauth2 provider:
Name - user nameID - hash of user idPicture - full link to proxied avatar (see "Avatar proxy")It also has placeholders for fields application can populate with custom token.ClaimsUpdater (see "Customization")
IP - hash of user's IP addressEmail - user's emailAttributes - map of string:any-value. To simplify management of this map some setters and getters provided, for example users.StrAttr, user.SetBoolAttr and so on. See user.go for more details.Direct links to avatars won't survive any real-life usage if they linked from a public page. For example, page like this may have hundreds of avatars and, most likely, will trigger throttling on provider's side. To eliminate such restriction auth library provides an automatic proxy
AvatarStoreAvatarStoreauth library. Each store has to implement avatar.Store interface.auth.Opts and needs:
AvatarStore - avatar store to use, i.e. avatar.NewLocalFS("/tmp/avatars") or more generic avatar.NewStore(uri)
file:///tmp/location or just /tmp/locationbolt://tmp/avatars.bdb"mongodb://127.0.0.1:27017/test?ava_db=db1&ava_coll=coll1AvatarRoutePath - route prefix for direct links to proxied avatar. For example /api/v1/avatars will make full links like this - http://example.com/api/v1/avatars/1234567890123.image. The url will be stored in user's token and retrieved by middleware (see "User Info")AvatarResizeLimit - size (in pixels) used to resize the avatar. Pls note - resize happens once as a part of Put call, i.e. on login. 0 size (default) disables resizing.In addition to oauth2 providers auth.Service allows to use direct user-defined authentication. This is done by adding direct provider with auth.AddDirectProvider.
service.AddDirectProvider("local", provider.CredCheckerFunc(func(user, password string) (ok bool, err error) {
ok, err = checkUserSomehow(user, password)
return ok, err
}))
Such provider acts like any other, i.e. will be registered as /auth/local/login.
The API for this provider supports both GET and POST requests:
POST /auth/<name>/login?session=[1|0]
body: application/x-www-form-urlencoded
user=<user>&passwd=<password>&aud=<site_id>
POST /auth/<name>/login?session=[1|0]
body: application/json
{
"user": "name",
"passwd": "xyz",
"aud": "bar",
}
GET /auth/<name>/login?user=<user>&passwd=<password>&aud=<site_id>&session=[1|0]
note: password parameter doesn't have to be naked/real password and can be any kind of password hash prepared by caller.
note: session=1 makes the auth cookie session-only, i.e. dropped when the browser closes. sess is accepted as an alias for backward compatibility.
Another non-oauth2 provider allowing user-confirmed authentication, for example by email or slack or telegram. This is
done by adding confirmed provider with auth.AddVerifProvider.
msgTemplate := "Confirmation email, token: {{.Token}}"
service.AddVerifProvider("email", msgTemplate, sender)
Message template may use the follow elements:
{{.Address}} - user address, for example email{{.User}} - user name{{.Token}} - confirmation token{{.Site}} - site IDSender should be provided by end-user and implements a single function interface
type Sender interface {
Send(address string, text string) error
}
For convenience a functional wrapper SenderFunc provided. Email sender provided in provider/sender package and can be
used as Sender.
The API for this provider:
GET /auth/<name>/login?user=<user>&address=<address>&aud=<site_id>&from=<url> - send confirmation request to user. site is accepted as an alias for aud. When host validation is enabled via Opts.AllowedRedirectHosts, the from URL is subject to the same allowlist check described in "Allowed redirect hosts"; otherwise any non-empty value is honoured.GET /auth/<name>/login?token=<conf.token>&session=[1|0] - authorize with confirmation token, sess is accepted as an alias for sessionThe provider acts like any other, i.e. will be registered as /auth/email/login.
Only a signed handshake token that carries this provider's name is accepted for login. Session tokens, confirmations issued by another verify provider, and handshake tokens an application mints without this provider's name are rejected, even though they are signed with the same secret.
Confirmation tokens are one-shot: a token redeemed once cannot be redeemed again within its TTL. This stops anyone with read access to the email link (forwarded mail, mail-gateway logs, mailbox archive) from independently consuming it after the user has.
By default the library installs an in-memory store on first call to AddVerifProvider. This is correct for single-instance deployments.
Multi-instance deployments behind a load balancer MUST supply a shared backend. With the default in-memory store, replay protection works only on the instance that originally consumed the token; an attacker who hits any other instance can still replay within the TTL. This is silent: the request succeeds and the auth flow completes normally, with no log indicating the protection was bypassed. Plug in Redis or any shared KV by setting Opts.VerifConfirmationStore to a value implementing provider.VerifConfirmationStore:
type VerifConfirmationStore interface {
// MarkUsed records key as consumed and returns alreadyUsed=true if it was
// already recorded. err signals a backend failure -- callers fail-closed
// (reject the redemption) on non-nil err to avoid replay during outages.
MarkUsed(key string, ttl time.Duration) (alreadyUsed bool, err error)
}
The store key is the SHA-256 of the raw confirmation token, so existing tokens issued before this protection landed are de-dup'd correctly without changing the wire format. Consumption is final: a transient downstream failure after the mark burns the token; the user must request a new confirmation email rather than retry the same link.
The verify provider returns a local user id of the form <provider>_<sha1(address)>. The confirmation round-trip proves current control of the address at the moment of login; it does not prove stable+unique identity over time. The owner of an address can change without the address changing:
[email protected] returns to the company when Alice leaves and may be reassigned to a new employee.This is inherent to email-as-identity, not specific to this library — the verify flow has no upstream identifier (such as an OIDC sub) it could fall back on. Any application that keys its own records directly on the returned id will treat the new owner of an address as the original user.
Integrators that need stable identity should:
users table primary key) at first successful verify, and key application records, ACLs, and audit logs on that internal id rather than on the value returned by the provider.If your threat model accepts that whoever currently controls an address gets the corresponding account (e.g. low-stakes commenting), no extra handling is required — but the property should be a deliberate choice, not an inherited default.
For email notify provider, please use github.com/go-pkgz/auth/provider/sender package:
sndr := sender.NewEmailClient(sender.EmailParams{
Host: "email.hostname",
Port: 567,
HELOHost: "mail.example.com", // optional, greeting hostname, defaults to localhost
SMTPUserName: "username",
SMTPPassword: "pass",
StartTLS: true,
InsecureSkipVerify: false,
From: "[email protected]",
Subject: "subject",
ContentType: "text/html",
Charset: "UTF-8",
}, log.Default())
authenticator.AddVerifProvider("email", "template goes here", sndr)
See that documentation for full options list.
Telegram provider allows your users to log in with Telegram account. First, you will need to create your bot. Contact @BotFather and follow his instructions to create your own bot (call it, for example, "My site auth bot")
Next initialize TelegramHandler with following parameters:
ProviderName - Any unique name to distinguish between providersSuccessMsg - Message sent to user on successfull authenticationErrorMsg - Message sent on errors (e.g. login request expired)Telegram - Telegram API implementation. Use provider.NewTelegramAPI with following arguments
token := os.Getenv("TELEGRAM_TOKEN")
telegram := provider.TelegramHandler{
ProviderName: "telegram",
ErrorMsg: "❌ Invalid auth request. Please try clicking link again.",
SuccessMsg: "✅ You have successfully authenticated!",
Telegram: provider.NewTelegramAPI(token, http.DefaultClient),
L: log.Default(),
TokenService: service.TokenService(),
AvatarSaver: service.AvatarProxy(),
}
After that run provider and register it's handlers:
// Run Telegram provider in the background
go func() {
err := telegram.Run(context.Background())
if err != nil {
log.Fatalf("[PANIC] failed to start telegram: %v", err)
}
}()
// Register Telegram provider
service.AddCustomHandler(&telegram)
Telegram's file API requires the bot token in the URL path
(https://api.telegram.org/file/bot<TOKEN>/...), so the URL is a bearer
credential and must never reach the JWT, the user JSON, or any debug log.
The provider fetches the avatar bytes server-side and stores them via
AvatarSaver, returning a clean local proxy URL.
This requires AvatarSaver to support direct content saving.
avatar.Proxy (the default returned by service.AvatarProxy()) does -- no
extra setup needed. Custom AvatarSaver implementations that don't
implement PutContent(userID string, content io.Reader) (string, error)
will see an empty User.Picture for Telegram logins, and the avatar proxy
will fall back to identicon. To preserve avatars with a custom saver,
implement that method alongside Put.
Behaviour change for custom
AvatarSaver: before this change, custom savers received the raw Telegram file URL (which included the bot token) viaPut. After this change they no longer do, because that URL is a bearer credential. Implementations that relied on receiving and refetching that URL must now implementPutContentto keep saving Telegram avatars, or accept that Telegram users get identicons.
Now all your users have to do is click one of the following links and press start
tg://resolve?domain=<botname>&start=<token> or https://t.me/<botname>/?start=<token>
Use the following routes to interact with provider:
/auth/<providerName>/login - Obtain auth token. Returns JSON object with bot (bot username) and token (token itself) fields.
/auth/<providerName>/login?token=<token> - Check if auth request has been confirmed (i.e. user pressed start). Sets session cookie and returns user info on success, errors with 404 otherwise.
/auth/<providerName>/logout - Invalidate user session.
This provider brings two extra functions:
Name - any name is allowed except the names from list of supported providers. It is possible to register more than one client for one given oauth2 provider (for example using different names bitbucket_dev and bitbucket_prod)Client - ID and secret of clientEndpoint - auth URL and token URL. This information could be obtained from auth2 provider pageInfoURL - oauth2 provider API method to read information of logged in user. This method could be found in documentation of oauth2 provider (e.g. for bitbucket https://developer.atlassian.com/bitbucket/api/2/reference/resource/user)MapUserFn - function to convert the response from InfoURL to token.User (s. example below)Scopes - minimal needed scope to read user information. Client should be authorized to these scopesc := auth.Client{
Cid: os.Getenv("AEXMPL_BITBUCKET_CID"),
Csecret: os.Getenv("AEXMPL_BITBUCKET_CSEC"),
}
service.AddCustomProvider("bitbucket", c, provider.CustomHandlerOpt{
Endpoint: oauth2.Endpoint{
AuthURL: "https://bitbucket.org/site/oauth2/authorize",
TokenURL: "https://bitbucket.org/site/oauth2/access_token",
},
InfoURL: "https://api.bitbucket.org/2.0/user/",
MapUserFn: func(data provider.UserData, _ []byte) token.User {
userInfo := token.User{
ID: "bitbucket_" + token.HashID(sha1.New(),
data.Value("username")),
Name: data.Value("nickname"),
}
return userInfo
},
Scopes: []string{"account"},
})
gopkg.in/oauth2.v3 library and example shows how to initialize the server and setup a provider.
URL - url of oauth2 server with portWithLoginPage - flag to define whether login page should be shownLoginPageHandler - function to handle login request. If not specified default login page will be shownsopts := provider.CustomServerOpt{
URL: "http://127.0.0.1:9096",
L: options.Logger,
WithLoginPage: true,
}
prov := provider.NewCustomServer(srv, sopts)
// Start server
go prov.Run(context.Background())
Name - any name except the names from list of supported providersClient - ID and secret of clientHandlerOpt - handler options of custom oauth providerservice.AddCustomProvider("custom123", auth.Client{Cid: "cid", Csecret: "csecret"}, prov.HandlerOpt)
Additionally it is possible to implement own auth handler. It may be useful if auth provider does not conform to oauth standard. Self-implemented handler has to implement provider.Provider interface.
// customHandler implements provider.Provider interface
c := customHandler{}
// add customHandler to stack of auth handlers
service.AddCustomHandler(c)
There are several ways to adjust functionality of the library:
SecretReader - interface with a single method Get(aud string) string to return the secret used for JWT signing and verificationClaimsUpdater - interface with Update(claims Claims) Claims method. This is the primary way to alter a token at login time and add any attributes, set ip, email, admin status, roles and so on.Validator - interface with Validate(token string, claims Claims) bool method. This is post-token hook and will be called on each request wrapped with Auth middleware. This will be the place for special logic to reject some tokens or users.UserUpdater - interface with Update(claims token.User) token.User method. This method will be called on each request wrapped with UpdateUser middleware. This will be the place for special logic modify User Info in request context. Example of usage.All of the interfaces above have corresponding Func adapters - SecretFunc, ClaimsUpdFunc, ValidatorFunc and UserUpdFunc.
Additionally, ErrorHandlerFunc type is available in the middleware package for custom error handling on authentication failures:
options := auth.Opts{
// ... other options ...
ErrorHandler: middleware.ErrorHandlerFunc(func(w http.ResponseWriter, r *http.Request, code int, err error) {
// return JSON error for API routes
if strings.HasPrefix(r.URL.Path, "/api/") {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
json.NewEncoder(w).Encode(map[string]string{"error": "authentication required"})
return
}
// redirect to login page for web routes
http.Redirect(w, r, "/login", http.StatusFound)
}),
}
By default (when ErrorHandler is nil), the middleware returns "Unauthorized" (401) or "Access denied" (403) text responses.
Restricting some users or some tokens is two step process:
ClaimsUpdater sets an attribute, like blocked (or allowed)Validator checks the attribute and returns true/falseThis technique used in the example code
The process can be simplified by doing all checks directly in Validator, but depends on particular case such solution
can be too expensive because Validator runs on each request as a part of auth middleware. In contrast, ClaimsUpdater called on token creation/refresh only.
For complex systems a single authenticator may serve multiple distinct subsystems or multiple set of independent users. For example some SaaS offerings may need to provide different authentications for different customers and prevent use of tokens/cookies made by another customer.
Such functionality can be implemented in 3 different ways:
auth.Service each one with different secret. Doing this way will ensure the highest level of isolation and cookies/tokens won't be even parsable across the instances. Practically such architecture can be too complicated and not always possible.
– Handling "allowed audience" as a part of ClaimsUpdater and Validator chain. I.e. ClaimsUpdater sets a claim indicating expected audience code/id and Validator making sure it matches. This way a single auth.Service could handle multiple groups of auth tokens and reject some based on the audience.aud claim. This method conceptually very similar to the previous one, but done by library internally and consumer don't need to define special ClaimsUpdater and Validator logic.In order to allow aud support the list of allowed audiences should be passed in as opts.Audiences parameter. Non-empty value will trigger internal checks for token generation (will reject token creation for alien aud) as well as Auth middleware.
The from query parameter accepted by oauth2/oauth1/apple/verify login flows tells the server where to send the user after a successful auth handshake. The value is signed into the handshake JWT, so it can't be tampered with mid-flow — but a caller can put any URL into the initial login link. Without restriction this becomes a phishing vector: an attacker crafts a login link that bounces the victim through legitimate OAuth and then to an attacker-controlled landing page.
Default behaviour is permissive (Opts.AllowedRedirectHosts is nil): any non-empty from value is honoured. This preserves the behaviour of versions before the redirect validator existed, so a dependency bump never silently breaks an existing deployment. Hardening is opt-in.
To enable host validation, set Opts.AllowedRedirectHosts to anything implementing the token.AllowedHosts interface:
type AllowedHosts interface {
Get() ([]string, error)
}
The token.AllowedHostsFunc adapter wraps an ordinary function, mirroring the token.AudienceFunc pattern:
opts := auth.Opts{
URL: "https://app.example.com",
AllowedRedirectHosts: token.AllowedHostsFunc(func() ([]string, error) {
return []string{"admin.example.com", "billing.example.com"}, nil
}),
// ...
}
Once the policy is on:
Opts.URL is always permitted implicitly (single-host deployments need no list entries — pass func() ([]string, error) { return nil, nil } to enable the policy with same-host-only semantics).Get() are appended to that. Subdomain wildcards are not supported — list each host explicitly.https://app.example.com and https://app.example.com:443 are treated as the same host.[WARN] level with only the host portion of the URL (paths and query strings are stripped to avoid logging attacker-supplied data).Working with oauth2 providers can be a pain, especially during development phase. A special, development-only provider dev can make it less painful. This one can be registered directly, i.e. service.AddProvider("dev", "", "") or service.AddDevProvider(host, port) and should be activated like this:
// runs dev oauth2 server on :8084 by default
go func() {
devAuthServer, err := service.DevAuth()
if err != nil {
log.Fatal(err)
}
devAuthServer.Run()
}()
It will run fake aouth2 "server" on port :8084 and user could login with any user name. See example for more details.
Warning: this is not the real oauth2 server but just a small fake thing for development and testing only. Don't use dev provider with any production code.
The dev OAuth server and the embedded CustomServer helper bind to 127.0.0.1 by default so the dev login UI is not silently exposed to the LAN, Docker network siblings, kubernetes pods on the same node, etc.
The dev provider has a separate Host field for the listen address; the OAuth callback URL is taken from Opts.URL and is independent. To bind to all interfaces (e.g. when the auth process runs inside a container and needs to be reachable from the host) pass Host explicitly:
// dev provider -- listen on all interfaces, callback URL stays whatever you set in Opts.URL
service.AddDevProvider("0.0.0.0", 8084)
CustomServer is the awkward case: CustomServerOpt.URL is used both as the bind source and as the advertised OAuth endpoint (it appears verbatim in AuthURL, TokenURL and InfoURL returned to clients). There is no separate bind setting today, so URL: "http://0.0.0.0:9096" is only appropriate when every consumer of those advertised endpoints is fine with 0.0.0.0 as a hostname -- which is essentially never the case in practice.
Recommended approaches:
URL: "http://localhost:9096" (or your real hostname). The default loopback bind matches the advertised host and everything works.URL to the public-facing value the proxy serves and bind the auth process to a network the proxy can reach (e.g. a docker network, or 0.0.0.0 if the proxy is on a different host and the network is otherwise restricted). The advertised URL stays correct because that is what Opts.URL says.URL to a real hostname or LAN IP that resolves on every consumer (e.g. http://oauth.lan.example:9096); the bind extracts the host and listens there. Avoid embedding 0.0.0.0 in URL unless you have already accepted the consequence that clients will see 0.0.0.0 in the advertised metadata.A future change is likely to add a separate bind option (CustomServerOpt.BindAddr or similar) so the bind and the advertised URL can diverge cleanly; until then, the bind follows URL.
By default, Dev provider doesn't return email claim from /user endpoint, to match behaviour of other providers which only request minimal scopes.
However sometimes it is useful to have email included into user info. This can be done by configuring devAuthServer.GetEmailFn function:
go func() {
devAuthServer, err := service.DevAuth()
devOauth2Srv.GetEmailFn = func(username string) string {
return username + "@example.com"
}
if err != nil {
log.Fatal(err)
}
devAuthServer.Run()
}()
In addition to the primary method (i.e. JWT cookie with XSRF header) there are two more ways to authenticate:
X-JWT. This shouldn't be used for web application, however can be helpful for service-to-service authentication./something?token=<jwt>In some cases the middleware.Authenticator allow use Basic access authentication, which transmits credentials as user-id/password pairs, encoded using Base64 (RFC7235).
When basic authentication used, client doesn't get auth token in response. It's auth type expect credentials in a header Authorization at every client request. It can be helpful, if client side not support cookie/token store (e.g. embedded device or custom apps).
This mode disabled by default and will be enabled with options.
The auth package has two options of basic authentication:
Opts.AdminPasswd defined. This will allow access with basic auth admin:<Opts.AdminPasswd> with user admin. Such method can be used for automation scripts.Opts.BasicAuthChecker defined. When BasicAuthChecker defined then Opts.AdminPasswd option will be ignore.options := auth.Opts{
//...
AdminPasswd: "admin_secret_password", // will ignore if BasicAuthChecker defined
BasicAuthChecker: func(user, passwd string) (bool, token.User, error) {
if user == "basic_user" && passwd == "123456" {
return true, token.User{Name: user, Role: "test_r"}, nil
}
return false, token.User{}, errors.New("basic auth credentials check failed")
}
//...
}
By default, this library doesn't print anything to stdout/stderr, however user can pass a logger implementing logger.L interface with a single method Logf(format string, args ...interface{}). Functional adapter for this interface included as logger.Func. There are two predefined implementations in the logger package - NoOp (prints nothing, default) and Std wrapping log.Printf from stdlib.
Authentication handled by external providers. You should setup oauth2 for all (or some) of them to allow users to authenticate. It is not mandatory to have all of them, but at least one should be correctly configured.
https://mysite.com/web/privacy.html of your Remark42 installation, e.g. https://remark42.mysite.com/web/privacy.html (please check that it works)mysite.comhttps://remark42.mysite.com/auth/google/callback, e.g., https://remark42.mysite.com/auth/google/callbackinstructions for google oauth2 setup borrowed from oauth2_proxy
/auth/microsoft/callback. i.e. https://example.mysite.com/auth/microsoft/callbackBy default, the Microsoft provider uses the common tenant endpoint, which requires an application accepting both work or school accounts and personal Microsoft accounts. That corresponds to signInAudience: AzureADandPersonalMicrosoftAccount, which can be checked and changed under "Manifest" for an application that already exists. An application restricted to a single account type is rejected at that endpoint, for instance with AADSTS50194 when it is not registered as multi-tenant.
For single-tenant Entra ID (Azure AD) applications, or for an application limited to personal Microsoft accounts (consumers), use AddMicrosoftProvider to specify the tenant:
service.AddMicrosoftProvider("<Client ID>", "<Client Secret>", "<Tenant ID>")
The tenant value can be a tenant ID (GUID), domain name, or one of the well-known values: common, organizations, consumers.
/auth/github/callback. ie https://example.mysite.com/auth/github/callbackBy default the GitHub provider derives the local user id from the account login, as github_<sha1(login)>. GitHub releases a username as soon as the account is renamed or deleted, and anyone can claim it afterwards. An application that keys its own records on the returned id will treat the next holder of a released username as the original user. This is the same property described in the email-as-identity caveat above, with one difference: GitHub returns an immutable numeric account id in the same response, so the provider can key on that instead.
To do so, register the provider with AddGithubProviderWithNumericID:
service.AddGithubProviderWithNumericID("<Client ID>", "<Client Secret>")
To combine it with UserAttributes, set GithubNumericID when building provider.Params directly and register the result with service.AddCustomHandler(provider.NewGithub(p)). That handler is registered as-is, so every field the service would normally supply has to be filled in by hand: URL, JwtService: service.TokenService(), AvatarSaver: service.AvatarProxy(), L, and AllowedRedirectHosts set to the same value passed in Opts. Leaving AllowedRedirectHosts nil turns off redirect host validation for that provider, so a service that sets it in Opts must repeat it here.
The numeric derivation is best-effort: if the /user response carries no usable numeric id, the provider keeps the login-derived id and logs a warning rather than failing the login.
This is off by default because it changes the id of every existing GitHub user. On a running deployment the switch is not transparent: comments, roles, blocks and any other records stored under the old id stop matching, and the old ids cannot be translated offline, because a login cannot be recovered from its hash. Turn it on for a new deployment, or migrate the stored ids yourself first. The recommendation in the email-as-identity caveat applies here too, and avoids the problem entirely: map the provider id to an internal immutable id of your own at first login, and key application records on that.
https://example.mysite.com/auth/facebook/callbackTo configure this provider, a user requires an Apple developer account (without it setting up a sign in with Apple is impossible). Sign in with Apple lets users log in to your app using their two-factor authentication Apple ID.
Follow to next steps for configuring on the Apple side:
https://example.com/ to "Return URLs".After completing the previous steps, you can proceed with configuring the Apple auth provider. Here are the parameters for AppleConfig:
form_post // apple config parameters
appleCfg := provider.AppleConfig{
TeamID: os.Getenv("AEXMPL_APPLE_TID"), // developer account identifier
ClientID: os.Getenv("AEXMPL_APPLE_CID"), // Service ID (or App ID)
KeyID: os.Getenv("AEXMPL_APPLE_KEYID"), // private key identifier
}
Then add an Apple provider that accepts the following parameters:
appleConfig (provider.AppleConfig) created aboveprivateKeyLoader (PrivateKeyLoaderInterface)PrivateKeyLoaderInterface implements a loader for the private key (which you downloaded above) to create a client_secret. The user can use a pre-defined function provider.LoadApplePrivateKeyFromFile(filePath string) to load the private key from local file.
AddAppleProvide tries to load private key at call and return an error if load failed. Always check error when calling this provider.
if err := service.AddAppleProvider(appleCfg, provider.LoadApplePrivateKeyFromFile("PATH_TO_PRIVATE_KEY_FILE")); err != nil {
log.Fatalf("[ERROR] failed create to AppleProvider: %v", err)
}
Limitation:
Token validation:
The provider verifies the Apple id_token signature against Apple's published JWK, then enforces:
iss == "https://appleid.apple.com" -- per Apple's Sign-in-with-Apple REST API specaud == ClientID (your Service ID or App ID)A token signed by Apple but issued for a different aud is rejected with 403 invalid id_token. The aud check (not the iss check) is what closes the confused-deputy attack: a sibling service in the same Apple developer team (or any other Sign-in-with-Apple client) holds tokens with the real Apple iss but their own aud, and could replay one of those tokens against this service if aud were not enforced.
Provider always get user UID (sub claim) in IDToken.
See example before use.
/auth/yandex/callback. ie https://example.mysite.com/auth/yandex/callbackFor more details refer to Yandex OAuth and Yandex.Passport API documentation.
For more details refer to Complete Guide of Battle.net OAuth API and Login Button or the official Battle.net OAuth2 guide
/auth/patreon/callback. ie https://example.mysite.com/auth/patreon/callback/auth/discord/callback. ie https://remark42.mysite.com/auth/discord/callbackcid and csecretBy default, the XSRF protections will apply to all requests which reach the middlewares.Auth,
middlewares.Admin or middlewares.RBAC middlewares. This will require setting a request header
with a key of <XSRFHeaderKey> containing the value of the cookie named <XSRFCookieName>.
To disable all XSRF protections, set DisableXSRF to true. This should probably only be used
during testing or debugging.
When setting a custom request header is not possible, such as when building a web application which
is not a Single-Page-Application and HTML link tags are used to navigate pages, specific HTTP methods
may be excluded using the XSRFIgnoreMethods option. For example, to disable GET requests, set this
option to XSRFIgnoreMethods: []string{"GET"}. Adding methods other than GET to this list may result
in XSRF vulnerabilities.
http.CrossOriginProtection (Go 1.25+)The JWT XSRF check above only fires when the JWT arrives in a cookie and only after a request
reaches the auth middleware. For browser-based applications it is recommended to additionally
wrap the router with http.CrossOriginProtection
(Go 1.25+), which checks the browser-set Sec-Fetch-Site header (with an Origin vs Host
fallback for older clients) and rejects cross-origin state-changing requests at the HTTP layer
before they reach any handler:
csrf := http.NewCrossOriginProtection()
_ = csrf.AddTrustedOrigin("https://app.example.com") // for cross-origin SPAs
csrf.AddInsecureBypassPattern("POST /auth/apple/") // Apple Sign In uses form_post
mux := http.NewServeMux()
mux.Handle("/api/", authenticator.Auth(apiHandler))
log.Fatal(http.ListenAndServe(":8080", csrf.Handler(mux)))
This is the algorithm recommended by OWASP
as a primary CSRF defence. Unlike SameSite=Lax, it distinguishes same-origin from
same-site, so subdomain attacks are blocked. It composes with the JWT XSRF: the new
middleware covers browser flows, the JWT XSRF still covers API clients sending the
JWT-derived header.
The library was originally extracted from the remark42 project. The code is in production use on multiple sites and has proven to be stable and reliable.
Version 1.x is stable and in maintenance mode. Version 2.x is now the actively developed branch, and we recommend using it for all new projects.
Overall
Maintained
Code review
Go module
Latest release
Releases this year
238 followers · starred May 2023
120 followers · starred May 2024
72 followers · starred Oct 2023
54 followers · starred Mar 2026
Authenticator via oauth2, direct, email and telegram
See the codeThis library provides "social login" with Github, Google, Facebook, Microsoft, Twitter, Yandex, Battle.net, Apple, Patreon, Discord and Telegram as well as custom auth providers and email verification.
dev provider allows local testing and developmentSecretReaderThe v2 version of this library is now the actively developed version, and we recommend using it for new projects. Key differences include:
github.com/golang-jwt/jwt/v5 (from v3)StandardClaims replaced with RegisteredClaimsClaims.ExpiresAt changed from int64 to *jwt.NumericDateClaims.NotBefore changed from int64 to *jwt.NumericDateClaims.IssuedAt changed from int64 to *jwt.NumericDateID field renamed from IdAudience changed from string to []stringGet(key string) (value token.Claims, ok bool)Set(key string, value token.Claims)To migrate from v1 to v2:
/v2 (e.g., github.com/go-pkgz/auth/v2)For a real-world example of migrating a complex project from v1 to v2, see Remark42's migration PR. This migration included:
/v2StandardClaims references to RegisteredClaims*jwt.NumericDateThe PR demonstrates how to handle migration for a production application with extensive auth usage.
go get -u github.com/go-pkgz/auth/v2
Example with routegroup (stdlib-based router):
func main() {
// define options
options := auth.Opts{
SecretReader: token.SecretFunc(func(id string) (string, error) { // secret key for JWT
return "secret", nil
}),
TokenDuration: time.Minute * 5, // token expires in 5 minutes
CookieDuration: time.Hour * 24, // cookie expires in 1 day and will enforce re-login
Issuer: "my-test-app",
URL: "http://127.0.0.1:8080",
AvatarStore: avatar.NewLocalFS("/tmp"),
Validator: token.ValidatorFunc(func(_ string, claims token.Claims) bool {
// allow only dev_* names
return claims.User != nil && strings.HasPrefix(claims.User.Name, "dev_")
}),
}
// create auth service with providers
service := auth.NewService(options)
service.AddProvider("github", "<Client ID>", "<Client Secret>") // add github provider
service.AddProvider("facebook", "<Client ID>", "<Client Secret>") // add facebook provider
// retrieve auth middleware
m := service.Middleware()
// setup http server
router := routegroup.New(http.NewServeMux())
router.HandleFunc("GET /open", openRouteHandler) // open api
router.Group().Route(func(r *routegroup.Bundle) {
r.Use(m.Auth)
r.HandleFunc("GET /private", protectedRouteHandler) // protected api
})
// setup auth routes
authRoutes, avaRoutes := service.Handlers()
router.Handle("/auth/", authRoutes) // add auth handlers
router.Handle("/avatar/", avaRoutes) // add avatar handler
log.Fatal(http.ListenAndServe(":8080", router))
}
func main() {
// define options
options := auth.Opts{
SecretReader: token.SecretFunc(func(id string) (string, error) { // secret key for JWT
return "secret", nil
}),
TokenDuration: time.Minute * 5, // token expires in 5 minutes
CookieDuration: time.Hour * 24, // cookie expires in 1 day and will enforce re-login
Issuer: "my-test-app",
URL: "http://127.0.0.1:8080",
AvatarStore: avatar.NewLocalFS("/tmp"),
Validator: token.ValidatorFunc(func(_ string, claims token.Claims) bool {
// allow only dev_* names
return claims.User != nil && strings.HasPrefix(claims.User.Name, "dev_")
}),
}
// create auth service with providers
service := auth.NewService(options)
service.AddProvider("github", "<Client ID>", "<Client Secret>") // add github provider
service.AddProvider("facebook", "<Client ID>", "<Client Secret>") // add facebook provider
// retrieve auth middleware
m := service.Middleware()
// setup http server
router := chi.NewRouter()
router.Get("/open", openRouteHandler) // open api
router.With(m.Auth).Get("/private", protectedRouteHandler) // protected api
// setup auth routes
authRoutes, avaRoutes := service.Handlers()
router.Mount("/auth", authRoutes) // add auth handlers
router.Mount("/avatar", avaRoutes) // add avatar handler
log.Fatal(http.ListenAndServe(":8080", router))
}
github.com/go-pkgz/auth/middleware provides ready-to-use middleware.
middleware.Auth - requires authenticated usermiddleware.Admin - requires authenticated admin usermiddleware.Trace - doesn't require authenticated user, but adds user info to requestmiddleware.RBAC - requires authenticated user with passed role(s)Also, there is a special middleware middleware.UpdateUser for population and modifying UserInfo in every request. See "Customization" for more details.
Generally, adding support of auth includes a few relatively simple steps:
auth.Opts structure with all parameters. Each of them documented and most of parameters are optional and have sane defaults.auth.Service with provided options.auth.ServiceFor the example above authentication handlers wired as /auth and provides:
/auth/<provider>/login?site=<site_id>&from=<redirect_url> - site_id used as aud claim for the token and can be processed by SecretReader to load/retrieve/define different secrets. redirect_url is the url to redirect after successful login. When Opts.AllowedRedirectHosts is set, only targets whose host matches Opts.URL or appears in the allowlist are honoured; others fall back to the user-info JSON response. With the default (nil) allowlist any non-empty from is honoured — see "Allowed redirect hosts" below for the opt-in details./avatar/<avatar_id> - returns the avatar (image). Links to those pictures added into user info automatically, for details see "Avatar proxy"/auth/<provider>/logout and /auth/logout - invalidate "session" by removing JWT cookie/auth/list - gives a json list of active providers/auth/user - returns token.User (json)/auth/status - returns status of logged in user (json)Middleware populates token.User to request's context. It can be loaded with token.GetUserInfo(r *http.Request) (user User, err error) or token.MustGetUserInfo(r *http.Request) User functions.
token.User object includes all fields retrieved from oauth2 provider:
Name - user nameID - hash of user idPicture - full link to proxied avatar (see "Avatar proxy")It also has placeholders for fields application can populate with custom token.ClaimsUpdater (see "Customization")
IP - hash of user's IP addressEmail - user's emailAttributes - map of string:any-value. To simplify management of this map some setters and getters provided, for example users.StrAttr, user.SetBoolAttr and so on. See user.go for more details.Direct links to avatars won't survive any real-life usage if they linked from a public page. For example, page like this may have hundreds of avatars and, most likely, will trigger throttling on provider's side. To eliminate such restriction auth library provides an automatic proxy
AvatarStoreAvatarStoreauth library. Each store has to implement avatar.Store interface.auth.Opts and needs:
AvatarStore - avatar store to use, i.e. avatar.NewLocalFS("/tmp/avatars") or more generic avatar.NewStore(uri)
file:///tmp/location or just /tmp/locationbolt://tmp/avatars.bdb"mongodb://127.0.0.1:27017/test?ava_db=db1&ava_coll=coll1AvatarRoutePath - route prefix for direct links to proxied avatar. For example /api/v1/avatars will make full links like this - http://example.com/api/v1/avatars/1234567890123.image. The url will be stored in user's token and retrieved by middleware (see "User Info")AvatarResizeLimit - size (in pixels) used to resize the avatar. Pls note - resize happens once as a part of Put call, i.e. on login. 0 size (default) disables resizing.In addition to oauth2 providers auth.Service allows to use direct user-defined authentication. This is done by adding direct provider with auth.AddDirectProvider.
service.AddDirectProvider("local", provider.CredCheckerFunc(func(user, password string) (ok bool, err error) {
ok, err = checkUserSomehow(user, password)
return ok, err
}))
Such provider acts like any other, i.e. will be registered as /auth/local/login.
The API for this provider supports both GET and POST requests:
POST /auth/<name>/login?session=[1|0]
body: application/x-www-form-urlencoded
user=<user>&passwd=<password>&aud=<site_id>
POST /auth/<name>/login?session=[1|0]
body: application/json
{
"user": "name",
"passwd": "xyz",
"aud": "bar",
}
GET /auth/<name>/login?user=<user>&passwd=<password>&aud=<site_id>&session=[1|0]
note: password parameter doesn't have to be naked/real password and can be any kind of password hash prepared by caller.
note: session=1 makes the auth cookie session-only, i.e. dropped when the browser closes. sess is accepted as an alias for backward compatibility.
Another non-oauth2 provider allowing user-confirmed authentication, for example by email or slack or telegram. This is
done by adding confirmed provider with auth.AddVerifProvider.
msgTemplate := "Confirmation email, token: {{.Token}}"
service.AddVerifProvider("email", msgTemplate, sender)
Message template may use the follow elements:
{{.Address}} - user address, for example email{{.User}} - user name{{.Token}} - confirmation token{{.Site}} - site IDSender should be provided by end-user and implements a single function interface
type Sender interface {
Send(address string, text string) error
}
For convenience a functional wrapper SenderFunc provided. Email sender provided in provider/sender package and can be
used as Sender.
The API for this provider:
GET /auth/<name>/login?user=<user>&address=<address>&aud=<site_id>&from=<url> - send confirmation request to user. site is accepted as an alias for aud. When host validation is enabled via Opts.AllowedRedirectHosts, the from URL is subject to the same allowlist check described in "Allowed redirect hosts"; otherwise any non-empty value is honoured.GET /auth/<name>/login?token=<conf.token>&session=[1|0] - authorize with confirmation token, sess is accepted as an alias for sessionThe provider acts like any other, i.e. will be registered as /auth/email/login.
Only a signed handshake token that carries this provider's name is accepted for login. Session tokens, confirmations issued by another verify provider, and handshake tokens an application mints without this provider's name are rejected, even though they are signed with the same secret.
Confirmation tokens are one-shot: a token redeemed once cannot be redeemed again within its TTL. This stops anyone with read access to the email link (forwarded mail, mail-gateway logs, mailbox archive) from independently consuming it after the user has.
By default the library installs an in-memory store on first call to AddVerifProvider. This is correct for single-instance deployments.
Multi-instance deployments behind a load balancer MUST supply a shared backend. With the default in-memory store, replay protection works only on the instance that originally consumed the token; an attacker who hits any other instance can still replay within the TTL. This is silent: the request succeeds and the auth flow completes normally, with no log indicating the protection was bypassed. Plug in Redis or any shared KV by setting Opts.VerifConfirmationStore to a value implementing provider.VerifConfirmationStore:
type VerifConfirmationStore interface {
// MarkUsed records key as consumed and returns alreadyUsed=true if it was
// already recorded. err signals a backend failure -- callers fail-closed
// (reject the redemption) on non-nil err to avoid replay during outages.
MarkUsed(key string, ttl time.Duration) (alreadyUsed bool, err error)
}
The store key is the SHA-256 of the raw confirmation token, so existing tokens issued before this protection landed are de-dup'd correctly without changing the wire format. Consumption is final: a transient downstream failure after the mark burns the token; the user must request a new confirmation email rather than retry the same link.
The verify provider returns a local user id of the form <provider>_<sha1(address)>. The confirmation round-trip proves current control of the address at the moment of login; it does not prove stable+unique identity over time. The owner of an address can change without the address changing:
[email protected] returns to the company when Alice leaves and may be reassigned to a new employee.This is inherent to email-as-identity, not specific to this library — the verify flow has no upstream identifier (such as an OIDC sub) it could fall back on. Any application that keys its own records directly on the returned id will treat the new owner of an address as the original user.
Integrators that need stable identity should:
users table primary key) at first successful verify, and key application records, ACLs, and audit logs on that internal id rather than on the value returned by the provider.If your threat model accepts that whoever currently controls an address gets the corresponding account (e.g. low-stakes commenting), no extra handling is required — but the property should be a deliberate choice, not an inherited default.
For email notify provider, please use github.com/go-pkgz/auth/provider/sender package:
sndr := sender.NewEmailClient(sender.EmailParams{
Host: "email.hostname",
Port: 567,
HELOHost: "mail.example.com", // optional, greeting hostname, defaults to localhost
SMTPUserName: "username",
SMTPPassword: "pass",
StartTLS: true,
InsecureSkipVerify: false,
From: "[email protected]",
Subject: "subject",
ContentType: "text/html",
Charset: "UTF-8",
}, log.Default())
authenticator.AddVerifProvider("email", "template goes here", sndr)
See that documentation for full options list.
Telegram provider allows your users to log in with Telegram account. First, you will need to create your bot. Contact @BotFather and follow his instructions to create your own bot (call it, for example, "My site auth bot")
Next initialize TelegramHandler with following parameters:
ProviderName - Any unique name to distinguish between providersSuccessMsg - Message sent to user on successfull authenticationErrorMsg - Message sent on errors (e.g. login request expired)Telegram - Telegram API implementation. Use provider.NewTelegramAPI with following arguments
token := os.Getenv("TELEGRAM_TOKEN")
telegram := provider.TelegramHandler{
ProviderName: "telegram",
ErrorMsg: "❌ Invalid auth request. Please try clicking link again.",
SuccessMsg: "✅ You have successfully authenticated!",
Telegram: provider.NewTelegramAPI(token, http.DefaultClient),
L: log.Default(),
TokenService: service.TokenService(),
AvatarSaver: service.AvatarProxy(),
}
After that run provider and register it's handlers:
// Run Telegram provider in the background
go func() {
err := telegram.Run(context.Background())
if err != nil {
log.Fatalf("[PANIC] failed to start telegram: %v", err)
}
}()
// Register Telegram provider
service.AddCustomHandler(&telegram)
Telegram's file API requires the bot token in the URL path
(https://api.telegram.org/file/bot<TOKEN>/...), so the URL is a bearer
credential and must never reach the JWT, the user JSON, or any debug log.
The provider fetches the avatar bytes server-side and stores them via
AvatarSaver, returning a clean local proxy URL.
This requires AvatarSaver to support direct content saving.
avatar.Proxy (the default returned by service.AvatarProxy()) does -- no
extra setup needed. Custom AvatarSaver implementations that don't
implement PutContent(userID string, content io.Reader) (string, error)
will see an empty User.Picture for Telegram logins, and the avatar proxy
will fall back to identicon. To preserve avatars with a custom saver,
implement that method alongside Put.
Behaviour change for custom
AvatarSaver: before this change, custom savers received the raw Telegram file URL (which included the bot token) viaPut. After this change they no longer do, because that URL is a bearer credential. Implementations that relied on receiving and refetching that URL must now implementPutContentto keep saving Telegram avatars, or accept that Telegram users get identicons.
Now all your users have to do is click one of the following links and press start
tg://resolve?domain=<botname>&start=<token> or https://t.me/<botname>/?start=<token>
Use the following routes to interact with provider:
/auth/<providerName>/login - Obtain auth token. Returns JSON object with bot (bot username) and token (token itself) fields.
/auth/<providerName>/login?token=<token> - Check if auth request has been confirmed (i.e. user pressed start). Sets session cookie and returns user info on success, errors with 404 otherwise.
/auth/<providerName>/logout - Invalidate user session.
This provider brings two extra functions:
Name - any name is allowed except the names from list of supported providers. It is possible to register more than one client for one given oauth2 provider (for example using different names bitbucket_dev and bitbucket_prod)Client - ID and secret of clientEndpoint - auth URL and token URL. This information could be obtained from auth2 provider pageInfoURL - oauth2 provider API method to read information of logged in user. This method could be found in documentation of oauth2 provider (e.g. for bitbucket https://developer.atlassian.com/bitbucket/api/2/reference/resource/user)MapUserFn - function to convert the response from InfoURL to token.User (s. example below)Scopes - minimal needed scope to read user information. Client should be authorized to these scopesc := auth.Client{
Cid: os.Getenv("AEXMPL_BITBUCKET_CID"),
Csecret: os.Getenv("AEXMPL_BITBUCKET_CSEC"),
}
service.AddCustomProvider("bitbucket", c, provider.CustomHandlerOpt{
Endpoint: oauth2.Endpoint{
AuthURL: "https://bitbucket.org/site/oauth2/authorize",
TokenURL: "https://bitbucket.org/site/oauth2/access_token",
},
InfoURL: "https://api.bitbucket.org/2.0/user/",
MapUserFn: func(data provider.UserData, _ []byte) token.User {
userInfo := token.User{
ID: "bitbucket_" + token.HashID(sha1.New(),
data.Value("username")),
Name: data.Value("nickname"),
}
return userInfo
},
Scopes: []string{"account"},
})
gopkg.in/oauth2.v3 library and example shows how to initialize the server and setup a provider.
URL - url of oauth2 server with portWithLoginPage - flag to define whether login page should be shownLoginPageHandler - function to handle login request. If not specified default login page will be shownsopts := provider.CustomServerOpt{
URL: "http://127.0.0.1:9096",
L: options.Logger,
WithLoginPage: true,
}
prov := provider.NewCustomServer(srv, sopts)
// Start server
go prov.Run(context.Background())
Name - any name except the names from list of supported providersClient - ID and secret of clientHandlerOpt - handler options of custom oauth providerservice.AddCustomProvider("custom123", auth.Client{Cid: "cid", Csecret: "csecret"}, prov.HandlerOpt)
Additionally it is possible to implement own auth handler. It may be useful if auth provider does not conform to oauth standard. Self-implemented handler has to implement provider.Provider interface.
// customHandler implements provider.Provider interface
c := customHandler{}
// add customHandler to stack of auth handlers
service.AddCustomHandler(c)
There are several ways to adjust functionality of the library:
SecretReader - interface with a single method Get(aud string) string to return the secret used for JWT signing and verificationClaimsUpdater - interface with Update(claims Claims) Claims method. This is the primary way to alter a token at login time and add any attributes, set ip, email, admin status, roles and so on.Validator - interface with Validate(token string, claims Claims) bool method. This is post-token hook and will be called on each request wrapped with Auth middleware. This will be the place for special logic to reject some tokens or users.UserUpdater - interface with Update(claims token.User) token.User method. This method will be called on each request wrapped with UpdateUser middleware. This will be the place for special logic modify User Info in request context. Example of usage.All of the interfaces above have corresponding Func adapters - SecretFunc, ClaimsUpdFunc, ValidatorFunc and UserUpdFunc.
Additionally, ErrorHandlerFunc type is available in the middleware package for custom error handling on authentication failures:
options := auth.Opts{
// ... other options ...
ErrorHandler: middleware.ErrorHandlerFunc(func(w http.ResponseWriter, r *http.Request, code int, err error) {
// return JSON error for API routes
if strings.HasPrefix(r.URL.Path, "/api/") {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
json.NewEncoder(w).Encode(map[string]string{"error": "authentication required"})
return
}
// redirect to login page for web routes
http.Redirect(w, r, "/login", http.StatusFound)
}),
}
By default (when ErrorHandler is nil), the middleware returns "Unauthorized" (401) or "Access denied" (403) text responses.
Restricting some users or some tokens is two step process:
ClaimsUpdater sets an attribute, like blocked (or allowed)Validator checks the attribute and returns true/falseThis technique used in the example code
The process can be simplified by doing all checks directly in Validator, but depends on particular case such solution
can be too expensive because Validator runs on each request as a part of auth middleware. In contrast, ClaimsUpdater called on token creation/refresh only.
For complex systems a single authenticator may serve multiple distinct subsystems or multiple set of independent users. For example some SaaS offerings may need to provide different authentications for different customers and prevent use of tokens/cookies made by another customer.
Such functionality can be implemented in 3 different ways:
auth.Service each one with different secret. Doing this way will ensure the highest level of isolation and cookies/tokens won't be even parsable across the instances. Practically such architecture can be too complicated and not always possible.
– Handling "allowed audience" as a part of ClaimsUpdater and Validator chain. I.e. ClaimsUpdater sets a claim indicating expected audience code/id and Validator making sure it matches. This way a single auth.Service could handle multiple groups of auth tokens and reject some based on the audience.aud claim. This method conceptually very similar to the previous one, but done by library internally and consumer don't need to define special ClaimsUpdater and Validator logic.In order to allow aud support the list of allowed audiences should be passed in as opts.Audiences parameter. Non-empty value will trigger internal checks for token generation (will reject token creation for alien aud) as well as Auth middleware.
The from query parameter accepted by oauth2/oauth1/apple/verify login flows tells the server where to send the user after a successful auth handshake. The value is signed into the handshake JWT, so it can't be tampered with mid-flow — but a caller can put any URL into the initial login link. Without restriction this becomes a phishing vector: an attacker crafts a login link that bounces the victim through legitimate OAuth and then to an attacker-controlled landing page.
Default behaviour is permissive (Opts.AllowedRedirectHosts is nil): any non-empty from value is honoured. This preserves the behaviour of versions before the redirect validator existed, so a dependency bump never silently breaks an existing deployment. Hardening is opt-in.
To enable host validation, set Opts.AllowedRedirectHosts to anything implementing the token.AllowedHosts interface:
type AllowedHosts interface {
Get() ([]string, error)
}
The token.AllowedHostsFunc adapter wraps an ordinary function, mirroring the token.AudienceFunc pattern:
opts := auth.Opts{
URL: "https://app.example.com",
AllowedRedirectHosts: token.AllowedHostsFunc(func() ([]string, error) {
return []string{"admin.example.com", "billing.example.com"}, nil
}),
// ...
}
Once the policy is on:
Opts.URL is always permitted implicitly (single-host deployments need no list entries — pass func() ([]string, error) { return nil, nil } to enable the policy with same-host-only semantics).Get() are appended to that. Subdomain wildcards are not supported — list each host explicitly.https://app.example.com and https://app.example.com:443 are treated as the same host.[WARN] level with only the host portion of the URL (paths and query strings are stripped to avoid logging attacker-supplied data).Working with oauth2 providers can be a pain, especially during development phase. A special, development-only provider dev can make it less painful. This one can be registered directly, i.e. service.AddProvider("dev", "", "") or service.AddDevProvider(host, port) and should be activated like this:
// runs dev oauth2 server on :8084 by default
go func() {
devAuthServer, err := service.DevAuth()
if err != nil {
log.Fatal(err)
}
devAuthServer.Run()
}()
It will run fake aouth2 "server" on port :8084 and user could login with any user name. See example for more details.
Warning: this is not the real oauth2 server but just a small fake thing for development and testing only. Don't use dev provider with any production code.
The dev OAuth server and the embedded CustomServer helper bind to 127.0.0.1 by default so the dev login UI is not silently exposed to the LAN, Docker network siblings, kubernetes pods on the same node, etc.
The dev provider has a separate Host field for the listen address; the OAuth callback URL is taken from Opts.URL and is independent. To bind to all interfaces (e.g. when the auth process runs inside a container and needs to be reachable from the host) pass Host explicitly:
// dev provider -- listen on all interfaces, callback URL stays whatever you set in Opts.URL
service.AddDevProvider("0.0.0.0", 8084)
CustomServer is the awkward case: CustomServerOpt.URL is used both as the bind source and as the advertised OAuth endpoint (it appears verbatim in AuthURL, TokenURL and InfoURL returned to clients). There is no separate bind setting today, so URL: "http://0.0.0.0:9096" is only appropriate when every consumer of those advertised endpoints is fine with 0.0.0.0 as a hostname -- which is essentially never the case in practice.
Recommended approaches:
URL: "http://localhost:9096" (or your real hostname). The default loopback bind matches the advertised host and everything works.URL to the public-facing value the proxy serves and bind the auth process to a network the proxy can reach (e.g. a docker network, or 0.0.0.0 if the proxy is on a different host and the network is otherwise restricted). The advertised URL stays correct because that is what Opts.URL says.URL to a real hostname or LAN IP that resolves on every consumer (e.g. http://oauth.lan.example:9096); the bind extracts the host and listens there. Avoid embedding 0.0.0.0 in URL unless you have already accepted the consequence that clients will see 0.0.0.0 in the advertised metadata.A future change is likely to add a separate bind option (CustomServerOpt.BindAddr or similar) so the bind and the advertised URL can diverge cleanly; until then, the bind follows URL.
By default, Dev provider doesn't return email claim from /user endpoint, to match behaviour of other providers which only request minimal scopes.
However sometimes it is useful to have email included into user info. This can be done by configuring devAuthServer.GetEmailFn function:
go func() {
devAuthServer, err := service.DevAuth()
devOauth2Srv.GetEmailFn = func(username string) string {
return username + "@example.com"
}
if err != nil {
log.Fatal(err)
}
devAuthServer.Run()
}()
In addition to the primary method (i.e. JWT cookie with XSRF header) there are two more ways to authenticate:
X-JWT. This shouldn't be used for web application, however can be helpful for service-to-service authentication./something?token=<jwt>In some cases the middleware.Authenticator allow use Basic access authentication, which transmits credentials as user-id/password pairs, encoded using Base64 (RFC7235).
When basic authentication used, client doesn't get auth token in response. It's auth type expect credentials in a header Authorization at every client request. It can be helpful, if client side not support cookie/token store (e.g. embedded device or custom apps).
This mode disabled by default and will be enabled with options.
The auth package has two options of basic authentication:
Opts.AdminPasswd defined. This will allow access with basic auth admin:<Opts.AdminPasswd> with user admin. Such method can be used for automation scripts.Opts.BasicAuthChecker defined. When BasicAuthChecker defined then Opts.AdminPasswd option will be ignore.options := auth.Opts{
//...
AdminPasswd: "admin_secret_password", // will ignore if BasicAuthChecker defined
BasicAuthChecker: func(user, passwd string) (bool, token.User, error) {
if user == "basic_user" && passwd == "123456" {
return true, token.User{Name: user, Role: "test_r"}, nil
}
return false, token.User{}, errors.New("basic auth credentials check failed")
}
//...
}
By default, this library doesn't print anything to stdout/stderr, however user can pass a logger implementing logger.L interface with a single method Logf(format string, args ...interface{}). Functional adapter for this interface included as logger.Func. There are two predefined implementations in the logger package - NoOp (prints nothing, default) and Std wrapping log.Printf from stdlib.
Authentication handled by external providers. You should setup oauth2 for all (or some) of them to allow users to authenticate. It is not mandatory to have all of them, but at least one should be correctly configured.
https://mysite.com/web/privacy.html of your Remark42 installation, e.g. https://remark42.mysite.com/web/privacy.html (please check that it works)mysite.comhttps://remark42.mysite.com/auth/google/callback, e.g., https://remark42.mysite.com/auth/google/callbackinstructions for google oauth2 setup borrowed from oauth2_proxy
/auth/microsoft/callback. i.e. https://example.mysite.com/auth/microsoft/callbackBy default, the Microsoft provider uses the common tenant endpoint, which requires an application accepting both work or school accounts and personal Microsoft accounts. That corresponds to signInAudience: AzureADandPersonalMicrosoftAccount, which can be checked and changed under "Manifest" for an application that already exists. An application restricted to a single account type is rejected at that endpoint, for instance with AADSTS50194 when it is not registered as multi-tenant.
For single-tenant Entra ID (Azure AD) applications, or for an application limited to personal Microsoft accounts (consumers), use AddMicrosoftProvider to specify the tenant:
service.AddMicrosoftProvider("<Client ID>", "<Client Secret>", "<Tenant ID>")
The tenant value can be a tenant ID (GUID), domain name, or one of the well-known values: common, organizations, consumers.
/auth/github/callback. ie https://example.mysite.com/auth/github/callbackBy default the GitHub provider derives the local user id from the account login, as github_<sha1(login)>. GitHub releases a username as soon as the account is renamed or deleted, and anyone can claim it afterwards. An application that keys its own records on the returned id will treat the next holder of a released username as the original user. This is the same property described in the email-as-identity caveat above, with one difference: GitHub returns an immutable numeric account id in the same response, so the provider can key on that instead.
To do so, register the provider with AddGithubProviderWithNumericID:
service.AddGithubProviderWithNumericID("<Client ID>", "<Client Secret>")
To combine it with UserAttributes, set GithubNumericID when building provider.Params directly and register the result with service.AddCustomHandler(provider.NewGithub(p)). That handler is registered as-is, so every field the service would normally supply has to be filled in by hand: URL, JwtService: service.TokenService(), AvatarSaver: service.AvatarProxy(), L, and AllowedRedirectHosts set to the same value passed in Opts. Leaving AllowedRedirectHosts nil turns off redirect host validation for that provider, so a service that sets it in Opts must repeat it here.
The numeric derivation is best-effort: if the /user response carries no usable numeric id, the provider keeps the login-derived id and logs a warning rather than failing the login.
This is off by default because it changes the id of every existing GitHub user. On a running deployment the switch is not transparent: comments, roles, blocks and any other records stored under the old id stop matching, and the old ids cannot be translated offline, because a login cannot be recovered from its hash. Turn it on for a new deployment, or migrate the stored ids yourself first. The recommendation in the email-as-identity caveat applies here too, and avoids the problem entirely: map the provider id to an internal immutable id of your own at first login, and key application records on that.
https://example.mysite.com/auth/facebook/callbackTo configure this provider, a user requires an Apple developer account (without it setting up a sign in with Apple is impossible). Sign in with Apple lets users log in to your app using their two-factor authentication Apple ID.
Follow to next steps for configuring on the Apple side:
https://example.com/ to "Return URLs".After completing the previous steps, you can proceed with configuring the Apple auth provider. Here are the parameters for AppleConfig:
form_post // apple config parameters
appleCfg := provider.AppleConfig{
TeamID: os.Getenv("AEXMPL_APPLE_TID"), // developer account identifier
ClientID: os.Getenv("AEXMPL_APPLE_CID"), // Service ID (or App ID)
KeyID: os.Getenv("AEXMPL_APPLE_KEYID"), // private key identifier
}
Then add an Apple provider that accepts the following parameters:
appleConfig (provider.AppleConfig) created aboveprivateKeyLoader (PrivateKeyLoaderInterface)PrivateKeyLoaderInterface implements a loader for the private key (which you downloaded above) to create a client_secret. The user can use a pre-defined function provider.LoadApplePrivateKeyFromFile(filePath string) to load the private key from local file.
AddAppleProvide tries to load private key at call and return an error if load failed. Always check error when calling this provider.
if err := service.AddAppleProvider(appleCfg, provider.LoadApplePrivateKeyFromFile("PATH_TO_PRIVATE_KEY_FILE")); err != nil {
log.Fatalf("[ERROR] failed create to AppleProvider: %v", err)
}
Limitation:
Token validation:
The provider verifies the Apple id_token signature against Apple's published JWK, then enforces:
iss == "https://appleid.apple.com" -- per Apple's Sign-in-with-Apple REST API specaud == ClientID (your Service ID or App ID)A token signed by Apple but issued for a different aud is rejected with 403 invalid id_token. The aud check (not the iss check) is what closes the confused-deputy attack: a sibling service in the same Apple developer team (or any other Sign-in-with-Apple client) holds tokens with the real Apple iss but their own aud, and could replay one of those tokens against this service if aud were not enforced.
Provider always get user UID (sub claim) in IDToken.
See example before use.
/auth/yandex/callback. ie https://example.mysite.com/auth/yandex/callbackFor more details refer to Yandex OAuth and Yandex.Passport API documentation.
For more details refer to Complete Guide of Battle.net OAuth API and Login Button or the official Battle.net OAuth2 guide
/auth/patreon/callback. ie https://example.mysite.com/auth/patreon/callback/auth/discord/callback. ie https://remark42.mysite.com/auth/discord/callbackcid and csecretBy default, the XSRF protections will apply to all requests which reach the middlewares.Auth,
middlewares.Admin or middlewares.RBAC middlewares. This will require setting a request header
with a key of <XSRFHeaderKey> containing the value of the cookie named <XSRFCookieName>.
To disable all XSRF protections, set DisableXSRF to true. This should probably only be used
during testing or debugging.
When setting a custom request header is not possible, such as when building a web application which
is not a Single-Page-Application and HTML link tags are used to navigate pages, specific HTTP methods
may be excluded using the XSRFIgnoreMethods option. For example, to disable GET requests, set this
option to XSRFIgnoreMethods: []string{"GET"}. Adding methods other than GET to this list may result
in XSRF vulnerabilities.
http.CrossOriginProtection (Go 1.25+)The JWT XSRF check above only fires when the JWT arrives in a cookie and only after a request
reaches the auth middleware. For browser-based applications it is recommended to additionally
wrap the router with http.CrossOriginProtection
(Go 1.25+), which checks the browser-set Sec-Fetch-Site header (with an Origin vs Host
fallback for older clients) and rejects cross-origin state-changing requests at the HTTP layer
before they reach any handler:
csrf := http.NewCrossOriginProtection()
_ = csrf.AddTrustedOrigin("https://app.example.com") // for cross-origin SPAs
csrf.AddInsecureBypassPattern("POST /auth/apple/") // Apple Sign In uses form_post
mux := http.NewServeMux()
mux.Handle("/api/", authenticator.Auth(apiHandler))
log.Fatal(http.ListenAndServe(":8080", csrf.Handler(mux)))
This is the algorithm recommended by OWASP
as a primary CSRF defence. Unlike SameSite=Lax, it distinguishes same-origin from
same-site, so subdomain attacks are blocked. It composes with the JWT XSRF: the new
middleware covers browser flows, the JWT XSRF still covers API clients sending the
JWT-derived header.
The library was originally extracted from the remark42 project. The code is in production use on multiple sites and has proven to be stable and reliable.
Version 1.x is stable and in maintenance mode. Version 2.x is now the actively developed branch, and we recommend using it for all new projects.
Overall
Maintained
Code review
Go module
Latest release
Releases this year
238 followers · starred May 2023
120 followers · starred May 2024
72 followers · starred Oct 2023
54 followers · starred Mar 2026