jwtauth/jwtauth.go
Peter Kieltyka aa727a6feb goimports
2019-01-09 10:43:31 -05:00

278 lines
7.8 KiB
Go

package jwtauth
import (
"context"
"errors"
"fmt"
"net/http"
"strings"
"time"
jwt "github.com/dgrijalva/jwt-go"
)
// Context keys
var (
TokenCtxKey = &contextKey{"Token"}
ErrorCtxKey = &contextKey{"Error"}
)
// Library errors
var (
ErrUnauthorized = errors.New("jwtauth: token is unauthorized")
ErrExpired = errors.New("jwtauth: token is expired")
ErrNBFInvalid = errors.New("jwtauth: token nbf validation failed")
ErrIATInvalid = errors.New("jwtauth: token iat validation failed")
ErrNoTokenFound = errors.New("jwtauth: no token found")
ErrAlgoInvalid = errors.New("jwtauth: algorithm mismatch")
)
type JWTAuth struct {
signKey interface{}
verifyKey interface{}
signer jwt.SigningMethod
parser *jwt.Parser
}
// New creates a JWTAuth authenticator instance that provides middleware handlers
// and encoding/decoding functions for JWT signing.
func New(alg string, signKey interface{}, verifyKey interface{}) *JWTAuth {
return NewWithParser(alg, &jwt.Parser{}, signKey, verifyKey)
}
// NewWithParser is the same as New, except it supports custom parser settings
// introduced in jwt-go/v2.4.0.
func NewWithParser(alg string, parser *jwt.Parser, signKey interface{}, verifyKey interface{}) *JWTAuth {
return &JWTAuth{
signKey: signKey,
verifyKey: verifyKey,
signer: jwt.GetSigningMethod(alg),
parser: parser,
}
}
// Verifier http middleware handler will verify a JWT string from a http request.
//
// Verifier will search for a JWT token in a http request, in the order:
// 1. 'jwt' URI query parameter
// 2. 'Authorization: BEARER T' request header
// 3. Cookie 'jwt' value
//
// The first JWT string that is found as a query parameter, authorization header
// or cookie header is then decoded by the `jwt-go` library and a *jwt.Token
// object is set on the request context. In the case of a signature decoding error
// the Verifier will also set the error on the request context.
//
// The Verifier always calls the next http handler in sequence, which can either
// be the generic `jwtauth.Authenticator` middleware or your own custom handler
// which checks the request context jwt token and error to prepare a custom
// http response.
func Verifier(ja *JWTAuth) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return Verify(ja, TokenFromQuery, TokenFromHeader, TokenFromCookie)(next)
}
}
func Verify(ja *JWTAuth, findTokenFns ...func(r *http.Request) string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
hfn := func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
token, err := VerifyRequest(ja, r, findTokenFns...)
ctx = NewContext(ctx, token, err)
next.ServeHTTP(w, r.WithContext(ctx))
}
return http.HandlerFunc(hfn)
}
}
func VerifyRequest(ja *JWTAuth, r *http.Request, findTokenFns ...func(r *http.Request) string) (*jwt.Token, error) {
var tokenStr string
var err error
// Extract token string from the request by calling token find functions in
// the order they where provided. Further extraction stops if a function
// returns a non-empty string.
for _, fn := range findTokenFns {
tokenStr = fn(r)
if tokenStr != "" {
break
}
}
if tokenStr == "" {
return nil, ErrNoTokenFound
}
// Verify the token
token, err := ja.Decode(tokenStr)
if err != nil {
if verr, ok := err.(*jwt.ValidationError); ok {
if verr.Errors&jwt.ValidationErrorExpired > 0 {
return token, ErrExpired
} else if verr.Errors&jwt.ValidationErrorIssuedAt > 0 {
return token, ErrIATInvalid
} else if verr.Errors&jwt.ValidationErrorIssuedAt > 0 {
return token, ErrNBFInvalid
}
}
return token, err
}
if token == nil || !token.Valid {
err = ErrUnauthorized
return token, err
}
// Verify signing algorithm
if token.Method != ja.signer {
return token, ErrAlgoInvalid
}
// Valid!
return token, nil
}
func (ja *JWTAuth) Encode(claims jwt.Claims) (t *jwt.Token, tokenString string, err error) {
t = jwt.New(ja.signer)
t.Claims = claims
tokenString, err = t.SignedString(ja.signKey)
t.Raw = tokenString
return
}
func (ja *JWTAuth) Decode(tokenString string) (t *jwt.Token, err error) {
t, err = ja.parser.Parse(tokenString, ja.keyFunc)
if err != nil {
return nil, err
}
return
}
func (ja *JWTAuth) keyFunc(t *jwt.Token) (interface{}, error) {
if ja.verifyKey != nil {
return ja.verifyKey, nil
} else {
return ja.signKey, nil
}
}
// Authenticator is a default authentication middleware to enforce access from the
// Verifier middleware request context values. The Authenticator sends a 401 Unauthorized
// response for any unverified tokens and passes the good ones through. It's just fine
// until you decide to write something similar and customize your client response.
func Authenticator(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token, _, err := FromContext(r.Context())
if err != nil {
http.Error(w, http.StatusText(401), 401)
return
}
if token == nil || !token.Valid {
http.Error(w, http.StatusText(401), 401)
return
}
// Token is authenticated, pass it through
next.ServeHTTP(w, r)
})
}
func NewContext(ctx context.Context, t *jwt.Token, err error) context.Context {
ctx = context.WithValue(ctx, TokenCtxKey, t)
ctx = context.WithValue(ctx, ErrorCtxKey, err)
return ctx
}
func FromContext(ctx context.Context) (*jwt.Token, jwt.MapClaims, error) {
token, _ := ctx.Value(TokenCtxKey).(*jwt.Token)
var claims jwt.MapClaims
if token != nil {
if tokenClaims, ok := token.Claims.(jwt.MapClaims); ok {
claims = tokenClaims
} else {
panic(fmt.Sprintf("jwtauth: unknown type of Claims: %T", token.Claims))
}
} else {
claims = jwt.MapClaims{}
}
err, _ := ctx.Value(ErrorCtxKey).(error)
return token, claims, err
}
// UnixTime returns the given time in UTC milliseconds
func UnixTime(tm time.Time) int64 {
return tm.UTC().Unix()
}
// EpochNow is a helper function that returns the NumericDate time value used by the spec
func EpochNow() int64 {
return time.Now().UTC().Unix()
}
// ExpireIn is a helper function to return calculated time in the future for "exp" claim
func ExpireIn(tm time.Duration) int64 {
return EpochNow() + int64(tm.Seconds())
}
// Set issued at ("iat") to specified time in the claims
func SetIssuedAt(claims jwt.MapClaims, tm time.Time) {
claims["iat"] = tm.UTC().Unix()
}
// Set issued at ("iat") to present time in the claims
func SetIssuedNow(claims jwt.MapClaims) {
claims["iat"] = EpochNow()
}
// Set expiry ("exp") in the claims
func SetExpiry(claims jwt.MapClaims, tm time.Time) {
claims["exp"] = tm.UTC().Unix()
}
// Set expiry ("exp") in the claims to some duration from the present time
func SetExpiryIn(claims jwt.MapClaims, tm time.Duration) {
claims["exp"] = ExpireIn(tm)
}
// TokenFromCookie tries to retreive the token string from a cookie named
// "jwt".
func TokenFromCookie(r *http.Request) string {
cookie, err := r.Cookie("jwt")
if err != nil {
return ""
}
return cookie.Value
}
// TokenFromHeader tries to retreive the token string from the
// "Authorization" reqeust header: "Authorization: BEARER T".
func TokenFromHeader(r *http.Request) string {
// Get token from authorization header.
bearer := r.Header.Get("Authorization")
if len(bearer) > 7 && strings.ToUpper(bearer[0:6]) == "BEARER" {
return bearer[7:]
}
return ""
}
// TokenFromQuery tries to retreive the token string from the "jwt" URI
// query parameter.
func TokenFromQuery(r *http.Request) string {
// Get token from query param named "jwt".
return r.URL.Query().Get("jwt")
}
// contextKey is a value for use with context.WithValue. It's used as
// a pointer so it fits in an interface{} without allocation. This technique
// for defining context keys was copied from Go 1.7's new use of context in net/http.
type contextKey struct {
name string
}
func (k *contextKey) String() string {
return "jwtauth context value " + k.name
}