go-pkgz / auth

Authenticator via oauth2, direct, email and telegram
https://go-pkgz.umputun.dev/auth/
MIT License
1.07k stars 84 forks source link
authentication custom-oauth2 go golang jwt library login middleware oauth2 oauth2-client

auth - authentication via oauth2, direct and email

Build Status Coverage Status godoc

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.

Install

go get -u github.com/go-pkgz/auth

Usage

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))
}

Middleware

github.com/go-pkgz/auth/middleware provides ready-to-use middleware.

Also, there is a special middleware middleware.UpdateUser for population and modifying UserInfo in every request. See "Customization" for more details.

Details

Generally, adding support of auth includes a few relatively simple steps:

  1. Setup auth.Opts structure with all parameters. Each of them documented and most of parameters are optional and have sane defaults.
  2. Create the new auth.Service with provided options.
  3. Add all desirable authentication providers.
  4. Retrieve middleware and http handlers from auth.Service
  5. Wire auth and avatar handlers into http router as sub–routes.

API

For the example above authentication handlers wired as /auth and provides:

User info

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:

It also has placeholders for fields application can populate with custom token.ClaimsUpdater (see "Customization")

Avatar proxy

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

Direct authentication

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:

note: password parameter doesn't have to be naked/real password and can be any kind of password hash prepared by caller.

Verified authentication

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:

Sender 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:

The provider acts like any other, i.e. will be registered as /auth/email/login.

Email

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

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:

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:

  1. /auth/<providerName>/login - Obtain auth token. Returns JSON object with bot (bot username) and token (token itself) fields.

  2. /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.

  3. /auth/<providerName>/logout - Invalidate user session.

Custom oauth2

This provider brings two extra functions:

  1. 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 client
    • Endpoint - auth URL and token URL. This information could be obtained from auth2 provider page
    • InfoURL - 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"}, })

  2. 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 port
      • WithLoginPage - flag to define whether login page should be shown
      • LoginPageHandler - 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())

    • to register handler for local oauth2 following option are required:
      • Name - any name except the names from list of supported providers
      • Client - ID and secret of client
      • HandlerOpt - handler options of custom oauth provider
        service.AddCustomProvider("custom123", auth.Client{Cid: "cid", Csecret: "csecret"}, prov.HandlerOpt)

Self-implemented auth handler

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)

Customization

There are several ways to adjust functionality of the library:

  1. SecretReader - interface with a single method Get(aud string) string to return the secret used for JWT signing and verification
  2. ClaimsUpdater - 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.
  3. 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.
  4. 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.

Implementing black list logic or some other filters

Restricting some users or some tokens is two step process:

_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.

Multi-tenant services and support for different audiences

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:

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.

Dev provider

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()
    }()

Other ways to authenticate

In addition to the primary method (i.e. JWT cookie with XSRF header) there are two more ways to authenticate:

  1. Send JWT header as X-JWT. This shouldn't be used for web application, however can be helpful for service-to-service authentication.
  2. Send JWT token as query parameter, i.e. /something?token=<jwt>
  3. Basic access authentication, for more details see below Basic authentication.

Basic authentication

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:

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.

Register oauth2 providers

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.

Google Auth Provider

  1. Create a new project: https://console.developers.google.com/project
  2. Choose the new project from the top right project dropdown (only if another project is selected)
  3. In the project Dashboard center pane, choose "API Manager"
  4. In the left Nav pane, choose "Credentials"
  5. In the center pane, choose the "OAuth consent screen" tab.
    • Select "External" and click "Create"
    • Fill in "App name" and select User support email
    • Upload a logo, if you want to
    • In the App Domain section:
    • Application home page - your site URL, e.g., https://mysite.com
    • Application privacy policy link - /web/privacy.html of your Remark42 installation, e.g. https://remark42.mysite.com/web/privacy.html (please check that it works)
    • Terms of service - leave empty
    • Authorized domains - your site domain, e.g., mysite.com
    • Developer contact information - add your email, and then click Save and continue
    • On the Scopes tab, just click Save and continue
    • On the Test users, add your email, then click Save and continue
    • Before going to the next step, set the app to "Production" and send it to verification
  6. In the center pane, choose the "Credentials" tab
    • Open the "Create credentials" drop-down
    • Choose "OAuth client ID"
    • Choose "Web application"
    • Application Name is freeform; choose something appropriate, like "Comments on mysite.com"
    • Authorized JavaScript Origins should be your domain, e.g., https://remark42.mysite.com
    • Authorized redirect URIs is the location of OAuth2/callback constructed as domain + /auth/google/callback, e.g., https://remark42.mysite.com/auth/google/callback
    • Click "Create"
  7. Take note of the Client ID and Client Secret

_instructions for google oauth2 setup borrowed from oauth2_proxy_

