This 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 developmentSecretReader
go get -u github.com/go-pkgz/auth
Example with chi 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.Service
For 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./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
AvatarStore
AvatarStore
auth
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/location
bolt://tmp/avatars.bdb
"mongodb://127.0.0.1:27017/test?ava_db=db1&ava_coll=coll1
AvatarRoutePath
- 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.
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 userGET /auth/<name>/login?token=<conf.token>&sess=[1|0]
- authorize with confirmation tokenThe provider acts like any other, i.e. will be registered as /auth/email/login
.
For email notify provider, please use github.com/go-pkgz/auth/provider/sender
package:
sndr := sender.NewEmailClient(sender.EmailParams{
Host: "email.hostname",
Port: 567,
SMTPUserName: "username",
SMTPPassword: "pass",
StartTLS: true,
InsecureSkipVerify: false,
From: "notify@email.hostname",
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)
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:
Adds ability to use any third-party oauth2 providers in addition to the list of directly supported. Included example demonstrates how to do it for bitbucket. In order to add a new oauth2 provider following input is required:
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 scopes
c := 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"}, })
Adds local oauth2 server user can fully customize. It uses gopkg.in/oauth2.v3
library and example shows how to initialize the server and setup a provider.
to start local oauth2 server following options are required:
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 shown
sopts := 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 provider
service.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
.
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/false_This 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.
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(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.
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.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.com
https://remark42.mysite.com
/auth/google/callback
, e.g., https://remark42.mysite.com/auth/google/callback
_instructions for google oauth2 setup borrowed from oauth2_proxy_
/auth/microsoft/callback
. i.e. https://example.mysite.com/auth/microsoft/callback
/auth/github/callback
. ie https://example.mysite.com/auth/github/callback
https://example.mysite.com
/auth/facebook/callback
To 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:
Map a userName (if specific scope defined) is only sent in the upon initial user sign up.
Subsequent logins to your app using Sign In with Apple with the same account do not share any user info and will only return a user identifier in IDToken claims.
This behaves correctly until a user delete sign in for you service with Apple ID in own Apple account profile (security section).
It is recommend that you securely cache the at first login containing the user info for bind it with a user UID at next login.
Provider always get user UID
(sub
claim) in IDToken
.
Apple doesn't have an API for fetch avatar and user info.
See example before use.
/auth/yandex/callback
. ie https://example.mysite.com/auth/yandex/callback
For 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/callback
cid
and csecret
By 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.
The library extracted from remark42 project. The original code in production use on multiple sites and seems to work fine.
go-pkgz/auth
library still in development and until version 1 released some breaking changes possible.