123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295 |
- package client
- import (
- "encoding/json"
- "fmt"
- "sort"
- "strings"
- "sync"
- "time"
- "github.com/jcmturner/gokrb5/v8/iana/nametype"
- "github.com/jcmturner/gokrb5/v8/krberror"
- "github.com/jcmturner/gokrb5/v8/messages"
- "github.com/jcmturner/gokrb5/v8/types"
- )
- // sessions hold TGTs and are keyed on the realm name
- type sessions struct {
- Entries map[string]*session
- mux sync.RWMutex
- }
- // destroy erases all sessions
- func (s *sessions) destroy() {
- s.mux.Lock()
- defer s.mux.Unlock()
- for k, e := range s.Entries {
- e.destroy()
- delete(s.Entries, k)
- }
- }
- // update replaces a session with the one provided or adds it as a new one
- func (s *sessions) update(sess *session) {
- s.mux.Lock()
- defer s.mux.Unlock()
- // if a session already exists for this, cancel its auto renew.
- if i, ok := s.Entries[sess.realm]; ok {
- if i != sess {
- // Session in the sessions cache is not the same as one provided.
- // Cancel the one in the cache and add this one.
- i.mux.Lock()
- defer i.mux.Unlock()
- i.cancel <- true
- s.Entries[sess.realm] = sess
- return
- }
- }
- // No session for this realm was found so just add it
- s.Entries[sess.realm] = sess
- }
- // get returns the session for the realm specified
- func (s *sessions) get(realm string) (*session, bool) {
- s.mux.RLock()
- defer s.mux.RUnlock()
- sess, ok := s.Entries[realm]
- return sess, ok
- }
- // session holds the TGT details for a realm
- type session struct {
- realm string
- authTime time.Time
- endTime time.Time
- renewTill time.Time
- tgt messages.Ticket
- sessionKey types.EncryptionKey
- sessionKeyExpiration time.Time
- cancel chan bool
- mux sync.RWMutex
- }
- // jsonSession is used to enable marshaling some information of a session in a JSON format
- type jsonSession struct {
- Realm string
- AuthTime time.Time
- EndTime time.Time
- RenewTill time.Time
- SessionKeyExpiration time.Time
- }
- // AddSession adds a session for a realm with a TGT to the client's session cache.
- // A goroutine is started to automatically renew the TGT before expiry.
- func (cl *Client) addSession(tgt messages.Ticket, dep messages.EncKDCRepPart) {
- if strings.ToLower(tgt.SName.NameString[0]) != "krbtgt" {
- // Not a TGT
- return
- }
- realm := tgt.SName.NameString[len(tgt.SName.NameString)-1]
- s := &session{
- realm: realm,
- authTime: dep.AuthTime,
- endTime: dep.EndTime,
- renewTill: dep.RenewTill,
- tgt: tgt,
- sessionKey: dep.Key,
- sessionKeyExpiration: dep.KeyExpiration,
- }
- cl.sessions.update(s)
- cl.enableAutoSessionRenewal(s)
- cl.Log("TGT session added for %s (EndTime: %v)", realm, dep.EndTime)
- }
- // update overwrites the session details with those from the TGT and decrypted encPart
- func (s *session) update(tgt messages.Ticket, dep messages.EncKDCRepPart) {
- s.mux.Lock()
- defer s.mux.Unlock()
- s.authTime = dep.AuthTime
- s.endTime = dep.EndTime
- s.renewTill = dep.RenewTill
- s.tgt = tgt
- s.sessionKey = dep.Key
- s.sessionKeyExpiration = dep.KeyExpiration
- }
- // destroy will cancel any auto renewal of the session and set the expiration times to the current time
- func (s *session) destroy() {
- s.mux.Lock()
- defer s.mux.Unlock()
- if s.cancel != nil {
- s.cancel <- true
- }
- s.endTime = time.Now().UTC()
- s.renewTill = s.endTime
- s.sessionKeyExpiration = s.endTime
- }
- // valid informs if the TGT is still within the valid time window
- func (s *session) valid() bool {
- s.mux.RLock()
- defer s.mux.RUnlock()
- t := time.Now().UTC()
- if t.Before(s.endTime) && s.authTime.Before(t) {
- return true
- }
- return false
- }
- // tgtDetails is a thread safe way to get the session's realm, TGT and session key values
- func (s *session) tgtDetails() (string, messages.Ticket, types.EncryptionKey) {
- s.mux.RLock()
- defer s.mux.RUnlock()
- return s.realm, s.tgt, s.sessionKey
- }
- // timeDetails is a thread safe way to get the session's validity time values
- func (s *session) timeDetails() (string, time.Time, time.Time, time.Time, time.Time) {
- s.mux.RLock()
- defer s.mux.RUnlock()
- return s.realm, s.authTime, s.endTime, s.renewTill, s.sessionKeyExpiration
- }
- // JSON return information about the held sessions in a JSON format.
- func (s *sessions) JSON() (string, error) {
- s.mux.RLock()
- defer s.mux.RUnlock()
- var js []jsonSession
- keys := make([]string, 0, len(s.Entries))
- for k := range s.Entries {
- keys = append(keys, k)
- }
- sort.Strings(keys)
- for _, k := range keys {
- r, at, et, rt, kt := s.Entries[k].timeDetails()
- j := jsonSession{
- Realm: r,
- AuthTime: at,
- EndTime: et,
- RenewTill: rt,
- SessionKeyExpiration: kt,
- }
- js = append(js, j)
- }
- b, err := json.MarshalIndent(js, "", " ")
- if err != nil {
- return "", err
- }
- return string(b), nil
- }
- // enableAutoSessionRenewal turns on the automatic renewal for the client's TGT session.
- func (cl *Client) enableAutoSessionRenewal(s *session) {
- var timer *time.Timer
- s.mux.Lock()
- s.cancel = make(chan bool, 1)
- s.mux.Unlock()
- go func(s *session) {
- for {
- s.mux.RLock()
- w := (s.endTime.Sub(time.Now().UTC()) * 5) / 6
- s.mux.RUnlock()
- if w < 0 {
- return
- }
- timer = time.NewTimer(w)
- select {
- case <-timer.C:
- renewal, err := cl.refreshSession(s)
- if err != nil {
- cl.Log("error refreshing session: %v", err)
- }
- if !renewal && err == nil {
- // end this goroutine as there will have been a new login and new auto renewal goroutine created.
- return
- }
- case <-s.cancel:
- // cancel has been called. Stop the timer and exit.
- timer.Stop()
- return
- }
- }
- }(s)
- }
- // renewTGT renews the client's TGT session.
- func (cl *Client) renewTGT(s *session) error {
- realm, tgt, skey := s.tgtDetails()
- spn := types.PrincipalName{
- NameType: nametype.KRB_NT_SRV_INST,
- NameString: []string{"krbtgt", realm},
- }
- _, tgsRep, err := cl.TGSREQGenerateAndExchange(spn, cl.Credentials.Domain(), tgt, skey, true)
- if err != nil {
- return krberror.Errorf(err, krberror.KRBMsgError, "error renewing TGT for %s", realm)
- }
- s.update(tgsRep.Ticket, tgsRep.DecryptedEncPart)
- cl.sessions.update(s)
- cl.Log("TGT session renewed for %s (EndTime: %v)", realm, tgsRep.DecryptedEncPart.EndTime)
- return nil
- }
- // refreshSession updates either through renewal or creating a new login.
- // The boolean indicates if the update was a renewal.
- func (cl *Client) refreshSession(s *session) (bool, error) {
- s.mux.RLock()
- realm := s.realm
- renewTill := s.renewTill
- s.mux.RUnlock()
- cl.Log("refreshing TGT session for %s", realm)
- if time.Now().UTC().Before(renewTill) {
- err := cl.renewTGT(s)
- return true, err
- }
- err := cl.realmLogin(realm)
- return false, err
- }
- // ensureValidSession makes sure there is a valid session for the realm
- func (cl *Client) ensureValidSession(realm string) error {
- s, ok := cl.sessions.get(realm)
- if ok {
- s.mux.RLock()
- d := s.endTime.Sub(s.authTime) / 6
- if s.endTime.Sub(time.Now().UTC()) > d {
- s.mux.RUnlock()
- return nil
- }
- s.mux.RUnlock()
- _, err := cl.refreshSession(s)
- return err
- }
- return cl.realmLogin(realm)
- }
- // sessionTGTDetails is a thread safe way to get the TGT and session key values for a realm
- func (cl *Client) sessionTGT(realm string) (tgt messages.Ticket, sessionKey types.EncryptionKey, err error) {
- err = cl.ensureValidSession(realm)
- if err != nil {
- return
- }
- s, ok := cl.sessions.get(realm)
- if !ok {
- err = fmt.Errorf("could not find TGT session for %s", realm)
- return
- }
- _, tgt, sessionKey = s.tgtDetails()
- return
- }
- // sessionTimes provides the timing information with regards to a session for the realm specified.
- func (cl *Client) sessionTimes(realm string) (authTime, endTime, renewTime, sessionExp time.Time, err error) {
- s, ok := cl.sessions.get(realm)
- if !ok {
- err = fmt.Errorf("could not find TGT session for %s", realm)
- return
- }
- _, authTime, endTime, renewTime, sessionExp = s.timeDetails()
- return
- }
- // spnRealm resolves the realm name of a service principal name
- func (cl *Client) spnRealm(spn types.PrincipalName) string {
- return cl.Config.ResolveRealm(spn.NameString[len(spn.NameString)-1])
- }
|