Microsoft Auth Provider

  1. Register a new application using the Azure portal.
  2. Under "Authentication/Platform configurations/Web" enter the correct url constructed as domain + /auth/microsoft/callback. i.e. https://example.mysite.com/auth/microsoft/callback
  3. In "Overview" take note of the Application (client) ID
  4. Choose the new project from the top right project dropdown (only if another project is selected)
  5. Select "Certificates & secrets" and click on "+ New Client Secret".

GitHub Auth Provider

  1. Create a new "OAuth App": https://github.com/settings/developers
  2. Fill "Application Name" and "Homepage URL" for your site
  3. Under "Authorization callback URL" enter the correct url constructed as domain + /auth/github/callback. ie https://example.mysite.com/auth/github/callback
  4. Take note of the Client ID and Client Secret

Facebook Auth Provider

  1. From https://developers.facebook.com select "My Apps" / "Add a new App"
  2. Set "Display Name" and "Contact email"
  3. Choose "Facebook Login" and then "Web"
  4. Set "Site URL" to your domain, ex: https://example.mysite.com
  5. Under "Facebook login" / "Settings" fill "Valid OAuth redirect URIs" with your callback url constructed as domain + /auth/facebook/callback
  6. Select "App Review" and turn public flag on. This step may ask you to provide a link to your privacy policy.

Apple Auth Provider

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:

  1. Log in to the developer account.
  2. If you don't have an App ID yet, create one. Later on, you'll need TeamID, which is an "App ID Prefix" value.
  3. Enable the "Sign in with Apple" capability for your App ID in the Certificates, Identifiers & Profiles section.
  4. Create Service ID and bind with App ID from the previous step. Apple will display the description field value to end-users on sign-in. You'll need that service Identifier as a ClientID later on.
  5. Configure "Sign in with Apple" for created Service ID. Add domain where you will use that auth on to "Domains and subdomains" and its main page URL (like https://example.com/ to "Return URLs".
  6. Register a New Key (private key) for the "Sign in with Apple" feature and download it. Write down the Key ID. This key will be used to create JWT Client Secret.
  7. Add your domain name and sender email in the Certificates, Identifiers & Profiles >> More section as a new Email Source.

After completing the previous steps, you can proceed with configuring the Apple auth provider. Here are the parameters for AppleConfig:

    // 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:

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:

See example before use.

Yandex Auth Provider

  1. Create a new "OAuth App": https://oauth.yandex.com/client/new
  2. Fill "App name" for your site
  3. Under Platforms select "Web services" and enter "Callback URI #1" constructed as domain + /auth/yandex/callback. ie https://example.mysite.com/auth/yandex/callback
  4. Select Permissions. You need following permissions only from the "Yandex.Passport API" section:
    • Access to user avatar
    • Access to username, first name and surname, gender
  5. Fill out the rest of fields if needed
  6. Take note of the ID and Password

For more details refer to Yandex OAuth and Yandex.Passport API documentation.

Battle.net Auth Provider
  1. Log into Battle.net as a developer: https://develop.battle.net/nav/login-redirect
  2. Click "+ CREATE CLIENT" https://develop.battle.net/access/clients/create
  3. For "Client name", enter whatever you want
  4. For "Redirect URLs", one of the lines must be "http[s]://your_remark_installation:port//auth/battlenet/callback", e.g. https://localhost:8443/auth/battlenet/callback or https://remark.mysite.com/auth/battlenet/callback
  5. For "Service URL", enter the URL to your site or check "I do not have a service URL for this client." checkbox if you don't have any
  6. For "Intended use", describe the application you're developing
  7. Click "Save".
  8. You can see your client ID and client secret at https://develop.battle.net/access/clients by clicking the client you created

For more details refer to Complete Guide of Battle.net OAuth API and Login Button or the official Battle.net OAuth2 guide

Patreon Auth Provider

  1. Create a new Patreon client https://www.patreon.com/portal/registration/register-clients
  2. Fill "App Name", "Description", "App Category" and "Author" for your site
  3. Under "Redirect URIs" enter the correct url constructed as domain + /auth/patreon/callback. ie https://example.mysite.com/auth/patreon/callback
  4. Take note of the Client ID and Client Secret

Discord Auth Provider

  1. Log into Discord Developer Portal https://discord.com/developers/applications
  2. Click on New Application to create the application required for Oauth
  3. After filling "NAME", navigate to "OAuth2" option on the left sidebar
  4. Under "Redirects" enter the correct url constructed as domain + /auth/discord/callback. ie https://remark42.mysite.com/auth/discord/callback
  5. Take note of the CLIENT ID and CLIENT SECRET

Twitter Auth Provider

  1. Create a new twitter application https://developer.twitter.com/en/apps
  2. Fill App name and Description and URL of your site
  3. In the field Callback URLs enter the correct url of your callback handler e.g. https://example.mysite.com/{route}/twitter/callback
  4. Under Key and tokens take note of the Consumer API Key and Consumer API Secret key. Those will be used as cid and csecret

XSRF Protections

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.

Status

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.