diff --git a/app/boot_levels.go b/app/boot_levels.go index 03b8c8daf..91287f9c0 100644 --- a/app/boot_levels.go +++ b/app/boot_levels.go @@ -337,6 +337,8 @@ func (app *CortezaApp) InitServices(ctx context.Context) (err error) { app.WsServer = websocket.Server(app.Log, app.Opt.Websocket) + corredor.Service().SetAuthTokenMaker(app.jwt) + ctx = actionlog.RequestOriginToContext(ctx, actionlog.RequestOrigin_APP_Init) defer sentry.Recover() @@ -414,9 +416,6 @@ func (app *CortezaApp) InitServices(ctx context.Context) (err error) { return } - //@todo remove vv - //auth.SetJWTStore(app.Store) - corredor.Service().SetUserFinder(sysService.DefaultUser) corredor.Service().SetRoleFinder(sysService.DefaultRole) diff --git a/auth/handlers/handle_oauth2.go b/auth/handlers/handle_oauth2.go index c2f18ed8f..33b124e34 100644 --- a/auth/handlers/handle_oauth2.go +++ b/auth/handlers/handle_oauth2.go @@ -10,9 +10,11 @@ import ( "net/url" "strconv" "strings" - "time" + "github.com/go-chi/jwtauth" + oauth2errors "github.com/go-oauth2/oauth2/v4/errors" "github.com/lestrrat-go/jwx/jwk" + "github.com/lestrrat-go/jwx/jwt" "github.com/cortezaproject/corteza-server/auth/oauth2" "github.com/cortezaproject/corteza-server/auth/request" @@ -21,7 +23,6 @@ import ( systemService "github.com/cortezaproject/corteza-server/system/service" "github.com/cortezaproject/corteza-server/system/types" oauth2def "github.com/go-oauth2/oauth2/v4" - oauth2errors "github.com/go-oauth2/oauth2/v4/errors" "go.uber.org/zap" ) @@ -168,8 +169,27 @@ func (h AuthHandlers) oauth2Token(req *request.AuthReq) (err error) { return h.handleTokenRequest(req, client) } +// oauth2Info handler validates token and responds with decoded claims func (h AuthHandlers) oauth2Info(w http.ResponseWriter, r *http.Request) { - ti, err := h.OAuth2.ValidationBearerToken(r) + var ( + jt jwt.Token + claims map[string]interface{} + + // scope is intentionally left empty + scope = make([]string, 0) + ) + + err := func() (err error) { + if jt, claims, err = jwtauth.FromContext(r.Context()); err != nil { + return + } + + if err = auth.JWT().Validate(r.Context(), jt, scope...); err != nil { + return + } + + return nil + }() if err != nil { if errors.Is(err, context.Canceled) { @@ -203,20 +223,7 @@ func (h AuthHandlers) oauth2Info(w http.ResponseWriter, r *http.Request) { return } - data := map[string]interface{}{ - "active": true, - "scope": ti.GetScope(), - "client_id": ti.GetClientID(), - "exp": int64(ti.GetAccessCreateAt().Add(ti.GetAccessExpiresIn()).Sub(time.Now()).Seconds()), - "aud": ti.GetClientID(), - } - - SubSplit(ti, data) - if err = Profile(r.Context(), ti, data); err != nil { - h.Log.Error("failed to add profile data", zap.Error(err)) - } - - _ = json.NewEncoder(w).Encode(data) + _ = json.NewEncoder(w).Encode(claims) } // oauth2authorizeDefaultClient acts as a proxy for default client @@ -359,8 +366,6 @@ func (h AuthHandlers) loadRequestedClient(req *request.AuthReq) (client *types.A } func (h AuthHandlers) handleTokenRequest(req *request.AuthReq, client *types.AuthClient) error { - req.Status = -1 - var ( r = req.Request w = req.Response @@ -402,6 +407,14 @@ func (h AuthHandlers) handleTokenRequest(req *request.AuthReq, client *types.Aut return h.tokenError(w, err) } + var ( + user = req.AuthUser.User.Clone() + signed []byte + ) + + signed, err = auth.JWT().Sign(ti.GetAccess(), user, client.ID, strings.Split(ti.GetScope(), " ")...) + ti.SetAccess(string(signed)) + return token(w, h.OAuth2.GetTokenData(ti), nil) } diff --git a/auth/oauth2/access_token.go b/auth/oauth2/access_token.go deleted file mode 100644 index 223ce9561..000000000 --- a/auth/oauth2/access_token.go +++ /dev/null @@ -1,62 +0,0 @@ -package oauth2 - -//import ( -// "context" -// "strings" -// -// "github.com/cortezaproject/corteza-server/pkg/auth" -// "github.com/cortezaproject/corteza-server/pkg/payload" -// "github.com/cortezaproject/corteza-server/pkg/rand" -// "github.com/go-oauth2/oauth2/v4" -// "github.com/spf13/cast" -//) -// -//// JWTAccessGenerate generate the jwt access token -//type ( -// tokenGenerator interface { -// Generate(ctx context.Context, i auth.Identifiable, clientID uint64, scope ...string) (token []byte, err error) -// } -// -// JWTAccessGenerate struct { -// tm tokenGenerator -// } -//) -// -//// NewJWTAccessGenerate create to generate the jwt access token instance -//// -//// @todo move this to pkg/auth (??) so it can be re-used -//func NewJWTAccessGenerate(tg tokenGenerator) *JWTAccessGenerate { -// return &JWTAccessGenerate{tg} -//} -// -//// Token based on the UUID generated token -//func (a *JWTAccessGenerate) Token(ctx context.Context, data *oauth2.GenerateBasic, isGenRefresh bool) (_ string, refresh string, err error) { -// var ( -// user auth.Identifiable -// rawToken []byte -// ) -// -// { -// // extract user ID and roles from a space-delimited list of IDs stored in userID -// userIdWithRoles := strings.Split(data.TokenInfo.GetUserID(), " ") -// if len(userIdWithRoles) == 1 { -// user = auth.Authenticated(cast.ToUint64(userIdWithRoles[0])) -// } else { -// user = auth.Authenticated( -// cast.ToUint64(userIdWithRoles[0]), -// payload.ParseUint64s(userIdWithRoles)..., -// ) -// } -// } -// -// rawToken, err = a.tm.Generate(ctx, user, cast.ToUint64(data.Client.GetID()), data.TokenInfo.GetScope()) -// if err != nil { -// return -// } -// -// if isGenRefresh { -// refresh = string(rand.Bytes(48)) -// } -// -// return string(rawToken), refresh, nil -//} diff --git a/auth/oauth2/user_authorizer.go b/auth/oauth2/user_authorizer.go index e34b60bb6..968e939f5 100644 --- a/auth/oauth2/user_authorizer.go +++ b/auth/oauth2/user_authorizer.go @@ -12,7 +12,6 @@ import ( func NewUserAuthorizer(sm *request.SessionManager, loginURL, clientAuthURL string) server.UserAuthorizationHandler { return func(w http.ResponseWriter, r *http.Request) (identity string, err error) { - var ( ses = sm.Get(r) au = request.GetAuthUser(ses) @@ -40,7 +39,7 @@ func NewUserAuthorizer(sm *request.SessionManager, loginURL, clientAuthURL strin } } - var roles = request.GetRoleMemberships(ses) + roles := au.User.Roles() if client.Security != nil { // filter user's roles with client security settings roles = internalAuth.ApplyRoleSecurity( diff --git a/auth/request/session.go b/auth/request/session.go index 979f5c53e..de6e65c65 100644 --- a/auth/request/session.go +++ b/auth/request/session.go @@ -1,9 +1,10 @@ package request import ( + "net/url" + "github.com/cortezaproject/corteza-server/system/types" "github.com/gorilla/sessions" - "net/url" ) const ( @@ -23,11 +24,16 @@ func GetAuthUser(ses *sessions.Session) *authUser { return nil } - return val.(*authUser) + au := val.(*authUser) + if au.User != nil { + au.User.SetRoles(getRoleMemberships(ses)...) + } + + return au } // GetRoleMemberships is wrapper to get value from session -func GetRoleMemberships(ses *sessions.Session) []uint64 { +func getRoleMemberships(ses *sessions.Session) []uint64 { val, has := ses.Values[keyRoles] if !has { return nil @@ -36,15 +42,6 @@ func GetRoleMemberships(ses *sessions.Session) []uint64 { return val.([]uint64) } -// SetRoleMemberships is a session value setting wrapper for RoleMemberships -func SetRoleMemberships(ses *sessions.Session, val []uint64) { - if val != nil { - ses.Values[keyRoles] = val - } else { - delete(ses.Values, keyRoles) - } -} - // GetOAuth2AuthParams is wrapper to get value from session func GetOAuth2AuthParams(ses *sessions.Session) url.Values { val, has := ses.Values[keyOAuth2AuthParams] diff --git a/pkg/auth/jwt.go b/pkg/auth/jwt.go index 179e845f0..d9719e0f7 100644 --- a/pkg/auth/jwt.go +++ b/pkg/auth/jwt.go @@ -11,7 +11,6 @@ import ( "github.com/cortezaproject/corteza-server/pkg/errors" "github.com/cortezaproject/corteza-server/pkg/logger" "github.com/cortezaproject/corteza-server/pkg/payload" - "github.com/cortezaproject/corteza-server/system/types" "github.com/go-chi/jwtauth" "github.com/go-oauth2/oauth2/v4" "github.com/lestrrat-go/jwx/jwa" @@ -22,12 +21,9 @@ import ( ) type ( - signer interface { - Sign(accessToken string, identity Identifiable, clientID uint64, scope ...string) (signed []byte, err error) - } - MiddlewareValidator interface { HttpValidator(scope ...string) func(http.Handler) http.Handler + Generate(ctx context.Context, i Identifiable, clientID uint64, scope ...string) (signed []byte, err error) } oauth2manager interface { @@ -48,27 +44,10 @@ type ( issuerClaim string } - - // @todo remove - tokenStore interface { - CreateAuthOa2token(ctx context.Context, rr ...*types.AuthOa2token) error - UpsertAuthConfirmedClient(ctx context.Context, rr ...*types.AuthConfirmedClient) error - } - - // @todo remove - //tokenLookup interface { - // LookupAuthOa2tokenByID(ctx context.Context, id uint64) (*types.AuthOa2token, error) - //} - // - //tokenStoreWithLookup interface { - // tokenStore - // tokenLookup - //} ) var ( defaultJWTManager *jwtManager - //DefaultJwtStore tokenStoreWithLookup ) // JWT returns d @@ -105,31 +84,13 @@ func NewJWTManager(oa2m oauth2manager, algo jwa.SignatureAlgorithm, secret strin return } -//// @todo remove -////// SetJWTStore set store for JWT -////// @todo find better way to initiate store, -////// it mainly used for generating and storing accessToken for impersonate and corredor, Ref: j.Generate() -////func SetJWTStore(store tokenStoreWithLookup) { -//// DefaultJwtStore = store -////} -// -//// Authenticate the token from the given string and return parsed token or error -//func (m *jwtManager) Authenticate(s string) (pToken jwt.Token, err error) { -// if pToken, err = jwt.Parse([]byte(s), jwt.WithVerify(m.signAlgo, m.signKey)); err != nil { -// return -// } -// -// if err = jwt.Validate(pToken); err != nil { -// return -// } -// -// return -//} - // Sign takes security information and returns signed JWT // -// Access token is expected to be issued by OAuth2 token manager -// without it, we can only do static (JWT itself) validation +// Access token is expected to be issued by OAuth2 token manager and we want to +// transport access-token one of the JWT claims (JWT ID!). +// +// This way we can perform static checks (origin, validity, expiration) +// before doing any storage lookups. //f // Identity holds user ID and all roles that go into this security context // Client ID represents the auth client that was used @@ -190,6 +151,13 @@ func (m *jwtManager) Sign(accessToken string, identity Identifiable, clientID ui return signed, nil } +// Generate new access-token and JWT +// +// Why so much effort and not just return the access token? +// We want to transport access-token one of the JWT claims (JWT ID!). +// +// This way we can perform static checks (origin, validity, expiration) +// before doing any storage lookups. func (m *jwtManager) Generate(ctx context.Context, i Identifiable, clientID uint64, scope ...string) (signed []byte, err error) { var ( ti oauth2.TokenInfo @@ -199,7 +167,7 @@ func (m *jwtManager) Generate(ctx context.Context, i Identifiable, clientID uint ClientID: strconv.FormatUint(clientID, 10), UserID: i.String(), Scope: strings.Join(scope, " "), - Refresh: "cli?", + Refresh: "??????????", AccessTokenExp: m.expiry, }) @@ -207,42 +175,7 @@ func (m *jwtManager) Generate(ctx context.Context, i Identifiable, clientID uint return } - return m.Sign(ti.GetAccess(), i, 0, scope...) -} - -func ValidateContext(ctx context.Context, oa2m oauth2manager, scope ...string) (err error) { - var ( - token jwt.Token - ) - - if token, _, err = jwtauth.FromContext(ctx); err != nil { - return ErrUnauthorized() - } - - return Validate(ctx, token, oa2m, scope...) -} - -func Validate(ctx context.Context, token jwt.Token, oa2m oauth2manager, scope ...string) (err error) { - if !CheckJwtScope(token, scope...) { - return ErrUnauthorizedScope() - } - - // Extract the JWT id from the token (string) and convert it to uint64 - // to be compatible with the lookup function - if len(token.JwtID()) < 10 { - return ErrMalformedToken("missing or malformed JWT ID") - } - - // @todo we could use a simple caching mechanism here - // 1. if lookup is successful, add a JWT ID to the list - // 2. add short exp time (that should not last longer than token's exp time) - // 3. check against the list first; if JWT ID is not present there check in storage - // - if _, err = oa2m.LoadAccessToken(ctx, token.JwtID()); err != nil { - return ErrUnauthorized() - } - - return nil + return m.Sign(ti.GetAccess(), i, clientID, scope...) } // HttpVerifier http middleware handler will verify a JWT string from a http request. @@ -271,85 +204,59 @@ func (m *jwtManager) HttpValidator(scope ...string) func(http.Handler) http.Hand } } -//// HttpAuthenticator converts JWT claims into identity and stores it into context -//func (m *jwtManager) HttpAuthenticator(next http.Handler) http.Handler { -// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { -// ctx := r.Context() -// -// tkn, _, err := jwtauth.FromContext(ctx) -// -// // Requests w/o token should not yield an error -// // there are parts of the system that can be access without it -// // and/or handle such situation internally -// if err != nil && !errors.Is(err, jwtauth.ErrNoTokenFound) { -// api.Send(w, r, err) -// return -// } -// -// // If token is present extract identity -// if tkn != nil { -// ctx = SetIdentityToContext(ctx, IdentityFromToken(tkn)) -// r = r.WithContext(ctx) -// -// // @todo verify JWT ID (access-token!! -// tkn.JwtID() -// -// } -// -// next.ServeHTTP(w, r) -// }) -//} +func (m *jwtManager) ValidateContext(ctx context.Context, scope ...string) error { + return ValidateContext(ctx, m.oa2m, scope...) +} +func (m *jwtManager) Validate(ctx context.Context, token jwt.Token, scope ...string) error { + return Validate(ctx, token, m.oa2m, scope...) +} + +// ValidateContext gets JWT & claims from context // -//// Generate makes a new token and stores it in the database -//func (tm *tokenManager) Generate(ctx context.Context, i Identifiable, clientID uint64, scope ...string) (token []byte, err error) { -// var ( -// // eti = GetExtraReqInfoFromContext(ctx) -// // oa2t = &types.AuthOa2token{ -// // ID: id.Next(), -// // CreatedAt: time.Now().Round(time.Second), -// // RemoteAddr: eti.RemoteAddr, -// // UserAgent: eti.UserAgent, -// // ClientID: clientID, -// // } -// // -// // acc = &types.AuthConfirmedClient{ -// // ConfirmedAt: oa2t.CreatedAt, -// // ClientID: clientID, -// // } -// oa2t *types.AuthOa2token -// acc *types.AuthConfirmedClient +// It's chi middleware that puts it there +func ValidateContext(ctx context.Context, oa2m oauth2manager, scope ...string) (err error) { + var ( + token jwt.Token + ) + + if token, _, err = jwtauth.FromContext(ctx); err != nil { + return ErrUnauthorized() + } + + return Validate(ctx, token, oa2m, scope...) +} + +// Validate performs token validation // -// jwtID = id.Next() -// ) +// Steps: +// - check scope in the JWT +// - check if JWT ID is set (where the access-token string is stored) +// - check if access-token exists in the DB // -// if oa2t, acc, err = MakeAuthStructs(ctx, jwtID, i.Identity(), clientID, nil, tm.expiry); err != nil { -// return -// } // -// if token, err = tm.make(jwtID, i, clientID, scope...); err != nil { -// return nil, err -// } -// -// oa2t.Access = string(token) -// -// // use the same expiration as on token -// //oa2t.ExpiresAt = oa2t.CreatedAt.Add(tm.expiry) -// -// //if oa2t.Data, err = json.Marshal(oa2t); err != nil { -// // return -// //} -// -// //if oa2t.UserID, _ = ExtractFromSubClaim(i.String()); oa2t.UserID == 0 { -// // // UserID stores collection of IDs: user's ID and set of all roles' user is member of -// // return nil, fmt.Errorf("could not parse user ID from token") -// //} -// // -// //// copy user id to auth client confirmation -// //acc.UserID = oa2t.UserID -// -// return token, StoreAuthToken(ctx, DefaultJwtStore, oa2t, acc) -//} +func Validate(ctx context.Context, token jwt.Token, oa2m oauth2manager, scope ...string) (err error) { + if len(scope) > 0 && !CheckJwtScope(token, scope...) { + return ErrUnauthorizedScope() + } + + // Extract the JWT id from the token (string) and convert it to uint64 + // to be compatible with the lookup function + if len(token.JwtID()) < 10 { + return ErrMalformedToken("missing or malformed JWT ID") + } + + // @todo we could use a simple caching mechanism here + // 1. if lookup is successful, add a JWT ID to the list + // 2. add short exp time (that should not last longer than token's exp time) + // 3. check against the list first; if JWT ID is not present there check in storage + // + if _, err = oa2m.LoadAccessToken(ctx, token.JwtID()); err != nil { + return ErrUnauthorized() + } + + return nil +} // IdentityFromToken decodes sub & roles claims into identity func IdentityFromToken(token jwt.Token) *identity { diff --git a/pkg/corredor/service.go b/pkg/corredor/service.go index a41bf7a28..804f0639d 100644 --- a/pkg/corredor/service.go +++ b/pkg/corredor/service.go @@ -104,7 +104,7 @@ type ( } authTokenMaker interface { - Generate(ctx context.Context, i auth.Identifiable, clientID uint64, scope ...string) (token []byte, err error) + Generate(ctx context.Context, i auth.Identifiable, clientID uint64, scope ...string) (signed []byte, err error) } ) @@ -735,7 +735,7 @@ func (svc service) exec(ctx context.Context, script string, runAs string, args S } // Generate and save the token - token, err = svc.authTokenMaker.Generate(ctx, definer, 0) + token, err = svc.authTokenMaker.Generate(ctx, definer, 0, "profile", "api") if err != nil { return } diff --git a/pkg/websocket/session.go b/pkg/websocket/session.go index ed073b080..2e647e0a5 100644 --- a/pkg/websocket/session.go +++ b/pkg/websocket/session.go @@ -26,6 +26,10 @@ var ( ) type ( + jwtValidator interface { + Validate(ctx context.Context, token jwt.Token, scope ...string) error + } + session struct { id uint64 once sync.Once @@ -45,6 +49,8 @@ type ( identity auth.Identifiable + jv jwtValidator + server *server } ) @@ -56,6 +62,7 @@ func Session(ctx context.Context, ws *server, conn *websocket.Conn) *session { config: ws.config, send: make(chan []byte, 512), stop: make(chan []byte, 1), + jv: auth.JWT(), server: ws, } @@ -292,14 +299,10 @@ func (s *session) authenticate(p *payloadAuth) error { return err } - if err = jwt.Validate(token); err != nil { + if err = s.jv.Validate(s.ctx, token, "api"); err != nil { return err } - if !auth.CheckJwtScope(token, "api") { - return fmt.Errorf("client does not allow use of websockets (missing 'api' scope)") - } - // Get identity using JWT claims identity := auth.IdentityFromToken(token) diff --git a/pkg/websocket/session_test.go b/pkg/websocket/session_test.go index 8a0d475f8..676c8ff96 100644 --- a/pkg/websocket/session_test.go +++ b/pkg/websocket/session_test.go @@ -1,6 +1,7 @@ package websocket import ( + "context" "testing" "time" @@ -8,14 +9,26 @@ import ( "github.com/cortezaproject/corteza-server/pkg/logger" "github.com/cortezaproject/corteza-server/pkg/options" "github.com/lestrrat-go/jwx/jwa" + "github.com/lestrrat-go/jwx/jwt" "github.com/stretchr/testify/require" "go.uber.org/zap" ) +type ( + dummyJwtValidator struct{ err error } +) + +func (d *dummyJwtValidator) Validate(_ context.Context, _ jwt.Token, _ ...string) error { + return d.err +} + func TestSession_procRawMessage(t *testing.T) { var ( req = require.New(t) - s = session{server: Server(nil, options.WebsocketOpt{})} + s = session{ + server: Server(nil, options.WebsocketOpt{}), + jv: &dummyJwtValidator{}, + } userID uint64 = 123 token []byte @@ -68,6 +81,4 @@ func TestSession_procRawMessage(t *testing.T) { req.NoError(err) req.EqualError(s.procRawMessage(mockResponse(token)), "unauthorized: identity does not match") - - t.Error("are we actually checking if access token exists?") }