jwtauth

package module
v2.0.0+incompatible Latest Latest
Warning

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

Go to latest
Published: May 20, 2019 License: MIT Imports: 20 Imported by: 0

README

Build Status Coverage Go Report Docs

Package jwtauth provides middlewares for the Goa framework that perform "auth" (authentication and authorization) using JSON WEB Tokens.

When you install the authentication middleware, it populates the context of every request with a Claims object, representing all of the JWT claims associated with the request. Unauthenticated requests have a present-but-empty Claims object.

The authorization middleware makes use of JWT claims, comparing them against goa's ContextRequiredScopes to decide whether the request may proceed.

Authentication and authorization behaviors can be customized by passing an optional callback when the middlewares are instantiated.

Usage

This is a trivial example; for thorough information, please consult the godoc.

First install jwtauth and its dependency:

go get -u github.com/rightscale/jwtauth github.com/dgrijalva/jwt-go

In your service's design DSL, declare a JWT security scheme and protect some of your actions with required scopes:

var JWT = JWTSecurity("JWT", func() {
        Header("Authorization")
})

var _ = Resource("Bottle", func() {  
   Security(JWT)

   Action("drink", func() {
     Security(JWT, func() {
       Scope("bottle:drink")
     })
   })      
})

When you create your goa.Service at startup, determine which keys to trust, then install a pair of jwtauth middlewares: one for authentication, one for authorization.

  secret := []byte("super secret HMAC key")
  store := &jwtauth.SimpleKeystore{Key: secret}

  // Authentication is not a security scheme in goa's terminology; it is
  // merely a prerequisite to authorization that handles parsing and validating
  // the JWT.
  service.Use(jwtauth.Authenticate(app.NewJWTSecurity(), store))

  // The authorization middleware should be mounted through goa's UseXxx
  // functions, so that goa knows which middleware is associated with which
  // security scheme.
  app.UseJWTMiddleware(service, jwtauth.Authorize())

Create a token and hand it out to your user:

  claims := jwtauth.NewClaims("iss", "example.com", "sub", "Bob", "scopes", []string{"bottle:drink"})
  token := jwtauth.NewToken("super secret HMAC key", claims)
  fmt.Println("the magic password is", token)

Now, sit back and enjoy the security! Your user won't be able to drink your bottles unless she includes the token as a header:

curl -X POST http://localhost:8080/bottles/drink -H "Authorization: Bearer $myjwt"

(The "bearer" is unimportant; it can be any word, or be absent, and jwtauth will still parse the token.)

You can also make use of authentication claims in your controllers:

func (c *BottleController) Drink(ctx app.DrinkBottleContext) {
  claims := jwtauth.ContextClaims(ctx)
  return fmt.Printf("Hello, %s", claims.Subject())
}

Documentation

Overview

Package jwtauth provides a middleware for the Goa framework that parses and validates JSON Web Tokens (JWTs) that appear in requests, then adds them to the request context. It supports any JWT algorithm that uses RSA, ECDSA or HMAC.

When you setup your goa.Service, install the jwtauth middlewares:

secret := []byte("super secret HMAC key")
store := jwtauth.SimpleKeystore{Key: secret}

service.Use(jwtauth.Authenticate(app.NewJWTSecurity(), store))

app.UseJWTMiddleware(service, jwtauth.Authorize())

In this example, jwtauth uses a single, static HMAC key and relies on the default authentication and authorization behavior. Your users can now include an authorization token with every request:

GET /foo
Authorization: Bearer <JWT goes here>

When someone makes a request containing a JWT, jwauth verifies that the token contains all of the scopes that are required by your action, as determined by goa.ContextRequiredScopes(). If anything is missing, jwtauth returns 4xx or 5xx error with a detailed message.

Authentication vs. Authorization

In Goa's parlance, a "security scheme" mostly concerns itself with authorization: deciding whether the request may proceed to your controller. However, before we can authorize, we must know who is making the request, i.e. we must authenticate the request.

