| // Package client provides a client library and methods for Kerberos 5 authentication. |
| package client |
| |
| import ( |
| "encoding/json" |
| "errors" |
| "fmt" |
| "io" |
| "strings" |
| "time" |
| |
| "github.com/jcmturner/gokrb5/v8/config" |
| "github.com/jcmturner/gokrb5/v8/credentials" |
| "github.com/jcmturner/gokrb5/v8/crypto" |
| "github.com/jcmturner/gokrb5/v8/crypto/etype" |
| "github.com/jcmturner/gokrb5/v8/iana/errorcode" |
| "github.com/jcmturner/gokrb5/v8/iana/nametype" |
| "github.com/jcmturner/gokrb5/v8/keytab" |
| "github.com/jcmturner/gokrb5/v8/krberror" |
| "github.com/jcmturner/gokrb5/v8/messages" |
| "github.com/jcmturner/gokrb5/v8/types" |
| ) |
| |
| // Client side configuration and state. |
| type Client struct { |
| Credentials *credentials.Credentials |
| Config *config.Config |
| settings *Settings |
| sessions *sessions |
| cache *Cache |
| } |
| |
| // NewWithPassword creates a new client from a password credential. |
| // Set the realm to empty string to use the default realm from config. |
| func NewWithPassword(username, realm, password string, krb5conf *config.Config, settings ...func(*Settings)) *Client { |
| creds := credentials.New(username, realm) |
| return &Client{ |
| Credentials: creds.WithPassword(password), |
| Config: krb5conf, |
| settings: NewSettings(settings...), |
| sessions: &sessions{ |
| Entries: make(map[string]*session), |
| }, |
| cache: NewCache(), |
| } |
| } |
| |
| // NewWithKeytab creates a new client from a keytab credential. |
| func NewWithKeytab(username, realm string, kt *keytab.Keytab, krb5conf *config.Config, settings ...func(*Settings)) *Client { |
| creds := credentials.New(username, realm) |
| return &Client{ |
| Credentials: creds.WithKeytab(kt), |
| Config: krb5conf, |
| settings: NewSettings(settings...), |
| sessions: &sessions{ |
| Entries: make(map[string]*session), |
| }, |
| cache: NewCache(), |
| } |
| } |
| |
| // NewFromCCache create a client from a populated client cache. |
| // |
| // WARNING: A client created from CCache does not automatically renew TGTs and a failure will occur after the TGT expires. |
| func NewFromCCache(c *credentials.CCache, krb5conf *config.Config, settings ...func(*Settings)) (*Client, error) { |
| cl := &Client{ |
| Credentials: c.GetClientCredentials(), |
| Config: krb5conf, |
| settings: NewSettings(settings...), |
| sessions: &sessions{ |
| Entries: make(map[string]*session), |
| }, |
| cache: NewCache(), |
| } |
| spn := types.PrincipalName{ |
| NameType: nametype.KRB_NT_SRV_INST, |
| NameString: []string{"krbtgt", c.DefaultPrincipal.Realm}, |
| } |
| cred, ok := c.GetEntry(spn) |
| if !ok { |
| return cl, errors.New("TGT not found in CCache") |
| } |
| var tgt messages.Ticket |
| err := tgt.Unmarshal(cred.Ticket) |
| if err != nil { |
| return cl, fmt.Errorf("TGT bytes in cache are not valid: %v", err) |
| } |
| cl.sessions.Entries[c.DefaultPrincipal.Realm] = &session{ |
| realm: c.DefaultPrincipal.Realm, |
| authTime: cred.AuthTime, |
| endTime: cred.EndTime, |
| renewTill: cred.RenewTill, |
| tgt: tgt, |
| sessionKey: cred.Key, |
| } |
| for _, cred := range c.GetEntries() { |
| var tkt messages.Ticket |
| err = tkt.Unmarshal(cred.Ticket) |
| if err != nil { |
| return cl, fmt.Errorf("cache entry ticket bytes are not valid: %v", err) |
| } |
| cl.cache.addEntry( |
| tkt, |
| cred.AuthTime, |
| cred.StartTime, |
| cred.EndTime, |
| cred.RenewTill, |
| cred.Key, |
| ) |
| } |
| return cl, nil |
| } |
| |
| // Key returns the client's encryption key for the specified encryption type and its kvno (kvno of zero will find latest). |
| // The key can be retrieved either from the keytab or generated from the client's password. |
| // If the client has both a keytab and a password defined the keytab is favoured as the source for the key |
| // A KRBError can be passed in the event the KDC returns one of type KDC_ERR_PREAUTH_REQUIRED and is required to derive |
| // the key for pre-authentication from the client's password. If a KRBError is not available, pass nil to this argument. |
| func (cl *Client) Key(etype etype.EType, kvno int, krberr *messages.KRBError) (types.EncryptionKey, int, error) { |
| if cl.Credentials.HasKeytab() && etype != nil { |
| return cl.Credentials.Keytab().GetEncryptionKey(cl.Credentials.CName(), cl.Credentials.Domain(), kvno, etype.GetETypeID()) |
| } else if cl.Credentials.HasPassword() { |
| if krberr != nil && krberr.ErrorCode == errorcode.KDC_ERR_PREAUTH_REQUIRED { |
| var pas types.PADataSequence |
| err := pas.Unmarshal(krberr.EData) |
| if err != nil { |
| return types.EncryptionKey{}, 0, fmt.Errorf("could not get PAData from KRBError to generate key from password: %v", err) |
| } |
| key, _, err := crypto.GetKeyFromPassword(cl.Credentials.Password(), krberr.CName, krberr.CRealm, etype.GetETypeID(), pas) |
| return key, 0, err |
| } |
| key, _, err := crypto.GetKeyFromPassword(cl.Credentials.Password(), cl.Credentials.CName(), cl.Credentials.Domain(), etype.GetETypeID(), types.PADataSequence{}) |
| return key, 0, err |
| } |
| return types.EncryptionKey{}, 0, errors.New("credential has neither keytab or password to generate key") |
| } |
| |
| // IsConfigured indicates if the client has the values required set. |
| func (cl *Client) IsConfigured() (bool, error) { |
| if cl.Credentials.UserName() == "" { |
| return false, errors.New("client does not have a username") |
| } |
| if cl.Credentials.Domain() == "" { |
| return false, errors.New("client does not have a define realm") |
| } |
| // Client needs to have either a password, keytab or a session already (later when loading from CCache) |
| if !cl.Credentials.HasPassword() && !cl.Credentials.HasKeytab() { |
| authTime, _, _, _, err := cl.sessionTimes(cl.Credentials.Domain()) |
| if err != nil || authTime.IsZero() { |
| return false, errors.New("client has neither a keytab nor a password set and no session") |
| } |
| } |
| if !cl.Config.LibDefaults.DNSLookupKDC { |
| for _, r := range cl.Config.Realms { |
| if r.Realm == cl.Credentials.Domain() { |
| if len(r.KDC) > 0 { |
| return true, nil |
| } |
| return false, errors.New("client krb5 config does not have any defined KDCs for the default realm") |
| } |
| } |
| } |
| return true, nil |
| } |
| |
| // Login the client with the KDC via an AS exchange. |
| func (cl *Client) Login() error { |
| if ok, err := cl.IsConfigured(); !ok { |
| return err |
| } |
| if !cl.Credentials.HasPassword() && !cl.Credentials.HasKeytab() { |
| _, endTime, _, _, err := cl.sessionTimes(cl.Credentials.Domain()) |
| if err != nil { |
| return krberror.Errorf(err, krberror.KRBMsgError, "no user credentials available and error getting any existing session") |
| } |
| if time.Now().UTC().After(endTime) { |
| return krberror.New(krberror.KRBMsgError, "cannot login, no user credentials available and no valid existing session") |
| } |
| // no credentials but there is a session with tgt already |
| return nil |
| } |
| ASReq, err := messages.NewASReqForTGT(cl.Credentials.Domain(), cl.Config, cl.Credentials.CName()) |
| if err != nil { |
| return krberror.Errorf(err, krberror.KRBMsgError, "error generating new AS_REQ") |
| } |
| ASRep, err := cl.ASExchange(cl.Credentials.Domain(), ASReq, 0) |
| if err != nil { |
| return err |
| } |
| cl.addSession(ASRep.Ticket, ASRep.DecryptedEncPart) |
| return nil |
| } |
| |
| // AffirmLogin will only perform an AS exchange with the KDC if the client does not already have a TGT. |
| func (cl *Client) AffirmLogin() error { |
| _, endTime, _, _, err := cl.sessionTimes(cl.Credentials.Domain()) |
| if err != nil || time.Now().UTC().After(endTime) { |
| err := cl.Login() |
| if err != nil { |
| return fmt.Errorf("could not get valid TGT for client's realm: %v", err) |
| } |
| } |
| return nil |
| } |
| |
| // realmLogin obtains or renews a TGT and establishes a session for the realm specified. |
| func (cl *Client) realmLogin(realm string) error { |
| if realm == cl.Credentials.Domain() { |
| return cl.Login() |
| } |
| _, endTime, _, _, err := cl.sessionTimes(cl.Credentials.Domain()) |
| if err != nil || time.Now().UTC().After(endTime) { |
| err := cl.Login() |
| if err != nil { |
| return fmt.Errorf("could not get valid TGT for client's realm: %v", err) |
| } |
| } |
| tgt, skey, err := cl.sessionTGT(cl.Credentials.Domain()) |
| if err != nil { |
| return err |
| } |
| |
| spn := types.PrincipalName{ |
| NameType: nametype.KRB_NT_SRV_INST, |
| NameString: []string{"krbtgt", realm}, |
| } |
| |
| _, tgsRep, err := cl.TGSREQGenerateAndExchange(spn, cl.Credentials.Domain(), tgt, skey, false) |
| if err != nil { |
| return err |
| } |
| cl.addSession(tgsRep.Ticket, tgsRep.DecryptedEncPart) |
| |
| return nil |
| } |
| |
| // Destroy stops the auto-renewal of all sessions and removes the sessions and cache entries from the client. |
| func (cl *Client) Destroy() { |
| creds := credentials.New("", "") |
| cl.sessions.destroy() |
| cl.cache.clear() |
| cl.Credentials = creds |
| cl.Log("client destroyed") |
| } |
| |
| // Diagnostics runs a set of checks that the client is properly configured and writes details to the io.Writer provided. |
| func (cl *Client) Diagnostics(w io.Writer) error { |
| cl.Print(w) |
| var errs []string |
| if cl.Credentials.HasKeytab() { |
| var loginRealmEncTypes []int32 |
| for _, e := range cl.Credentials.Keytab().Entries { |
| if e.Principal.Realm == cl.Credentials.Realm() { |
| loginRealmEncTypes = append(loginRealmEncTypes, e.Key.KeyType) |
| } |
| } |
| for _, et := range cl.Config.LibDefaults.DefaultTktEnctypeIDs { |
| var etInKt bool |
| for _, val := range loginRealmEncTypes { |
| if val == et { |
| etInKt = true |
| break |
| } |
| } |
| if !etInKt { |
| errs = append(errs, fmt.Sprintf("default_tkt_enctypes specifies %d but this enctype is not available in the client's keytab", et)) |
| } |
| } |
| for _, et := range cl.Config.LibDefaults.PreferredPreauthTypes { |
| var etInKt bool |
| for _, val := range loginRealmEncTypes { |
| if int(val) == et { |
| etInKt = true |
| break |
| } |
| } |
| if !etInKt { |
| errs = append(errs, fmt.Sprintf("preferred_preauth_types specifies %d but this enctype is not available in the client's keytab", et)) |
| } |
| } |
| } |
| udpCnt, udpKDC, err := cl.Config.GetKDCs(cl.Credentials.Realm(), false) |
| if err != nil { |
| errs = append(errs, fmt.Sprintf("error when resolving KDCs for UDP communication: %v", err)) |
| } |
| if udpCnt < 1 { |
| errs = append(errs, "no KDCs resolved for communication via UDP.") |
| } else { |
| b, _ := json.MarshalIndent(&udpKDC, "", " ") |
| fmt.Fprintf(w, "UDP KDCs: %s\n", string(b)) |
| } |
| tcpCnt, tcpKDC, err := cl.Config.GetKDCs(cl.Credentials.Realm(), false) |
| if err != nil { |
| errs = append(errs, fmt.Sprintf("error when resolving KDCs for TCP communication: %v", err)) |
| } |
| if tcpCnt < 1 { |
| errs = append(errs, "no KDCs resolved for communication via TCP.") |
| } else { |
| b, _ := json.MarshalIndent(&tcpKDC, "", " ") |
| fmt.Fprintf(w, "TCP KDCs: %s\n", string(b)) |
| } |
| |
| if errs == nil || len(errs) < 1 { |
| return nil |
| } |
| err = fmt.Errorf(strings.Join(errs, "\n")) |
| return err |
| } |
| |
| // Print writes the details of the client to the io.Writer provided. |
| func (cl *Client) Print(w io.Writer) { |
| c, _ := cl.Credentials.JSON() |
| fmt.Fprintf(w, "Credentials:\n%s\n", c) |
| |
| s, _ := cl.sessions.JSON() |
| fmt.Fprintf(w, "TGT Sessions:\n%s\n", s) |
| |
| c, _ = cl.cache.JSON() |
| fmt.Fprintf(w, "Service ticket cache:\n%s\n", c) |
| |
| s, _ = cl.settings.JSON() |
| fmt.Fprintf(w, "Settings:\n%s\n", s) |
| |
| j, _ := cl.Config.JSON() |
| fmt.Fprintf(w, "Krb5 config:\n%s\n", j) |
| |
| k, _ := cl.Credentials.Keytab().JSON() |
| fmt.Fprintf(w, "Keytab:\n%s\n", k) |
| } |