Fixing codebase after JWT lib switch
This commit is contained in:
@@ -85,6 +85,8 @@ func (s server) Serve(ctx context.Context) {
|
||||
r.Use(LogResponse)
|
||||
}
|
||||
|
||||
println("using up DefaultJwtHandler", auth.DefaultJwtHandler != nil)
|
||||
|
||||
r.Use(
|
||||
auth.DefaultJwtHandler.HttpVerifier(),
|
||||
auth.DefaultJwtHandler.HttpAuthenticator(),
|
||||
|
||||
+65
-67
@@ -4,12 +4,15 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/pkg/api"
|
||||
"github.com/cortezaproject/corteza-server/pkg/id"
|
||||
"github.com/cortezaproject/corteza-server/pkg/payload"
|
||||
"github.com/cortezaproject/corteza-server/system/types"
|
||||
"github.com/go-chi/jwtauth"
|
||||
"github.com/lestrrat-go/jwx/jwa"
|
||||
"github.com/lestrrat-go/jwx/jwk"
|
||||
"github.com/lestrrat-go/jwx/jwt"
|
||||
@@ -22,7 +25,7 @@ type (
|
||||
expiry time.Duration
|
||||
|
||||
signAlgo jwa.SignatureAlgorithm
|
||||
signKey jwk.Set
|
||||
signKey jwk.Key
|
||||
}
|
||||
|
||||
tokenStore interface {
|
||||
@@ -47,33 +50,30 @@ var (
|
||||
DefaultJwtStore tokenStore
|
||||
)
|
||||
|
||||
func SetupDefault(secret string, expiry time.Duration) {
|
||||
func SetupDefault(secret string, expiry time.Duration) (err error) {
|
||||
// Use JWT secret for hmac signer for now
|
||||
DefaultSigner = HmacSigner(secret)
|
||||
DefaultJwtHandler, _ = TokenManager(secret, expiry)
|
||||
DefaultJwtHandler, err = TokenManager(secret, expiry)
|
||||
return
|
||||
}
|
||||
|
||||
func TokenManager(secret string, expiry time.Duration) (*tokenManager, error) {
|
||||
var (
|
||||
err error
|
||||
set jwk.Set
|
||||
)
|
||||
|
||||
if len(secret) == 0 {
|
||||
return nil, fmt.Errorf("JWT secret missing")
|
||||
}
|
||||
|
||||
// @todo jwk.Parse can accept other input types beside byte-slice
|
||||
// we could use it to strength Corteza's security
|
||||
if set, err = jwk.Parse([]byte(secret)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &tokenManager{
|
||||
// TokenManager returns token management facility
|
||||
// @todo should be extended to accept different kinds of algorythms, private-keys etc.
|
||||
func TokenManager(secret string, expiry time.Duration) (tm *tokenManager, err error) {
|
||||
tm = &tokenManager{
|
||||
expiry: expiry,
|
||||
signAlgo: jwa.HS512,
|
||||
signKey: set,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if len(secret) == 0 {
|
||||
return nil, fmt.Errorf("JWK missing")
|
||||
}
|
||||
|
||||
if tm.signKey, err = jwk.New([]byte(secret)); err != nil {
|
||||
return nil, fmt.Errorf("could not parse JWK: %w", err)
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
//
|
||||
//var (
|
||||
@@ -103,9 +103,9 @@ func SetJWTStore(store tokenStore) {
|
||||
DefaultJwtStore = store
|
||||
}
|
||||
|
||||
// Authenticate the tokej from the given string and return parsed token or error
|
||||
// Authenticate the token from the given string and return parsed token or error
|
||||
func (tm *tokenManager) Authenticate(token string) (pToken jwt.Token, err error) {
|
||||
if pToken, err = jwt.Parse([]byte(token), jwt.WithKeySet(tm.signKey)); err != nil {
|
||||
if pToken, err = jwt.Parse([]byte(token), jwt.WithVerify(tm.signAlgo, tm.signKey)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ func (tm *tokenManager) Encode(identity Identifiable, clientID uint64, scope ...
|
||||
|
||||
// previous implementation had special a "salt" claim that ensured JWT uniquness
|
||||
// we're using more standard approach with JWT ID now.
|
||||
if err = token.Set(jwt.JwtIDKey, id.Next()); err != nil {
|
||||
if err = token.Set(jwt.JwtIDKey, fmt.Sprintf("%d", id.Next())); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -194,49 +194,47 @@ func (tm *tokenManager) Encode(identity Identifiable, clientID uint64, scope ...
|
||||
//return access
|
||||
}
|
||||
|
||||
//// HttpVerifier returns a HTTP handler that verifies JWT and stores it into context
|
||||
//func (t *tokenManager) HttpVerifier() func(http.Handler) http.Handler {
|
||||
// //jwt.WithHTTPClient()
|
||||
// return func(next http.Handler) http.Handler {
|
||||
// return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
// token, err := jwt.ParseRequest(req)
|
||||
// if err != nil {
|
||||
//
|
||||
// }
|
||||
//
|
||||
// next.ServeHTTP(w, req)
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// return jwtauth.Verifier(t.tokenAuth)
|
||||
//}
|
||||
// HttpVerifier returns a HTTP handler that verifies JWT and stores it into context
|
||||
func (tm *tokenManager) HttpVerifier() func(http.Handler) http.Handler {
|
||||
////jwt.WithHTTPClient()
|
||||
//return func(next http.Handler) http.Handler {
|
||||
// return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
// token, err := jwt.ParseRequest(req)
|
||||
// if err != nil {
|
||||
//
|
||||
// }
|
||||
//
|
||||
// next.ServeHTTP(w, req)
|
||||
// })
|
||||
//}
|
||||
|
||||
//// HttpAuthenticator converts JWT claims into identity and stores it into context
|
||||
//func (tm *tokenManager) HttpAuthenticator() func(http.Handler) http.Handler {
|
||||
// return func(next http.Handler) http.Handler {
|
||||
// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// ctx := r.Context()
|
||||
//
|
||||
// tkn, claims, err := jwtauth.FromContext(ctx)
|
||||
//
|
||||
// // When token is present, expect no errors and valid claims!
|
||||
// if tkn != nil {
|
||||
// if err != nil {
|
||||
// // But if token is present, the shouldn't be an error
|
||||
// api.Send(w, r, err)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// ctx = SetIdentityToContext(ctx, ClaimsToIdentity(claims))
|
||||
// ctx = context.WithValue(ctx, scopeCtxKey{}, claims["scope"])
|
||||
//
|
||||
// r = r.WithContext(ctx)
|
||||
// }
|
||||
//
|
||||
// next.ServeHTTP(w, r)
|
||||
// })
|
||||
// }
|
||||
//}
|
||||
return jwtauth.Verifier(jwtauth.New(tm.signAlgo.String(), tm.signKey, nil))
|
||||
}
|
||||
|
||||
// HttpAuthenticator converts JWT claims into identity and stores it into context
|
||||
func (tm *tokenManager) HttpAuthenticator() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
tkn, _, err := jwtauth.FromContext(ctx)
|
||||
|
||||
// When token is present, expect no errors and valid claims!
|
||||
if tkn != nil {
|
||||
if err != nil {
|
||||
// But if token is present, there shouldn't be an error
|
||||
api.Send(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx = SetIdentityToContext(ctx, IdentityFromToken(tkn))
|
||||
r = r.WithContext(ctx)
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Generates JWT and stores alongside with client-confirmation entry,
|
||||
func (tm *tokenManager) Generate(ctx context.Context, i Identifiable, clientID uint64, scope ...string) (token []byte, err error) {
|
||||
|
||||
+25
-18
@@ -4,6 +4,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/pkg/errors"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/go-chi/jwtauth"
|
||||
)
|
||||
|
||||
@@ -16,30 +17,36 @@ func AccessTokenCheck(scope ...string) func(http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var ctx = r.Context()
|
||||
|
||||
jwtauth.Authenticator()
|
||||
token, _, err := jwtauth.FromContext(ctx)
|
||||
spew.Dump(token, err)
|
||||
|
||||
// retrieve token and claims from context
|
||||
tkn, _, err := jwtauth.FromContext(ctx)
|
||||
if err != nil || !tkn.Valid {
|
||||
errors.ProperlyServeHTTP(w, r, ErrUnauthorized(), false)
|
||||
return
|
||||
}
|
||||
|
||||
// check valid scope
|
||||
for _, s := range scope {
|
||||
if !CheckScope(ctx.Value(scopeCtxKey{}), s) {
|
||||
errors.ProperlyServeHTTP(w, r, ErrUnauthorizedScope(), false)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// verify JWT from store
|
||||
_, err = DefaultJwtStore.LookupAuthOa2tokenByAccess(ctx, tkn.Raw)
|
||||
if err != nil {
|
||||
errors.ProperlyServeHTTP(w, r, ErrUnauthorized(), false)
|
||||
return
|
||||
}
|
||||
|
||||
if !CheckJwtScope(token, scope...) {
|
||||
errors.ProperlyServeHTTP(w, r, ErrUnauthorizedScope(), false)
|
||||
}
|
||||
|
||||
// @todo we need to check if token is in store!!
|
||||
// @todo we need to check if token is in store!!
|
||||
// @todo we need to check if token is in store!!
|
||||
// @todo we need to check if token is in store!!
|
||||
// @todo we need to check if token is in store!!
|
||||
// @todo we need to check if token is in store!!
|
||||
// @todo we need to check if token is in store!!
|
||||
// @todo we need to check if token is in store!!
|
||||
// @todo we need to check if token is in store!!
|
||||
// @todo we need to check if token is in store!!
|
||||
//
|
||||
//// verify JWT from store
|
||||
//_, err = DefaultJwtStore.LookupAuthOa2tokenByAccess(ctx, tkn.Raw)
|
||||
//if err != nil {
|
||||
// errors.ProperlyServeHTTP(w, r, ErrUnauthorized(), false)
|
||||
// return
|
||||
//}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
+23
-10
@@ -2,27 +2,40 @@ package auth
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type (
|
||||
scopeCtxKey struct{}
|
||||
"github.com/lestrrat-go/jwx/jwt"
|
||||
)
|
||||
|
||||
const (
|
||||
scopeDelimiter = " "
|
||||
)
|
||||
|
||||
// Checks if required scope is in claim
|
||||
// CheckJwtScope verifies if required scope is in claim
|
||||
// We're using interface{} and casting it if needed to simplify usage of the fn by directly
|
||||
// using it with map[string]interface{} claims type
|
||||
func CheckScope(claim interface{}, req string) bool {
|
||||
claimStr, ok := claim.(string)
|
||||
func CheckJwtScope(token jwt.Token, required ...string) bool {
|
||||
scopeClaimRaw, has := token.Get("scope")
|
||||
if !has {
|
||||
return false
|
||||
}
|
||||
|
||||
scopeClaim, ok := scopeClaimRaw.(string)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return strings.Contains(
|
||||
scopeDelimiter+claimStr+scopeDelimiter,
|
||||
scopeDelimiter+strings.TrimSpace(req)+scopeDelimiter,
|
||||
)
|
||||
return CheckScope(scopeClaim, required...)
|
||||
}
|
||||
|
||||
func CheckScope(scope string, required ...string) bool {
|
||||
scope = scopeDelimiter + strings.TrimSpace(scope) + scopeDelimiter
|
||||
|
||||
for _, req := range required {
|
||||
req = scopeDelimiter + strings.TrimSpace(req) + scopeDelimiter
|
||||
if strings.Contains(scope, req) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -296,7 +296,7 @@ func (s *session) authenticate(p *payloadAuth) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if scope, has := token.Get("scope"); !has || !auth.CheckScope(scope, "api") {
|
||||
if !auth.CheckJwtScope(token, "api") {
|
||||
return fmt.Errorf("client does not allow use of websockets (missing 'api' scope)")
|
||||
}
|
||||
|
||||
|
||||
@@ -15,9 +15,17 @@ func TestSession_procRawMessage(t *testing.T) {
|
||||
var (
|
||||
req = require.New(t)
|
||||
s = session{server: Server(nil, options.WebsocketOpt{})}
|
||||
jwtHandler, err = auth.JWT("secret", time.Minute)
|
||||
jwtHandler, err = auth.TokenManager("secret", time.Minute)
|
||||
|
||||
userID uint64 = 123
|
||||
token []byte
|
||||
|
||||
mockResponse = func(token []byte) (out []byte) {
|
||||
out = []byte(`{"@type": "credentials", "@value": {"accessToken": "`)
|
||||
out = append(out, token...)
|
||||
out = append(out, []byte(`"}}`)...)
|
||||
return
|
||||
}
|
||||
)
|
||||
|
||||
if testing.Verbose() {
|
||||
@@ -27,17 +35,17 @@ func TestSession_procRawMessage(t *testing.T) {
|
||||
}
|
||||
|
||||
req.NoError(err)
|
||||
s.server.accessToken = jwtHandler
|
||||
|
||||
jwt := jwtHandler.Encode(auth.Authenticated(userID, 456, 789))
|
||||
token, err = jwtHandler.Encode(auth.Authenticated(userID, 456, 789), 0, "api")
|
||||
req.NoError(err)
|
||||
|
||||
req.EqualError(s.procRawMessage([]byte("{}")), "unauthenticated session")
|
||||
req.Nil(s.identity)
|
||||
|
||||
req.EqualError(s.procRawMessage([]byte(`{"@type": "credentials", "@value": {"accessToken": ""}}`)), "unauthorized: token contains an invalid number of segments")
|
||||
req.EqualError(s.procRawMessage(mockResponse(nil)), "unauthorized: failed to parse token: EOF")
|
||||
req.Nil(s.identity)
|
||||
|
||||
req.NoError(s.procRawMessage([]byte(`{"@type": "credentials", "@value": {"accessToken": "` + jwt + `"}}`)))
|
||||
req.NoError(s.procRawMessage(mockResponse(token)))
|
||||
req.NotNil(s.identity)
|
||||
req.Equal(userID, s.identity.Identity())
|
||||
|
||||
@@ -45,15 +53,17 @@ func TestSession_procRawMessage(t *testing.T) {
|
||||
req.Equal(userID, s.identity.Identity())
|
||||
|
||||
// Repeat with the same user
|
||||
jwt = jwtHandler.Encode(auth.Authenticated(userID, 456, 789))
|
||||
token, err = jwtHandler.Encode(auth.Authenticated(userID, 456, 789), 0, "api")
|
||||
req.NoError(err)
|
||||
|
||||
req.NoError(s.procRawMessage([]byte(`{"@type": "credentials", "@value": {"accessToken": "` + jwt + `"}}`)))
|
||||
req.NoError(s.procRawMessage(mockResponse(token)))
|
||||
req.NotNil(s.identity)
|
||||
req.Equal(userID, s.identity.Identity())
|
||||
|
||||
// Try to authenticate on an existing authenticated session as a different user
|
||||
jwt = jwtHandler.Encode(auth.Authenticated(userID+1, 456, 789))
|
||||
token, err = jwtHandler.Encode(auth.Authenticated(userID+1, 456, 789), 0, "api")
|
||||
req.NoError(err)
|
||||
|
||||
req.EqualError(s.procRawMessage([]byte(`{"@type": "credentials", "@value": {"accessToken": "`+jwt+`"}}`)), "unauthorized: identity does not match")
|
||||
req.EqualError(s.procRawMessage(mockResponse(token)), "unauthorized: identity does not match")
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user