Package jwtauth encourages separation of concerns by performing authentication and authorization in two separate middlewares. The division of responsibility is as follows.

Authentication: determines whether a JWT is present; parses the JWT; validates its signature; creates a jwtauth.Claims object representing the information in the JWT; calls jwtauth.WithClaims() to create a new Context containing the Claims.

Authorization: calls jwtauth.ContextClaims(), then decides whether the request is allowed based on the claims, the required scopes, and potentially on other request information.

Multiple Issuers

For real-world applications, it is advisable to register several trusted keys so you can perform key rotation on the fly and compartmentalize trust. If you initialize the middleware with a NamedKeystore, it uses the value of the JWT "iss" (Issuer) claim to select a verification key for each token.

import jwtgo "github.com/dgrijalva/jwt-go"
usKey := jwtgo.ParseRSAPublicFromPEM(ioutil.ReadFile("us.pem))
euKey := jwtgo.ParseRSAPublicKeyFromPEM(ioutil.ReadFile("eu.pem))

store := jwt.NamedKeystore{}
store.Trust("us.acme.com", usKey))
store.Trust("eu.acme.com", euKey))

middleware := jwt.New(app.NewJWTSecurity(), store)

Using a NamedKeystore, you can grant or revoke trust at any time, even while the application is running, and your changes will take effect on the next request.

Custom Authorization

To change how jwtauth performs authorization, write your own function that matches the signature of type AuthorizationFunc, then tell jwtauth ot use your function instead of its own:

    func myAuthzFunc(ctx context.Context) error {
			return fmt.Errorf("nobody may do anything!")
	  }

		middleware := jwtauth.AuthorizeWithFunc(myAuthzFunc)

When overriding authorization behavior, you can always delegate some work to the default behavior. For example, to let users do anything on their birthday:

		func myAuthzFunc(ctx context.Context) error {
      claims := jwtauth.ContextClaims(ctx)
      if birthday := claims.Time("birthday"); !birthday.IsZero() {
        _, bm, bd := birthday.Date()
        _, m, d := time.Now().Date()
        if bm == m && bd == d {
          // Happy birthday!
          return nil
        }
      }

			return jwtauth.DefaultAuthorization(ctx)
		}

Custom Token Extraction

You can specialize the logic used to extract a JWT from the request by providing the Extraction() option:

func myExtraction(*goa.JWTSecurity, *http.Request) (string, error) {
  return "", fmt.Errorf("I hate token1!")
}

store := jwt.SimpleKeystore{[]byte("This is my HMAC key")}
middleware := jwtauth.New(scheme, store,
  jwtauth.Extraction(myExtraction)
)

The default extraction behavior, described below, should be sufficient for almost any use case.

DefaultExtraction supports only security schemes that use goa.LocHeader; JWTs in the query string, or in other locations, are not supported.

Although jwtauth uses the header name specified by the goa.JWTSecurity definition that is used to initialize it, some assumptions are made about the format of the header value. It must contain a base64-encoded JWT, which may be prefixed by a single-word qualifier. Assuming the security scheme uses the Authorization header, any of the following would be acceptable:

Authorization: <base64_token>
Authorization: Bearer <base64_token>
Authorization: JWT <base64_token>
Authorization: AnyOtherWordHere <base64_token>

Token Management

If you need to create tokens, jwtauth contains a simplistic helper that helps to shadow the dependency on dgrijalva/jwt:

claims := jwtauth.NewClaims()
token, err := NewToken("my HMAC key", claims)
fmt.Println("the magic token is", token)

Error Handling

Common errors are returned as instances of a goa error class, which have the effect of responding with a specific HTTP status:

ErrUnsupported (500): the token or configuration uses an unsupported feature.

ErrInvalidToken (401): the token is malformed or its signature is bad.

ErrAuthenticationFailed (403): the token is well-formed but the issuer is not trusted, it has expired, or is not yet valid.

ErrAuthorizationFailed (403): the token is well-formed and valid, but the authentication principal did not satisfy all of the scopes required to call the requested goa action.

Testing

Call TestMiddleware() to create a middleware initialized to trust a static key, e.g. for unit tests.

Call TestToken() to create a valid token signed by the same key.

NEVER USE THESE FUNCTIONS in production; they are intended only for testing!

Index

Constants

View Source
const TestKey = "https://github.com/rightscale/jwtauth#test"

TestKey is a static HMAC key used to sign and verify test JWTs.

Variables

View Source
var (
	// ErrUnsupported indicates that the application is configured to use a
	// capability that jwtauth does not support.
	ErrUnsupported = goa.NewErrorClass("unsupported", 500)

	// ErrInvalidToken indicates that the request's JWT was malformed or
	// its signature could not be verified.
	ErrInvalidToken = goa.NewErrorClass("invalid_token", 401)

	// ErrAuthorizationFailed indicates that the request's JWT was well-formed
	// and valid, but the user is not authorized to perform the requested
	// operation.
	ErrAuthorizationFailed = goa.NewErrorClass("authorization_failed", 403)
)
View Source
var ScopesClaim = "scopes"

ScopesClaim is a Private Claim Name, as stipulated in RFC7519 Section 4.3, that jwtauth uses to store scope information in tokens. If you need to interoperate with third parties w/r/t to token scope, it may be advisable to change this to a Collision-Resistant Claim Name instead.

Functions

func Authenticate

func Authenticate(scheme *goa.JWTSecurity, store Keystore) goa.Middleware

Authenticate creates a middleware that authenticates incoming requests. Specifically, the middleware parses JWTs from a location specified by scheme, validates their signatures using the keys in store, and adds a Claims object to the context, which can be accessed by calling ContextClaims().

Authentication is not authorization! Do not use this middleware as a goa security scheme itself; rather, install this middleware application-wide, so that the authentication claims become available to your authorization middleware(s) that implement your security schemes.

func AuthenticateWithFunc

func AuthenticateWithFunc(scheme *goa.JWTSecurity, store Keystore, extraction ExtractionFunc) goa.Middleware

AuthenticateWithFunc creates an authentication middleware that uses a custom ExtractionFunc.

func Authorize

func Authorize() goa.Middleware

Authorize creates a middleware that authorizes incoming requests. Specifically, the middleware compares goa's required scopes against the claimed scopes contained in the JWT, ensuring that the claimed scopes are a superset of the required scopes.

Most applications will require a more nuanced authorization scheme; to do this, use DefaultAuthorization() as a starting point for implementing your own authorization behavior; then, instead of calling this function, call AuthorizeWithFunc() to instantiate a middleware that uses your custom behavior.

func AuthorizeWithFunc

func AuthorizeWithFunc(fn AuthorizationFunc) goa.Middleware

AuthorizeWithFunc creates a middleware that authorizes requests using a custom AuthorizationFunc.

func ContextToken

func ContextToken(ctx context.Context) string

ContextToken retrieves the actual JWT associated with the request.

func DefaultAuthorization

func DefaultAuthorization(ctx context.Context, claims Claims) error

DefaultAuthorization is the default authorization method. It compares the context's required scopes against a list of scopes that are claimed in the JWT. If the claimed scopes satisfy all required scopes, DefaultAuthorization passes the request; otherwise, it responds with ErrAuthorizationFailed.

If the context requires no scopes, DefaultAuthorization still verifies that SOME claims are present, under the assumption that the user needs to be authenticated even if they do not require any specific privilege.

func DefaultExtraction

func DefaultExtraction(scheme *goa.JWTSecurity, req *http.Request) (string, error)

DefaultExtraction is the default token-extraction method. It finds the header named in the security scheme, discards an optional one-word prefix such as "Bearer" or "JWT", and returns the remainder of the header value.

DefaultExtraction is compatible with OAuth2 bearer-token and other schemes that use the Authorization header to transmit a JWT.

func LoadKey

func LoadKey(material []byte) interface{}

LoadKey is a helper function that transforms raw key material into a properly- typed key.

LoadKey returns a different type depending on the value of material:

If material is a []byte that contains a PEM-encoded PKIX key (e.g. "BEGIN PUBLIC KEY"), LoadKey parses it and returns a single public or private key of an algorithm-specific type.

If material is any other []byte, LoadKey returns it unmodified so that it can be used as an HMAC key.

Because LoadKey is designed to be used at startup, it panics if the PEM block is malformed.

func NewToken

func NewToken(key interface{}, claims Claims) (string, error)

NewToken creates a JWT with the specified claims and signs it using the specified issuer key.

This method assumes that odd-numbered keyvals are always strings (claim names) and panics otherwise.

Example token identifying Bob, issued by Alice, and good for one hour:

exp := time.Now().Add(time.Hour)
claims := jwt.NewClaims("iss", "alice", "sub", "bob", "exp", exp)
tok := jwt.NewToken(alicesKey, claims)

Example token that contains authorization scopes, which the default authorization function will test against goa's RequiredScopes:

scopes = []string{"read","write"}
claims := jwt.NewClaims("iss", "alice", "exp", exp, jwtauth.ScopesClaim, scopes)

In order for recipients to verify the example tokens above, their keystore must associate the "alice" issuer with alicesKey -- which can be either a []byte (for HMAC tokens) or a crypto.PrivateKey (for public-key tokens).

There is no standard claim name for authorization scopes, so jwtauth uses the least-surprising name, "scopes."

func TestAuthenticate

func TestAuthenticate(scheme *goa.JWTSecurity) goa.Middleware

TestAuthenticate returns an authentication middleware that accepts any JWT signed with TestKey.

func TestToken

func TestToken(keyvals ...interface{}) string

TestToken creates a JWT with the specified claims and signs it using TestKey.

func WithClaims

func WithClaims(ctx context.Context, claims Claims) context.Context

WithClaims creates a child context containing the given JWT claims.

func WithToken

func WithToken(ctx context.Context, token string) context.Context

WithToken creates a child context containing the given JWT.

Types

type AuthorizationFunc

type AuthorizationFunc func(context.Context, Claims) error

AuthorizationFunc is an optional callback that allows customization of the way the middleware authorizes each request.

type Claims

type Claims map[string]interface{}

Claims is a collection of claims extracted from a JWT.

func ContextClaims

func ContextClaims(ctx context.Context) Claims

ContextClaims retrieves the JWT claims associated with the request.

func NewClaims

func NewClaims(keyvals ...interface{}) Claims

NewClaims builds a map of claims using alternate keys and values from the variadic parameters. It is sugar designed to make new-token creation code more readable. Example:

claims := jwtauth.NewClaims("iss", "alice", "sub", "bob", "scopes", []string{"read", "write"})

If any odd-numbered key is not a string, this function will panic!

func (Claims) Bool

func (c Claims) Bool(name string) bool

Bool returns the named claim as a boolean, converting from other types as necessary. If the claim is absent or cannot be converted to a boolean, Bool returns false.

func (Claims) ExpiresAt

func (c Claims) ExpiresAt() time.Time

ExpiresAt returns time at which the claims were issued.

func (Claims) Int

func (c Claims) Int(name string) int64

Int returns the named claim as an integer, converting from other types as necessary. If the claim is absent or cannot be converted to an integer, Int returns 0.

func (Claims) IssuedAt

func (c Claims) IssuedAt() time.Time

IssuedAt returns time at which the claims were issued.

func (Claims) Issuer

func (c Claims) Issuer() string

Issuer returns the value of the standard JWT "iss" claim, converting to string if necessary.

func (Claims) NotBefore

func (c Claims) NotBefore() time.Time

NotBefore returns time at which the claims were issued.

func (Claims) String

func (c Claims) String(name string) string

String returns the named claim as a string, converting from other types using fmt.Stringer if supported, or fmt.Sprint() otherwise. If the claim is absent, String returns "".

func (Claims) Strings

func (c Claims) Strings(name string) []string

Strings returns the named claim as a list of strings, following the same conversion rules as String(). If the claim is absent, Strings returns nil.

func (Claims) Subject

func (c Claims) Subject() string

Subject returns the value of the standard JWT "iss" claim, converting to string if necessary.

func (Claims) Time

func (c Claims) Time(name string) time.Time

Time returns the named claim as a Time in the Unix epoch. If the claim is absent or cannot be converted to an integer, it returns 0.

type ExtractionFunc

type ExtractionFunc func(*goa.JWTSecurity, *http.Request) (string, error)

ExtractionFunc is an optional callback that allows you to customize jwtauth's handling of JSON Web Tokens during authentication.

If your use case involves a proprietary JWT encoding, or a nonstandard location for the JWT, you can handle it with a custom ExtractionFunc.

The return value from ExtractionFunc should either be the empty string (if no token was present in the request), or a well-formed JWT.

type Keystore

type Keystore interface {
	// Trust grants trust in an issuer.
	Trust(issuer string, key interface{}) error
	// RevokeTrust revokes trust in an issuer.
	RevokeTrust(issuer string)
	// Get returns the key associated with the named issuer.
	Get(issuer string) interface{}
}

Keystore interface

When the middleware receives a request containing a JWT, it extracts the "iss" (Issuer) claim from the JWT body and gets a correspondingly-named key from the keystore, which it uses to verify the JWT's integrity.

type NamedKeystore

type NamedKeystore struct {
	sync.RWMutex
	// contains filtered or unexported fields
}

NamedKeystore is a concurrency-safe, in-memory Keystore implementation that allows trust to be granted/revoked from issuers at any time.

All methods are safe to call on the zero value of this type; fields are initialized as needed.

func (*NamedKeystore) Get

func (nk *NamedKeystore) Get(issuer string) interface{}

Get implements jwtauth.Keystore#Get

func (*NamedKeystore) RevokeTrust

func (nk *NamedKeystore) RevokeTrust(issuer string)

RevokeTrust implements jwtauth.Keystore#RevokeTrust

func (*NamedKeystore) Trust

func (nk *NamedKeystore) Trust(issuer string, key interface{}) error

Trust implements jwtauth.Keystore#Trust

Grants trust in an issuer. It accepts any of the following types:

  • []byte (for HS tokens)
  • *rsa.PublicKey (for RS tokens)
  • *ecdsa.PublicKey (for ES tokens)

As a convenience, it converts the following to a related type:

  • string becomes []byte
  • *rsa.PrivateKey becomes its public key
  • *ecdsa.PrivateKey becomes its public key

type SimpleKeystore

type SimpleKeystore struct {
	Key interface{}
}

SimpleKeystore is a Keystore that trusts exactly one key regardless of the token's issuer.

Trust() and RevokeTrust() have no effect, although Trust() returns an error if called with a key other than the one-and-only trusted key.

func (*SimpleKeystore) Get

func (sk *SimpleKeystore) Get(issuer string) interface{}

Get implements jwtauth.Keystore#Get

func (*SimpleKeystore) RevokeTrust

func (sk *SimpleKeystore) RevokeTrust(issuer string)

RevokeTrust implements jwtauth.Keystore#RevokeTrust

func (*SimpleKeystore) Trust

func (sk *SimpleKeystore) Trust(issuer string, key interface{}) error

Trust implements jwtauth.Keystore#Trust

Directories

Path Synopsis
v2
pkg

Jump to

Keyboard shortcuts

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