123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329 |
- // 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)
- }
|