mirror of
https://forgejo.merr.is/annika/jwtauth.git
synced 2025-12-11 13:47:41 -05:00
Refactor jwtauth pkg, move to go-chi org, and support dgrijalva/jwt-go v3
This commit is contained in:
parent
d5f87ca5c3
commit
4c654d77d5
20 changed files with 180 additions and 1350 deletions
165
jwtauth.go
165
jwtauth.go
|
|
@ -8,12 +8,17 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnauthorized = errors.New("jwtauth: unauthorized token")
|
||||
ErrExpired = errors.New("jwtauth: expired token")
|
||||
TokenCtxKey = &contextKey{"Token"}
|
||||
ErrorCtxKey = &contextKey{"Error"}
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnauthorized = errors.New("jwtauth: token is unauthorized")
|
||||
ErrExpired = errors.New("jwtauth: token is expired")
|
||||
)
|
||||
|
||||
type JwtAuth struct {
|
||||
|
|
@ -26,16 +31,17 @@ type JwtAuth struct {
|
|||
// New creates a JwtAuth authenticator instance that provides middleware handlers
|
||||
// and encoding/decoding functions for JWT signing.
|
||||
func New(alg string, signKey []byte, verifyKey []byte) *JwtAuth {
|
||||
return &JwtAuth{
|
||||
signKey: signKey,
|
||||
verifyKey: verifyKey,
|
||||
signer: jwt.GetSigningMethod(alg),
|
||||
}
|
||||
return NewWithParser(alg, &jwt.Parser{}, signKey, verifyKey)
|
||||
}
|
||||
|
||||
// NewWithParser is the same as New, except it supports custom parser settings
|
||||
// introduced in ver. 2.4.0 of jwt-go
|
||||
// introduced in jwt-go/v2.4.0.
|
||||
//
|
||||
// We explicitly toggle `SkipClaimsValidation` in the `jwt-go` parser so that
|
||||
// we can control when the claims are validated - in our case, by the Verifier
|
||||
// http middleware handler.
|
||||
func NewWithParser(alg string, parser *jwt.Parser, signKey []byte, verifyKey []byte) *JwtAuth {
|
||||
parser.SkipClaimsValidation = true
|
||||
return &JwtAuth{
|
||||
signKey: signKey,
|
||||
verifyKey: verifyKey,
|
||||
|
|
@ -44,24 +50,27 @@ func NewWithParser(alg string, parser *jwt.Parser, signKey []byte, verifyKey []b
|
|||
}
|
||||
}
|
||||
|
||||
// Verifier middleware will verify a JWT passed by a client request.
|
||||
// The Verifier will look for a JWT token from:
|
||||
// 1. 'jwt' URI query parameter
|
||||
// 2. 'Authorization: BEARER T' request header
|
||||
// 3. Cookie 'jwt' value
|
||||
// Verifier http middleware handler will verify a JWT string from a http request.
|
||||
//
|
||||
// The verification processes finishes here and sets the token and
|
||||
// a error in the request context and calls the next handler.
|
||||
// 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
|
||||
//
|
||||
// Make sure to have your own handler following the Validator that
|
||||
// will check the value of the "jwt" and "jwt.err" in the context
|
||||
// and respond to the client accordingly. A generic Authenticator
|
||||
// middleware is provided by this package, that will return a 401
|
||||
// message for all unverified tokens, see jwtauth.Authenticator.
|
||||
// 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 (ja *JwtAuth) Verifier(next http.Handler) http.Handler {
|
||||
return ja.Verify("")(next)
|
||||
}
|
||||
|
||||
// TODO: explain
|
||||
func (ja *JwtAuth) Verify(paramAliases ...string) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
hfn := func(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
@ -72,7 +81,7 @@ func (ja *JwtAuth) Verify(paramAliases ...string) func(http.Handler) http.Handle
|
|||
// Get token from query params
|
||||
tokenStr = r.URL.Query().Get("jwt")
|
||||
|
||||
// Get token from other query param aliases
|
||||
// Get token from other param aliases
|
||||
if tokenStr == "" && paramAliases != nil && len(paramAliases) > 0 {
|
||||
for _, p := range paramAliases {
|
||||
tokenStr = r.URL.Query().Get(p)
|
||||
|
|
@ -92,16 +101,14 @@ func (ja *JwtAuth) Verify(paramAliases ...string) func(http.Handler) http.Handle
|
|||
|
||||
// Get token from cookie
|
||||
if tokenStr == "" {
|
||||
// TODO: paramAliases should apply to cookies too..
|
||||
cookie, err := r.Cookie("jwt")
|
||||
if err == nil {
|
||||
tokenStr = cookie.Value
|
||||
}
|
||||
}
|
||||
|
||||
// Token is required, cya
|
||||
if tokenStr == "" {
|
||||
err = ErrUnauthorized
|
||||
}
|
||||
// TODO: what other kinds of validations should we do / error messages?
|
||||
|
||||
// Verify the token
|
||||
token, err := ja.Decode(tokenStr)
|
||||
|
|
@ -139,9 +146,47 @@ func (ja *JwtAuth) Verify(paramAliases ...string) func(http.Handler) http.Handle
|
|||
}
|
||||
}
|
||||
|
||||
// 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 := TokenContext(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 TokenContext(ctx context.Context) (*jwt.Token, Claims, error) {
|
||||
token, _ := ctx.Value(TokenCtxKey).(*jwt.Token)
|
||||
|
||||
var claims Claims
|
||||
if token != nil {
|
||||
claims, _ = token.Claims.(Claims)
|
||||
} else {
|
||||
claims = Claims{}
|
||||
}
|
||||
|
||||
err, _ := ctx.Value(ErrorCtxKey).(error)
|
||||
|
||||
return token, claims, err
|
||||
}
|
||||
|
||||
func (ja *JwtAuth) SetContext(ctx context.Context, t *jwt.Token, err error) context.Context {
|
||||
ctx = context.WithValue(ctx, "jwt", t)
|
||||
ctx = context.WithValue(ctx, "jwt.err", err)
|
||||
ctx = context.WithValue(ctx, TokenCtxKey, t)
|
||||
ctx = context.WithValue(ctx, ErrorCtxKey, err)
|
||||
return ctx
|
||||
}
|
||||
|
||||
|
|
@ -154,10 +199,20 @@ func (ja *JwtAuth) Encode(claims Claims) (t *jwt.Token, tokenString string, err
|
|||
}
|
||||
|
||||
func (ja *JwtAuth) Decode(tokenString string) (t *jwt.Token, err error) {
|
||||
if ja.parser != nil {
|
||||
return ja.parser.Parse(tokenString, ja.keyFunc)
|
||||
// Decode the tokenString, but avoid using custom Claims via jwt-go's
|
||||
// ParseWithClaims as the jwt-go types will cause some glitches, so easier
|
||||
// to decode as MapClaims then wrap the underlying map[string]interface{}
|
||||
// to our Claims type
|
||||
t, err = ja.parser.Parse(tokenString, ja.keyFunc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return jwt.Parse(tokenString, ja.keyFunc)
|
||||
|
||||
// Wrap type to jwtauth.Claims
|
||||
claims := Claims(t.Claims.(jwt.MapClaims))
|
||||
t.Claims = claims
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (ja *JwtAuth) keyFunc(t *jwt.Token) (interface{}, error) {
|
||||
|
|
@ -169,7 +224,9 @@ func (ja *JwtAuth) keyFunc(t *jwt.Token) (interface{}, error) {
|
|||
}
|
||||
|
||||
func (ja *JwtAuth) IsExpired(t *jwt.Token) bool {
|
||||
if expv, ok := t.Claims["exp"]; ok {
|
||||
claims := t.Claims.(Claims)
|
||||
|
||||
if expv, ok := claims["exp"]; ok {
|
||||
var exp int64
|
||||
switch v := expv.(type) {
|
||||
case float64:
|
||||
|
|
@ -189,35 +246,16 @@ func (ja *JwtAuth) IsExpired(t *jwt.Token) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// Authenticator is a default authentication middleware to enforce access following
|
||||
// the Verifier middleware. The Authenticator sends a 401 Unauthorized response for
|
||||
// all 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) {
|
||||
ctx := r.Context()
|
||||
|
||||
if jwtErr, ok := ctx.Value("jwt.err").(error); ok {
|
||||
if jwtErr != nil {
|
||||
http.Error(w, http.StatusText(401), 401)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
jwtToken, ok := ctx.Value("jwt").(*jwt.Token)
|
||||
if !ok || jwtToken == nil || !jwtToken.Valid {
|
||||
http.Error(w, http.StatusText(401), 401)
|
||||
return
|
||||
}
|
||||
|
||||
// Token is authenticated, pass it through
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// Claims is a convenience type to manage a JWT claims hash.
|
||||
type Claims map[string]interface{}
|
||||
|
||||
// NOTE: as of v3.0 of jwt-go, Valid() interface method is called to verify
|
||||
// the claims. However, the current design we test these claims in the
|
||||
// Verifier middleware, so we skip this step.
|
||||
func (c Claims) Valid() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Claims) Set(k string, v interface{}) Claims {
|
||||
c[k] = v
|
||||
return c
|
||||
|
|
@ -262,3 +300,14 @@ func EpochNow() int64 {
|
|||
func ExpireIn(tm time.Duration) int64 {
|
||||
return EpochNow() + int64(tm.Seconds())
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue