Fixing codebase after JWT lib switch

This commit is contained in:
Denis Arh
2022-01-11 15:35:05 +01:00
parent 72999ca692
commit 3ffa0ef8be
16 changed files with 253 additions and 208 deletions
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+1 -1
View File
@@ -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)")
}
+19 -9
View File
@@ -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")
}