github.com/jcmturner/gokrb5/v8@v8.4.4/client/client.go (about)

     1  // Package client provides a client library and methods for Kerberos 5 authentication.
     2  package client
     3  
     4  import (
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/jcmturner/gokrb5/v8/config"
    13  	"github.com/jcmturner/gokrb5/v8/credentials"
    14  	"github.com/jcmturner/gokrb5/v8/crypto"
    15  	"github.com/jcmturner/gokrb5/v8/crypto/etype"
    16  	"github.com/jcmturner/gokrb5/v8/iana/errorcode"
    17  	"github.com/jcmturner/gokrb5/v8/iana/nametype"
    18  	"github.com/jcmturner/gokrb5/v8/keytab"
    19  	"github.com/jcmturner/gokrb5/v8/krberror"
    20  	"github.com/jcmturner/gokrb5/v8/messages"
    21  	"github.com/jcmturner/gokrb5/v8/types"
    22  )
    23  
    24  // Client side configuration and state.
    25  type Client struct {
    26  	Credentials *credentials.Credentials
    27  	Config      *config.Config
    28  	settings    *Settings
    29  	sessions    *sessions
    30  	cache       *Cache
    31  }
    32  
    33  // NewWithPassword creates a new client from a password credential.
    34  // Set the realm to empty string to use the default realm from config.
    35  func NewWithPassword(username, realm, password string, krb5conf *config.Config, settings ...func(*Settings)) *Client {
    36  	creds := credentials.New(username, realm)
    37  	return &Client{
    38  		Credentials: creds.WithPassword(password),
    39  		Config:      krb5conf,
    40  		settings:    NewSettings(settings...),
    41  		sessions: &sessions{
    42  			Entries: make(map[string]*session),
    43  		},
    44  		cache: NewCache(),
    45  	}
    46  }
    47  
    48  // NewWithKeytab creates a new client from a keytab credential.
    49  func NewWithKeytab(username, realm string, kt *keytab.Keytab, krb5conf *config.Config, settings ...func(*Settings)) *Client {
    50  	creds := credentials.New(username, realm)
    51  	return &Client{
    52  		Credentials: creds.WithKeytab(kt),
    53  		Config:      krb5conf,
    54  		settings:    NewSettings(settings...),
    55  		sessions: &sessions{
    56  			Entries: make(map[string]*session),
    57  		},
    58  		cache: NewCache(),
    59  	}
    60  }
    61  
    62  // NewFromCCache create a client from a populated client cache.
    63  //
    64  // WARNING: A client created from CCache does not automatically renew TGTs and a failure will occur after the TGT expires.
    65  func NewFromCCache(c *credentials.CCache, krb5conf *config.Config, settings ...func(*Settings)) (*Client, error) {
    66  	cl := &Client{
    67  		Credentials: c.GetClientCredentials(),
    68  		Config:      krb5conf,
    69  		settings:    NewSettings(settings...),
    70  		sessions: &sessions{
    71  			Entries: make(map[string]*session),
    72  		},
    73  		cache: NewCache(),
    74  	}
    75  	spn := types.PrincipalName{
    76  		NameType:   nametype.KRB_NT_SRV_INST,
    77  		NameString: []string{"krbtgt", c.DefaultPrincipal.Realm},
    78  	}
    79  	cred, ok := c.GetEntry(spn)
    80  	if !ok {
    81  		return cl, errors.New("TGT not found in CCache")
    82  	}
    83  	var tgt messages.Ticket
    84  	err := tgt.Unmarshal(cred.Ticket)
    85  	if err != nil {
    86  		return cl, fmt.Errorf("TGT bytes in cache are not valid: %v", err)
    87  	}
    88  	cl.sessions.Entries[c.DefaultPrincipal.Realm] = &session{
    89  		realm:      c.DefaultPrincipal.Realm,
    90  		authTime:   cred.AuthTime,
    91  		endTime:    cred.EndTime,
    92  		renewTill:  cred.RenewTill,
    93  		tgt:        tgt,
    94  		sessionKey: cred.Key,
    95  	}
    96  	for _, cred := range c.GetEntries() {
    97  		var tkt messages.Ticket
    98  		err = tkt.Unmarshal(cred.Ticket)
    99  		if err != nil {
   100  			return cl, fmt.Errorf("cache entry ticket bytes are not valid: %v", err)
   101  		}
   102  		cl.cache.addEntry(
   103  			tkt,
   104  			cred.AuthTime,
   105  			cred.StartTime,
   106  			cred.EndTime,
   107  			cred.RenewTill,
   108  			cred.Key,
   109  		)
   110  	}
   111  	return cl, nil
   112  }
   113  
   114  // Key returns the client's encryption key for the specified encryption type and its kvno (kvno of zero will find latest).
   115  // The key can be retrieved either from the keytab or generated from the client's password.
   116  // If the client has both a keytab and a password defined the keytab is favoured as the source for the key
   117  // A KRBError can be passed in the event the KDC returns one of type KDC_ERR_PREAUTH_REQUIRED and is required to derive
   118  // the key for pre-authentication from the client's password. If a KRBError is not available, pass nil to this argument.
   119  func (cl *Client) Key(etype etype.EType, kvno int, krberr *messages.KRBError) (types.EncryptionKey, int, error) {
   120  	if cl.Credentials.HasKeytab() && etype != nil {
   121  		return cl.Credentials.Keytab().GetEncryptionKey(cl.Credentials.CName(), cl.Credentials.Domain(), kvno, etype.GetETypeID())
   122  	} else if cl.Credentials.HasPassword() {
   123  		if krberr != nil && krberr.ErrorCode == errorcode.KDC_ERR_PREAUTH_REQUIRED {
   124  			var pas types.PADataSequence
   125  			err := pas.Unmarshal(krberr.EData)
   126  			if err != nil {
   127  				return types.EncryptionKey{}, 0, fmt.Errorf("could not get PAData from KRBError to generate key from password: %v", err)
   128  			}
   129  			key, _, err := crypto.GetKeyFromPassword(cl.Credentials.Password(), krberr.CName, krberr.CRealm, etype.GetETypeID(), pas)
   130  			return key, 0, err
   131  		}
   132  		key, _, err := crypto.GetKeyFromPassword(cl.Credentials.Password(), cl.Credentials.CName(), cl.Credentials.Domain(), etype.GetETypeID(), types.PADataSequence{})
   133  		return key, 0, err
   134  	}
   135  	return types.EncryptionKey{}, 0, errors.New("credential has neither keytab or password to generate key")
   136  }
   137  
   138  // IsConfigured indicates if the client has the values required set.
   139  func (cl *Client) IsConfigured() (bool, error) {
   140  	if cl.Credentials.UserName() == "" {
   141  		return false, errors.New("client does not have a username")
   142  	}
   143  	if cl.Credentials.Domain() == "" {
   144  		return false, errors.New("client does not have a define realm")
   145  	}
   146  	// Client needs to have either a password, keytab or a session already (later when loading from CCache)
   147  	if !cl.Credentials.HasPassword() && !cl.Credentials.HasKeytab() {
   148  		authTime, _, _, _, err := cl.sessionTimes(cl.Credentials.Domain())
   149  		if err != nil || authTime.IsZero() {
   150  			return false, errors.New("client has neither a keytab nor a password set and no session")
   151  		}
   152  	}
   153  	if !cl.Config.LibDefaults.DNSLookupKDC {
   154  		for _, r := range cl.Config.Realms {
   155  			if r.Realm == cl.Credentials.Domain() {
   156  				if len(r.KDC) > 0 {
   157  					return true, nil
   158  				}
   159  				return false, errors.New("client krb5 config does not have any defined KDCs for the default realm")
   160  			}
   161  		}
   162  	}
   163  	return true, nil
   164  }
   165  
   166  // Login the client with the KDC via an AS exchange.
   167  func (cl *Client) Login() error {
   168  	if ok, err := cl.IsConfigured(); !ok {
   169  		return err
   170  	}
   171  	if !cl.Credentials.HasPassword() && !cl.Credentials.HasKeytab() {
   172  		_, endTime, _, _, err := cl.sessionTimes(cl.Credentials.Domain())
   173  		if err != nil {
   174  			return krberror.Errorf(err, krberror.KRBMsgError, "no user credentials available and error getting any existing session")
   175  		}
   176  		if time.Now().UTC().After(endTime) {
   177  			return krberror.New(krberror.KRBMsgError, "cannot login, no user credentials available and no valid existing session")
   178  		}
   179  		// no credentials but there is a session with tgt already
   180  		return nil
   181  	}
   182  	ASReq, err := messages.NewASReqForTGT(cl.Credentials.Domain(), cl.Config, cl.Credentials.CName())
   183  	if err != nil {
   184  		return krberror.Errorf(err, krberror.KRBMsgError, "error generating new AS_REQ")
   185  	}
   186  	ASRep, err := cl.ASExchange(cl.Credentials.Domain(), ASReq, 0)
   187  	if err != nil {
   188  		return err
   189  	}
   190  	cl.addSession(ASRep.Ticket, ASRep.DecryptedEncPart)
   191  	return nil
   192  }
   193  
   194  // AffirmLogin will only perform an AS exchange with the KDC if the client does not already have a TGT.
   195  func (cl *Client) AffirmLogin() error {
   196  	_, endTime, _, _, err := cl.sessionTimes(cl.Credentials.Domain())
   197  	if err != nil || time.Now().UTC().After(endTime) {
   198  		err := cl.Login()
   199  		if err != nil {
   200  			return fmt.Errorf("could not get valid TGT for client's realm: %v", err)
   201  		}
   202  	}
   203  	return nil
   204  }
   205  
   206  // realmLogin obtains or renews a TGT and establishes a session for the realm specified.
   207  func (cl *Client) realmLogin(realm string) error {
   208  	if realm == cl.Credentials.Domain() {
   209  		return cl.Login()
   210  	}
   211  	_, endTime, _, _, err := cl.sessionTimes(cl.Credentials.Domain())
   212  	if err != nil || time.Now().UTC().After(endTime) {
   213  		err := cl.Login()
   214  		if err != nil {
   215  			return fmt.Errorf("could not get valid TGT for client's realm: %v", err)
   216  		}
   217  	}
   218  	tgt, skey, err := cl.sessionTGT(cl.Credentials.Domain())
   219  	if err != nil {
   220  		return err
   221  	}
   222  
   223  	spn := types.PrincipalName{
   224  		NameType:   nametype.KRB_NT_SRV_INST,
   225  		NameString: []string{"krbtgt", realm},
   226  	}
   227  
   228  	_, tgsRep, err := cl.TGSREQGenerateAndExchange(spn, cl.Credentials.Domain(), tgt, skey, false)
   229  	if err != nil {
   230  		return err
   231  	}
   232  	cl.addSession(tgsRep.Ticket, tgsRep.DecryptedEncPart)
   233  
   234  	return nil
   235  }
   236  
   237  // Destroy stops the auto-renewal of all sessions and removes the sessions and cache entries from the client.
   238  func (cl *Client) Destroy() {
   239  	creds := credentials.New("", "")
   240  	cl.sessions.destroy()
   241  	cl.cache.clear()
   242  	cl.Credentials = creds
   243  	cl.Log("client destroyed")
   244  }
   245  
   246  // Diagnostics runs a set of checks that the client is properly configured and writes details to the io.Writer provided.
   247  func (cl *Client) Diagnostics(w io.Writer) error {
   248  	cl.Print(w)
   249  	var errs []string
   250  	if cl.Credentials.HasKeytab() {
   251  		var loginRealmEncTypes []int32
   252  		for _, e := range cl.Credentials.Keytab().Entries {
   253  			if e.Principal.Realm == cl.Credentials.Realm() {
   254  				loginRealmEncTypes = append(loginRealmEncTypes, e.Key.KeyType)
   255  			}
   256  		}
   257  		for _, et := range cl.Config.LibDefaults.DefaultTktEnctypeIDs {
   258  			var etInKt bool
   259  			for _, val := range loginRealmEncTypes {
   260  				if val == et {
   261  					etInKt = true
   262  					break
   263  				}
   264  			}
   265  			if !etInKt {
   266  				errs = append(errs, fmt.Sprintf("default_tkt_enctypes specifies %d but this enctype is not available in the client's keytab", et))
   267  			}
   268  		}
   269  		for _, et := range cl.Config.LibDefaults.PreferredPreauthTypes {
   270  			var etInKt bool
   271  			for _, val := range loginRealmEncTypes {
   272  				if int(val) == et {
   273  					etInKt = true
   274  					break
   275  				}
   276  			}
   277  			if !etInKt {
   278  				errs = append(errs, fmt.Sprintf("preferred_preauth_types specifies %d but this enctype is not available in the client's keytab", et))
   279  			}
   280  		}
   281  	}
   282  	udpCnt, udpKDC, err := cl.Config.GetKDCs(cl.Credentials.Realm(), false)
   283  	if err != nil {
   284  		errs = append(errs, fmt.Sprintf("error when resolving KDCs for UDP communication: %v", err))
   285  	}
   286  	if udpCnt < 1 {
   287  		errs = append(errs, "no KDCs resolved for communication via UDP.")
   288  	} else {
   289  		b, _ := json.MarshalIndent(&udpKDC, "", "  ")
   290  		fmt.Fprintf(w, "UDP KDCs: %s\n", string(b))
   291  	}
   292  	tcpCnt, tcpKDC, err := cl.Config.GetKDCs(cl.Credentials.Realm(), false)
   293  	if err != nil {
   294  		errs = append(errs, fmt.Sprintf("error when resolving KDCs for TCP communication: %v", err))
   295  	}
   296  	if tcpCnt < 1 {
   297  		errs = append(errs, "no KDCs resolved for communication via TCP.")
   298  	} else {
   299  		b, _ := json.MarshalIndent(&tcpKDC, "", "  ")
   300  		fmt.Fprintf(w, "TCP KDCs: %s\n", string(b))
   301  	}
   302  
   303  	if errs == nil || len(errs) < 1 {
   304  		return nil
   305  	}
   306  	err = fmt.Errorf(strings.Join(errs, "\n"))
   307  	return err
   308  }
   309  
   310  // Print writes the details of the client to the io.Writer provided.
   311  func (cl *Client) Print(w io.Writer) {
   312  	c, _ := cl.Credentials.JSON()
   313  	fmt.Fprintf(w, "Credentials:\n%s\n", c)
   314  
   315  	s, _ := cl.sessions.JSON()
   316  	fmt.Fprintf(w, "TGT Sessions:\n%s\n", s)
   317  
   318  	c, _ = cl.cache.JSON()
   319  	fmt.Fprintf(w, "Service ticket cache:\n%s\n", c)
   320  
   321  	s, _ = cl.settings.JSON()
   322  	fmt.Fprintf(w, "Settings:\n%s\n", s)
   323  
   324  	j, _ := cl.Config.JSON()
   325  	fmt.Fprintf(w, "Krb5 config:\n%s\n", j)
   326  
   327  	k, _ := cl.Credentials.Keytab().JSON()
   328  	fmt.Fprintf(w, "Keytab:\n%s\n", k)
   329  }