servertoken

package module
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 22, 2019 License: MIT Imports: 9 Imported by: 1

README

servertoken

Server Token authorization using Go. A server token is a way to identify a client of an API that is not accessing a resource directly as the user (as in Oauth2). Some shops call these server tokens API keys, I chose server token.

To validate the server token, Basic Authentication is used for endpoints that do not require a user's authentication. For Basic Auth endpoints, a pre-assigned "Server" token should be sent in the username field of the Basic Authentication scheme. The password should be left blank.

Code Walkthrough

Through the magic of closures, the servertoken.Handler function is able to reference the request details(r.Context and r.BasicAuth below). The API servertoken is taken from the username portion of the Basic Authorization HTTP request header. I chose to do API authorization with Basic Auth because it's easy to understand. Everyone does authorization differently (see some research below). Authorization using Oauth2 (and now Webauthn) are something I need to use for many of my APIs as well, but that's a whole other story.

// Handler middleware performs Server Token authorization
// The client must send their Server token as the username portion of
// HTTP Basic Authenication. This middleware will parse the token and
// determine if the token is valid for the request path and method
func Handler(log zerolog.Logger, db *sql.DB) (mw func(http.Handler) http.Handler) {
    mw = func(h http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            const op errors.Op = "servertoken/Handler"

            // retrieve the context from the http.Request
            ctx := r.Context()

            token, _, ok := r.BasicAuth()
            if !ok {
                err := errors.E(op, "Unable to parse Basic Auth string")
                // log error
                log.Error().Err(err).
                    Str("path", r.URL.Path).
                    Str("method", r.Method).Msg("")
                // use RE (Response Error) to send Response with appropriate http status code
                err = errors.RE(http.StatusUnauthorized, errors.Permission, "BasicAuth Parse Error", err)
                errors.HTTPError(w, err)
                return
               }

Once the token has been successfully parsed from the Basic Auth header, it is set inside the ServerToken type and the Authorize method is called sending the request context, logger, app db, path of the incoming request as well as the request method.

            st := ServerToken(token)

            ctx, err := st.Authorize(ctx, log, db, r.URL.Path, r.Method)
            if err != nil {
                err := errors.E(op, err)
                // log error
                log.Error().Err(err).
                    Str("servertoken", st.String()).
                    Str("path", r.URL.Path).
                    Str("method", r.Method).Msg("")
                // use RE (Response Error) to send Response with appropriate http status code
                err = errors.RE(http.StatusUnauthorized, errors.Permission, "Unauthorized", err)
                errors.HTTPError(w, err)
                return
            }

Inside the servertoken.Authorize method, the servertoken is added to the context

func (s *ServerToken) Authorize(ctx context.Context, log zerolog.Logger, db *sql.DB, path string, method string) (context.Context, error) {
    const op errors.Op = "servertoken/Authorize"

    ctx = s.Add2Ctx(ctx)

using the Add2Ctx method

type contextKey string

var ctxKey = contextKey("ServerToken")

// Add2Ctx adds the calling serverToken to the context
func (s *ServerToken) Add2Ctx(ctx context.Context) context.Context {

   ctx = context.WithValue(ctx, ctxKey, s.String())

   return ctx
}

Next in the servertoken.Authorize method, I authorize the servertoken against my app database using the servertoken.authorizeDB method. You could do this in something like Redis for speed, but for keeping it simple for now, I used the same app db as everything else.

    err := s.authorizeDB(ctx, log, db, path, method)

The Authorize method uses a stored function istokenvalid which returns a boolean in the database.

create function istokenvalid(p_server_token character varying, p_path character varying, p_method character varying)
returns boolean
  language plpgsql
as
$$
DECLARE
  v_exists int;
  v_ok boolean;
BEGIN

  BEGIN
    SELECT 1 INTO STRICT v_exists
      FROM auth.server_token a
     WHERE a.server_token = p_server_token
       and a.resource = p_path
       and a.method = p_method
       and (a.exp_timestamp is null or a.exp_timestamp > current_timestamp);
    EXCEPTION
        WHEN NO_DATA_FOUND THEN
            v_ok = false;
        WHEN TOO_MANY_ROWS THEN
            v_ok = false;
  END;

  if v_exists = 1 then
    v_ok = true;
  end if;

  return v_ok;

END;

$$;

The table structure is quite simple

create table auth.server_token
(
    server_token  varchar(1000) not null,
    resource      varchar(1000) not null,
    method        varchar(30)  not null,
    exp_timestamp timestamp
);

comment on table auth.server_token is 'Table is used to authorize server tokens.';

The stored function is called below to determine validity of the token.

// authorizeServerToken uses a db function to authorize a client's ServerToken
func (s *ServerToken) authorizeDB(ctx context.Context, log zerolog.Logger, db *sql.DB, path string, method string) error {
    const op errors.Op = "servertoken/authorizeDB"

    var ok *bool

    err := db.QueryRowContext(ctx, `SELECT auth.istokenvalid(
        p_server_token => $1,
        p_path => $2,
        p_method => $3)`, s, path, method).Scan(&ok)

    switch {
    case err == sql.ErrNoRows:
        return errors.E(op, "Database Error - select returned no results")
    case err != nil:
        return errors.E(op, err)
    }

    if !*ok {
        return errors.E(op, "Token is not authorized")
    }

    return nil

}

If valid, then back in servertoken.Authorize, the context with the servertoken is returned to the Handler.

    return ctx, err
}

Back inside the Handler function, the context is added to the request context as part of the call to the next Handler in the chain.

            h.ServeHTTP(w, r.WithContext(ctx)) // call original
        })
    }
    return
}

