Exemple #1
2
// NewToken creates new JWT token for the gien username. It embedds the given
// public key as kontrolKey and signs the token with the private one.
func NewToken(username, private, public string) *jwt.Token {
	tknID := uuid.NewV4()

	hostname, err := os.Hostname()
	if err != nil {
		panic(err)
	}

	if username == "" {
		username = "******"
	}

	if testuser := os.Getenv("TESTKEY_USERNAME"); testuser != "" {
		username = testuser
	}

	claims := &kitekey.KiteClaims{
		StandardClaims: jwt.StandardClaims{
			Issuer:   "testuser",
			Subject:  username,
			Audience: hostname,
			IssuedAt: time.Now().UTC().Unix(),
			Id:       tknID.String(),
		},
		KontrolKey: public,
		KontrolURL: "http://localhost:4000/kite",
	}

	token := jwt.NewWithClaims(jwt.GetSigningMethod("RS256"), claims)

	rsaPrivate, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(private))
	if err != nil {
		panic(err)
	}

	token.Raw, err = token.SignedString(rsaPrivate)
	if err != nil {
		panic(err)
	}

	// verify the token
	_, err = jwt.ParseWithClaims(token.Raw, claims, func(*jwt.Token) (interface{}, error) {
		return jwt.ParseRSAPublicKeyFromPEM([]byte(public))
	})

	if err != nil {
		panic(err)
	}

	token.Valid = true
	return token

}
Exemple #2
1
func (k *Kontrol) HandleGetKey(r *kite.Request) (interface{}, error) {
	// Only accept requests with kiteKey because we need this info
	// for checking if the key is valid and needs to be regenerated
	if r.Auth.Type != "kiteKey" {
		return nil, fmt.Errorf("Unexpected authentication type: %s", r.Auth.Type)
	}

	ex := &kitekey.Extractor{
		Claims: &kitekey.KiteClaims{},
	}

	if _, err := jwt.ParseWithClaims(r.Auth.Key, ex.Claims, ex.Extract); err != nil {
		return nil, err
	}

	if ex.Claims.KontrolKey == "" {
		return nil, errors.New("public key is not passed")
	}

	switch k.keyPair.IsValid(ex.Claims.KontrolKey) {
	case nil:
		// everything is ok, just return the old one
		return ex.Claims.KontrolKey, nil
	case ErrKeyDeleted:
		// client is using old key, update to current
		if kp, err := k.KeyPair(); err == nil {
			return kp.Public, nil
		}
	}

	keyPair, err := k.pickKey(r)
	if err != nil {
		return nil, err
	}

	return keyPair.Public, nil
}
Exemple #3
0
// Auth is a gin middleware that checks for session cookie
func Auth() gin.HandlerFunc {
	return func(c *gin.Context) {

		// get the jwt cookie from the request
		cookie, err := c.Request.Cookie(u.CookieName)
		if err != nil {
			c.Error(err).SetMeta("middleware.Auth.Cookie")
			c.Redirect(http.StatusFound, "/admin/login")
			c.Abort()
			return
		}

		token, err := jwt.ParseWithClaims(cookie.Value, &u.TokenClaims{}, func(token *jwt.Token) (interface{}, error) {
			return u.ValidateToken(token)
		})
		// the client side should delete any saved JWT tokens on unauth error
		if err != nil || !token.Valid {
			// delete the cookie
			http.SetCookie(c.Writer, u.DeleteCookie())
			c.Error(err).SetMeta("middleware.Auth.ParseWithClaims")
			c.Redirect(http.StatusFound, "/admin/login")
			c.Abort()
			return
		}

		// set user data for controllers
		c.Set("authenticated", true)

		c.Next()

	}
}
Exemple #4
0
// Auth is a gin middleware that checks for session cookie and
// handles permissions
func Auth(authenticated bool) gin.HandlerFunc {
	return func(c *gin.Context) {

		// error if theres no secret set
		if Secret == "" {
			c.JSON(e.ErrorMessage(e.ErrInternalError))
			c.Error(e.ErrNoSecret).SetMeta("auth.Auth")
			c.Abort()
			return
		}

		// set default anonymous user
		user := DefaultUser()

		// try and get the jwt cookie from the request
		cookie, err := c.Request.Cookie(CookieName)
		// parse jwt token if its there
		if err != http.ErrNoCookie {
			token, err := jwt.ParseWithClaims(cookie.Value, &TokenClaims{}, func(token *jwt.Token) (interface{}, error) {
				return validateToken(token, &user)
			})
			// if theres some jwt error other than no token in request or the token is
			// invalid then return unauth
			// the client side should delete any saved JWT tokens on unauth error
			if err != nil || !token.Valid {
				// delete the cookie
				http.SetCookie(c.Writer, DeleteCookie())
				c.JSON(e.ErrorMessage(e.ErrUnauthorized))
				c.Error(err).SetMeta("user.Auth")
				c.Abort()
				return
			}
		}

		// check if user needed to be authenticated
		// this needs to be like this for routes that dont need auth
		// if we just check equality then logged in users wont be able
		// to view anon pages ;P
		if authenticated && !user.IsAuthenticated {
			c.JSON(e.ErrorMessage(e.ErrForbidden))
			c.Error(e.ErrForbidden).SetMeta("user.Auth")
			c.Abort()
			return
		}

		// set user data for controllers
		c.Set("userdata", user)

		c.Next()

	}

}
Exemple #5
0
func parseToken(tokenString string, publicKey *rsa.PublicKey) (*keycloakTokenClaims, error) {
	token, err := jwt.ParseWithClaims(tokenString, &keycloakTokenClaims{}, func(t *jwt.Token) (interface{}, error) {
		return publicKey, nil
	})
	if err != nil {
		return nil, err
	}
	claims := token.Claims.(*keycloakTokenClaims)
	if token.Valid {
		return claims, nil
	}
	return nil, errs.WithStack(errors.New("token is not valid"))
}
Exemple #6
0
// KeyPair looks up a key pair that was used to sign Kontrol's kite key.
//
// The value is cached on first call of the function.
func (k *Kontrol) KeyPair() (pair *KeyPair, err error) {
	if k.selfKeyPair != nil {
		return k.selfKeyPair, nil
	}

	kiteKey := k.Kite.KiteKey()

	if kiteKey == "" || len(k.lastPublic) == 0 {
		return nil, errNoSelfKeyPair
	}

	keyIndex := -1

	me := new(multiError)

	for i := range k.lastPublic {
		ri := len(k.lastPublic) - i - 1

		keyFn := func(token *jwt.Token) (interface{}, error) {
			if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
				return nil, errors.New("invalid signing method")
			}

			return jwt.ParseRSAPublicKeyFromPEM([]byte(k.lastPublic[ri]))
		}

		if _, err := jwt.ParseWithClaims(kiteKey, &kitekey.KiteClaims{}, keyFn); err != nil {
			me.err = append(me.err, err)
			continue
		}

		keyIndex = ri
		break
	}

	if keyIndex == -1 {
		return nil, fmt.Errorf("no matching self key pair found: %s", me)
	}

	k.selfKeyPair = &KeyPair{
		ID:      k.lastIDs[keyIndex],
		Public:  k.lastPublic[keyIndex],
		Private: k.lastPrivate[keyIndex],
	}

	return k.selfKeyPair, nil
}
Exemple #7
0
// parse the token string and set
func (t *TokenRenewer) parse(tokenString string) error {
	claims := &kitekey.KiteClaims{}

	_, err := jwt.ParseWithClaims(tokenString, claims, t.localKite.RSAKey)
	if err != nil {
		valErr, ok := err.(*jwt.ValidationError)
		if !ok {
			return err
		}

		// do noy return for ValidationErrorSignatureValid. This is because we
		// might asked for a kite who's public Key is different what we have.
		// We still should be able to send them requests.
		if (valErr.Errors & jwt.ValidationErrorSignatureInvalid) == 0 {
			return fmt.Errorf("Cannot parse token: %s", err)
		}
	}

	t.validUntil = time.Unix(claims.ExpiresAt, 0).UTC()
	return nil
}
Exemple #8
0
func (k *Kite) updateAuth(reg *protocol.RegisterResult) {
	k.configMu.Lock()
	defer k.configMu.Unlock()

	switch {
	case reg.KiteKey != "":
		k.Config.KiteKey = reg.KiteKey

		ex := &kitekey.Extractor{
			Claims: &kitekey.KiteClaims{},
		}

		if _, err := jwt.ParseWithClaims(reg.KiteKey, ex.Claims, ex.Extract); err != nil {
			k.Log.Error("auth update: unable to extract kontrol key: %s", err)

			break
		}

		if ex.Claims.KontrolKey != "" {
			reg.PublicKey = ex.Claims.KontrolKey
		}
	}

	// we also received a new public key (means the old one was invalidated).
	// Use it now.
	if reg.PublicKey != "" {
		k.Config.KontrolKey = reg.PublicKey

		key, err := jwt.ParseRSAPublicKeyFromPEM([]byte(reg.PublicKey))
		if err != nil {
			k.Log.Error("auth update: unable to update kontrol key: %s", err)

			return
		}

		k.kontrolKey = key
	}
}
func TestCapability(t *testing.T) {
	t.Parallel()
	cap := NewCapability("AC123", "123")
	cap.AllowClientIncoming("client-name")
	tok, err := cap.GenerateToken(time.Hour)
	if err != nil {
		t.Fatal(err)
	}
	fmt.Println(tok)
	cc := new(customClaim)
	_, err = jwt.ParseWithClaims(tok, cc, func(tkn *jwt.Token) (interface{}, error) {
		return []byte("123"), nil
	})
	if err != nil {
		t.Fatal(err)
	}
	if cc.StandardClaims.Issuer != "AC123" {
		t.Errorf("bad Issuer")
	}
	if cc.Scope != "scope:client:incoming?clientName=client-name" {
		t.Errorf("bad Scope")
	}
}
Exemple #10
0
func (k *Konfig) buildKiteConfig() *konfig.Config {
	if k.KiteKey != "" {
		tok, err := jwt.ParseWithClaims(k.KiteKey, &kitekey.KiteClaims{}, kitekey.GetKontrolKey)
		if err == nil {
			cfg := &konfig.Config{}

			if err = cfg.ReadToken(tok); err == nil {
				return cfg
			}
		}
	}

	if k.KiteKeyFile != "" {
		if cfg, err := konfig.NewFromKiteKey(k.KiteKeyFile); err == nil {
			return cfg
		}
	}

	if cfg, err := konfig.Get(); err == nil {
		return cfg
	}

	return konfig.New()
}
Exemple #11
0
// Midware handles token authentication for external authentication
// sources.
func Midware(publicKeyBase64Str string, config MidwareOpts) (web.Middleware, error) {
	publicKey, err := auth.DecodePublicKey(publicKeyBase64Str)
	if err != nil {
		log.Error("startup", "auth : Midware", err, "Can not decode the public key base64 encoding")
		return nil, err
	}

	// Create the middleware to actually return.
	m := func(h web.Handler) web.Handler {

		// Create the handler that we should return as a part of the middleware
		// chain.
		f := func(c *web.Context) error {
			log.Dev(c.SessionID, "auth : Midware", "Started")

			// Extract the token from the Authorization header provided on the request.
			tokenString := c.Request.Header.Get("Authorization")

			// In the event that the request does not have a header key for the
			// Authorization header, and we are allowed to check the query string, then
			// we need to try and access it from the URL query parameters.
			if tokenString == "" && config.AllowQueryString {
				tokenString = c.Request.URL.Query().Get("access_token")
			}

			if tokenString == "" {
				log.Error(c.SessionID, "auth : Midware", ErrInvalidToken, "No token on request")
				return web.ErrNotAuthorized
			}

			// This describes the key validation function to provide the certificate
			// to validate the signature on the passed in JWT.
			keyValidation := func(token *jwt.Token) (interface{}, error) {

				// Don't forget to validate the alg is what you expect.
				if _, ok := token.Method.(*jwt.SigningMethodECDSA); !ok {
					return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
				}

				// Return with the public key that was provided in the config.
				return publicKey, nil
			}

			// Here we actually parse/verify the signature on the JWT and extract the
			// claims.
			token, err := jwt.ParseWithClaims(tokenString, &jwt.MapClaims{}, keyValidation)
			if err != nil {
				log.Error(c.SessionID, "auth : Midware", err, "Token could not be parsed")
				return web.ErrNotAuthorized
			}

			// Return with an error if the token is not valid.
			if !token.Valid {
				log.Error(c.SessionID, "auth : Midware", ErrInvalidToken, "Token not valid")
				return web.ErrNotAuthorized
			}

			// Ensure that the claims that are inside the token are indeed the MapClaims
			// that we expect.
			claims, ok := token.Claims.(*jwt.MapClaims)
			if !ok {
				log.Error(c.SessionID, "auth : Midware", ErrInvalidToken, "Claims not valid")
				return web.ErrNotAuthorized
			}

			// Validate that all the parameters we expect are correct, noteably, the
			// expiry date, and not before claims should be verified.
			if err := claims.Valid(); err != nil {
				log.Error(c.SessionID, "auth : Midware", err, "Claims not valid")
				return web.ErrNotAuthorized
			}

			// Add the claims to the context.
			c.Ctx["claims"] = claims

			log.Dev(c.SessionID, "auth : Midware", "Completed : Valid")
			return h(c)
		}

		return f
	}

	return m, nil
}
func TestJWT(t *testing.T) {
	t.Parallel()

	accTkn := New(ACC_SID, API_KEY, API_SECRET, IDENTITY, time.Hour)
	accTkn.NotBefore = time.Now()
	convGrant := NewConversationsGrant(APP_SID)

	accTkn.AddGrant(convGrant)
	jwtString, err := accTkn.JWT()

	if err != nil {
		t.Error("Unexpected error when generating the token", err)
	}
	if jwtString == "" {
		t.Error("token returned is empty")
	}

	token, err := jwt.ParseWithClaims(jwtString, &myCustomClaims{}, func(tkn *jwt.Token) (interface{}, error) {
		return []byte(API_SECRET), nil
	})
	if err != nil {
		t.Error("Unexpected error when generating the token", err)
	}

	claims := token.Claims.(*myCustomClaims)

	if &claims.StandardClaims == nil {
		t.Error("Claim doesn't conaint a standard claims struct")
	}

	if claims.StandardClaims.ExpiresAt == 0 {
		t.Error("ExpiredAt is not set")
	}

	if claims.StandardClaims.Id == "" {
		t.Error("ID is not set")
	}

	if claims.StandardClaims.IssuedAt == 0 {
		t.Error("IssuedAt is not set")
	}

	if claims.StandardClaims.NotBefore == 0 {
		t.Error("NotBefore is not set")
	}

	if claims.StandardClaims.Issuer != API_KEY {
		t.Errorf("Issuer expected to be: %s, got %s\n", API_KEY, claims.StandardClaims.Issuer)
	}

	if claims.StandardClaims.Subject != ACC_SID {
		t.Errorf("Subject expected to be: %s, got %s\n", ACC_SID, claims.StandardClaims.Subject)
	}

	if claims.Grants == nil {
		t.Error("Expected Grants to exist")
	}

	if claims.Grants["identity"] != IDENTITY {
		t.Errorf("Grants identity expected to be %s, got %s\n", IDENTITY, claims.Grants["identity"])
	}
}
Exemple #13
0
func (k *Kontrol) HandleRegisterHTTP(rw http.ResponseWriter, req *http.Request) {
	var args protocol.RegisterArgs

	if err := json.NewDecoder(req.Body).Decode(&args); err != nil {
		errMsg := fmt.Errorf("wrong register input: '%s'", err)
		http.Error(rw, jsonError(errMsg), http.StatusBadRequest)
		return
	}

	k.log.Info("Register (via HTTP) request from: %s", args.Kite)

	// Only accept requests with kiteKey, because that's the only way one can
	// register itself to kontrol.
	if args.Auth.Type != "kiteKey" {
		err := fmt.Errorf("unexpected authentication type: %s", args.Auth.Type)
		http.Error(rw, jsonError(err), http.StatusBadRequest)
		return
	}

	// empty url is useless for us
	if args.URL == "" {
		err := errors.New("empty URL")
		http.Error(rw, jsonError(err), http.StatusBadRequest)
		return
	}

	// decode and authenticated the token key. We'll get the authenticated
	// username
	username, err := k.Kite.AuthenticateSimpleKiteKey(args.Auth.Key)
	if err != nil {
		http.Error(rw, jsonError(err), http.StatusUnauthorized)
		return
	}
	args.Kite.Username = username

	ex := &kitekey.Extractor{
		Claims: &kitekey.KiteClaims{},
	}

	t, err := jwt.ParseWithClaims(args.Auth.Key, ex.Claims, ex.Extract)
	if err != nil {
		http.Error(rw, jsonError(err), http.StatusBadRequest)
		return
	}

	var keyPair *KeyPair
	resp := &protocol.RegisterResult{
		URL:               args.URL,
		HeartbeatInterval: int64(HeartbeatInterval / time.Second),
	}

	// check if the key is valid and is stored in the key pair storage, if not
	// found we don't allow to register anyone.
	r := &kite.Request{
		Username: username,
		Auth: &kite.Auth{
			Type: args.Auth.Type,
			Key:  args.Auth.Key,
		},
	}

	keyPair, resp.KiteKey, err = k.getOrUpdateKeyPub(ex.Claims.KontrolKey, t, r)
	if err != nil {
		http.Error(rw, jsonError(err), http.StatusBadRequest)
		return
	}

	if ex.Claims.KontrolKey != keyPair.Public {
		// NOTE(rjeczalik): updates public key for old kites, new kites
		// expect kite key to be updated
		resp.PublicKey = keyPair.Public
	}

	remoteKite := args.Kite

	// Be sure we have a valid Kite representation. We should not allow someone
	// with an empty field to be registered.
	if err := validateKiteKey(remoteKite); err != nil {
		http.Error(rw, jsonError(err), http.StatusBadRequest)
		return
	}

	// This will be stored into the final storage
	value := &kontrolprotocol.RegisterValue{
		URL:   args.URL,
		KeyID: keyPair.ID,
	}

	// Register first by adding the value to the storage. Return if there is
	// any error.
	if err := k.storage.Upsert(remoteKite, value); err != nil {
		k.log.Error("storage add '%s' error: %s", remoteKite, err)
		http.Error(rw, jsonError(errors.New("internal error - register")), http.StatusInternalServerError)
		return
	}

	k.heartbeatsMu.Lock()
	defer k.heartbeatsMu.Unlock()

	h, ok := k.heartbeats[remoteKite.ID]
	if ok {
		// there is already a previous registration, use it
		k.log.Info("Kite was already register (via HTTP), use timer cache %s", remoteKite)

		h.timer.Reset(HeartbeatInterval + HeartbeatDelay)

		// update registerURL of the previously started heartbeat goroutine
		// so it does not get overwritten back to the old value
		h.updateC <- func() error {
			return k.storage.Update(remoteKite, value)
		}
	} else {
		// we create a new ticker which is going to update the key periodically in
		// the storage so it's always up to date. Instead of updating the key
		// periodically according to the HeartBeatInterval below, we are buffering
		// the write speed here with the UpdateInterval.
		h = &heartbeat{
			updateC: make(chan func() error),
		}

		updater := time.NewTicker(UpdateInterval)

		go func() {
			update := func() error {
				return k.storage.Update(remoteKite, value)
			}

			for {
				select {
				case <-k.closed:
					return
				case <-updater.C:
					k.log.Debug("Kite is active (via HTTP), updating the value %s", remoteKite)

					if err := update(); err != nil {
						k.log.Error("storage update '%s' error: %s", remoteKite, err)
					}
				case fn, ok := <-h.updateC:
					if !ok {
						k.log.Info("Kite is nonactive (via HTTP). Updater is closed %s", remoteKite)
						return
					}

					update = fn
				}
			}
		}()

		// we are now creating a timer that is going to call the function which
		// stops the background updater if it's not resetted. The time is being
		// resetted on a separate HTTP endpoint "/heartbeat"
		h.timer = time.AfterFunc(HeartbeatInterval+HeartbeatDelay, func() {
			k.log.Info("Kite didn't sent any heartbeat (via HTTP). Stopping the updater %s", remoteKite)

			// stop the updater so it doesn't update it in the background
			updater.Stop()

			k.heartbeatsMu.Lock()
			defer k.heartbeatsMu.Unlock()

			select {
			case <-h.updateC:
			default:
				close(h.updateC)
			}

			delete(k.heartbeats, remoteKite.ID)
		})

		k.heartbeats[remoteKite.ID] = h
	}

	k.log.Info("Kite registered (via HTTP): %s", remoteKite)

	// send the response back to the requester
	if err := json.NewEncoder(rw).Encode(resp); err != nil {
		errMsg := fmt.Errorf("could not encode response: '%s'", err)
		http.Error(rw, jsonError(errMsg), http.StatusInternalServerError)
		return
	}
}
Exemple #14
0
func (k *Kontrol) HandleRegister(r *kite.Request) (interface{}, error) {
	k.log.Info("Register request from: %s", r.Client.Kite)

	// Only accept requests with kiteKey because we need this info
	// for generating tokens for this kite.
	if r.Auth.Type != "kiteKey" {
		return nil, fmt.Errorf("Unexpected authentication type: %s", r.Auth.Type)
	}

	var args struct {
		URL string `json:"url"`
	}

	if err := r.Args.One().Unmarshal(&args); err != nil {
		return nil, err
	}

	if args.URL == "" {
		return nil, errors.New("empty url")
	}

	if _, err := url.Parse(args.URL); err != nil {
		return nil, fmt.Errorf("invalid register URL: %s", err)
	}

	res := &protocol.RegisterResult{
		URL: args.URL,
	}

	ex := &kitekey.Extractor{
		Claims: &kitekey.KiteClaims{},
	}

	t, err := jwt.ParseWithClaims(r.Auth.Key, ex.Claims, ex.Extract)
	if err != nil {
		return nil, err
	}

	var keyPair *KeyPair
	var origKey = ex.Claims.KontrolKey

	// check if the key is valid and is stored in the key pair storage, if not
	// check if there is a new key we can use.
	keyPair, res.KiteKey, err = k.getOrUpdateKeyPub(ex.Claims.KontrolKey, t, r)
	if err != nil {
		return nil, err
	}

	if origKey != keyPair.Public {
		// NOTE(rjeczalik): updates public key for old kites, new kites
		// expect kite key to be updated
		res.PublicKey = keyPair.Public
	}

	if err := validateKiteKey(&r.Client.Kite); err != nil {
		return nil, err
	}

	value := &kontrolprotocol.RegisterValue{
		URL:   args.URL,
		KeyID: keyPair.ID,
	}

	// Register first by adding the value to the storage. Return if there is
	// any error.
	if err := k.storage.Upsert(&r.Client.Kite, value); err != nil {
		k.log.Error("storage add '%s' error: %s", &r.Client.Kite, err)
		return nil, errors.New("internal error - register")
	}

	every := onceevery.New(UpdateInterval)

	ping := make(chan struct{}, 1)
	closed := int32(0)

	kiteCopy := r.Client.Kite

	updaterFunc := func() {
		for {
			select {
			case <-k.closed:
				return
			case <-ping:
				k.log.Debug("Kite is active, got a ping %s", &kiteCopy)
				every.Do(func() {
					k.log.Debug("Kite is active, updating the value %s", &kiteCopy)
					err := k.storage.Update(&kiteCopy, value)
					if err != nil {
						k.log.Error("storage update '%s' error: %s", &kiteCopy, err)
					}
				})
			case <-time.After(HeartbeatInterval + HeartbeatDelay):
				k.log.Debug("Kite didn't sent any heartbeat %s.", &kiteCopy)
				atomic.StoreInt32(&closed, 1)
				return
			}
		}
	}

	go updaterFunc()

	heartbeatArgs := []interface{}{
		HeartbeatInterval / time.Second,
		dnode.Callback(func(args *dnode.Partial) {
			k.log.Debug("Kite send us an heartbeat. %s", &kiteCopy)

			k.clientLocks.Get(kiteCopy.ID).Lock()
			defer k.clientLocks.Get(kiteCopy.ID).Unlock()

			select {
			case ping <- struct{}{}:
			default:
			}

			// seems we miss a heartbeat, so start it again!
			if atomic.CompareAndSwapInt32(&closed, 1, 0) {
				k.log.Warning("Updater was closed, but we are still getting heartbeats. Starting again %s", &kiteCopy)

				// it might be removed because the ttl cleaner would come
				// before us, so try to add it again, the updater will than
				// continue to update it afterwards.
				k.storage.Upsert(&kiteCopy, value)
				go updaterFunc()
			}
		}),
	}

	// now trigger the remote kite so it sends us periodically an heartbeat
	resp := r.Client.GoWithTimeout("kite.heartbeat", 4*time.Second, heartbeatArgs...)

	go func() {
		if err := (<-resp).Err; err != nil {
			k.log.Error("failed requesting heartbeats from %q kite: %s", kiteCopy.Name, err)
		}
	}()

	k.log.Info("Kite registered: %s", &r.Client.Kite)

	clientKite := r.Client.Kite.String()

	r.Client.OnDisconnect(func() {
		k.log.Info("Kite disconnected: %s", clientKite)
	})

	return res, nil
}