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
- Variables
- func Authenticate(scheme *goa.JWTSecurity, store Keystore) goa.Middleware
- func AuthenticateWithFunc(scheme *goa.JWTSecurity, store Keystore, extraction ExtractionFunc) goa.Middleware
- func Authorize() goa.Middleware
- func AuthorizeWithFunc(fn AuthorizationFunc) goa.Middleware
- func ContextToken(ctx context.Context) string
- func DefaultAuthorization(ctx context.Context, claims Claims) error
- func DefaultExtraction(scheme *goa.JWTSecurity, req *http.Request) (string, error)
- func LoadKey(material []byte) interface{}
- func NewToken(key interface{}, claims Claims) (string, error)
- func TestAuthenticate(scheme *goa.JWTSecurity) goa.Middleware
- func TestToken(keyvals ...interface{}) string
- func WithClaims(ctx context.Context, claims Claims) context.Context
- func WithToken(ctx context.Context, token string) context.Context
- type AuthorizationFunc
- type Claims
- func (c Claims) Bool(name string) bool
- func (c Claims) ExpiresAt() time.Time
- func (c Claims) Int(name string) int64
- func (c Claims) IssuedAt() time.Time
- func (c Claims) Issuer() string
- func (c Claims) NotBefore() time.Time
- func (c Claims) String(name string) string
- func (c Claims) Strings(name string) []string
- func (c Claims) Subject() string
- func (c Claims) Time(name string) time.Time
- type ExtractionFunc
- type Keystore
- type NamedKeystore
- type SimpleKeystore
Constants ¶
const TestKey = "https://github.com/rightscale/jwtauth#test"
TestKey is a static HMAC key used to sign and verify test JWTs.
Variables ¶
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) )
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 ¶
ContextToken retrieves the actual JWT associated with the request.
func DefaultAuthorization ¶
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 ¶
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 ¶
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 ¶
WithClaims creates a child context containing the given JWT claims.
Types ¶
type AuthorizationFunc ¶
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 ¶
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 ¶
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) Int ¶
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) Issuer ¶
Issuer returns the value of the standard JWT "iss" claim, converting to string if necessary.
func (Claims) 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 ¶
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.
type ExtractionFunc ¶
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 ¶
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