Research for Server Authorization Tokens

Uber

Uber provides both a server token and a user token in the Uber admin console

  • Uber uses an OAuth Bearer token for requests that require a user's login:
$ curl -H 'Authorization: Bearer <USER_ACCESS_TOKEN>' \
     -H 'Accept-Language: en_US' \
     -H 'Content-Type: application/json' \
     'https://api.uber.com/v1.2/estimates/price?start_latitude=37.7752315&start_longitude=-122.418075&end_latitude=37.7752415&end_longitude=-122.518075'
  • Uber uses an unusual "Token" Authorization scheme for server tokens that do not require a user login:
$ curl -H 'Authorization: Token <SERVER_TOKEN>' \
     -H 'Accept-Language: en_US' \
     -H 'Content-Type: application/json' \
     'https://api.uber.com/v1.2/estimates/price?start_latitude=37.7752315&start_longitude=-122.418075&end_latitude=37.7752415&end_longitude=-122.518075'
Twilio
  • Twilio uses HTTP Basic Authentication and provides a SID (some custom unique ID) and an auth token as part of the Twilio admin console. This account SID thing is also passed back in responses
$ curl -G https://api.twilio.com/2010-04-01/Accounts \
    -u '[YOUR ACCOUNT SID]:[YOUR AUTH TOKEN]'
Stripe
  • Stripe just has you pass an API key as the username of HTTP Basic Authentication and no password
$ curl https://api.stripe.com/v1/charges \
   -u 'sk_test_4eC39HqLyjWDarjtT1zdp7dc:'
Mailchimp
  • Mailchimp has you pass your API key in the password field of HTTP Basic Authentication (you can pass anything you want in the username section). Mailchimp also supports Oauth2, but has a somewhat unusual implementation of it (no Bearer token, an "Oauth token" instead), no refresh token, etc.
$ curl --request GET \
--url 'https://<dc>.api.mailchimp.com/3.0/' \
--user 'anystring:<your_apikey>'

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Handler

func Handler(env *env.Env) (mw func(http.Handler) http.Handler)

Handler middleware pulls the servertoken from the request (populated in the user field of the HTTP Basic Auth scheme). It then determines the client for the servertoken and authorizes that the client has access to the service given the host, path and http method

Types

type ServerToken

type ServerToken string

ServerToken is a token which represents a Server

func (*ServerToken) Authorize

func (s *ServerToken) Authorize(ctx context.Context, log zerolog.Logger, db *sql.DB, r *http.Request) (context.Context, error)

Authorize leverages HTTP standards for Basic Authentication. The expectation is that the client will provide their ServerToken in the username field of the Basic Authentication header. The token must be base64 encoded to be compliant with the spec and be parsed.

func (ServerToken) String added in v0.1.1

func (s ServerToken) String() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL