package jwtauth import ( "context" "errors" "fmt" "net/http" "strings" "time" "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.MapClaims) (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()) } // 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 }