| // Package client provides a client library and methods for Kerberos 5 authentication. |
| package client |
| |
| import ( |
| "errors" |
| "fmt" |
| "time" |
| |
| "gopkg.in/jcmturner/gokrb5.v7/config" |
| "gopkg.in/jcmturner/gokrb5.v7/credentials" |
| "gopkg.in/jcmturner/gokrb5.v7/crypto" |
| "gopkg.in/jcmturner/gokrb5.v7/crypto/etype" |
| "gopkg.in/jcmturner/gokrb5.v7/iana/errorcode" |
| "gopkg.in/jcmturner/gokrb5.v7/iana/nametype" |
| "gopkg.in/jcmturner/gokrb5.v7/keytab" |
| "gopkg.in/jcmturner/gokrb5.v7/krberror" |
| "gopkg.in/jcmturner/gokrb5.v7/messages" |
| "gopkg.in/jcmturner/gokrb5.v7/types" |
| ) |
| |
| // Client side configuration and state. |
| type Client struct { |
| Credentials *credentials.Credentials |
| Config *config.Config |
| settings *Settings |
| sessions *sessions |
| cache *Cache |
| } |
| |
| // NewClientWithPassword creates a new client from a password credential. |
| // Set the realm to empty string to use the default realm from config. |
| func NewClientWithPassword(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(), |
| } |
| } |
| |
| // NewClientWithKeytab creates a new client from a keytab credential. |
| func NewClientWithKeytab(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(), |
| } |
| } |
| |
| // NewClientFromCCache 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 NewClientFromCCache(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. |
| // 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, krberr *messages.KRBError) (types.EncryptionKey, error) { |
| if cl.Credentials.HasKeytab() && etype != nil { |
| return cl.Credentials.Keytab().GetEncryptionKey(cl.Credentials.CName(), cl.Credentials.Domain(), 0, 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{}, 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, err |
| } |
| key, _, err := crypto.GetKeyFromPassword(cl.Credentials.Password(), cl.Credentials.CName(), cl.Credentials.Domain(), etype.GetETypeID(), types.PADataSequence{}) |
| return key, err |
| } |
| return types.EncryptionKey{}, 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.NewKrberror(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") |
| } |