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 }