client.go 11 KB

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