blob: 074e3f1245f7faff0bee55953f0e085db655a934 [file] [log] [blame]
David K. Bainbridgebd6b2882021-08-26 13:31:02 +00001// Package client provides a client library and methods for Kerberos 5 authentication.
2package client
3
4import (
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.
25type 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.
35func 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.
49func 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.
65func 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.
119func (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.
139func (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.
167func (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.
195func (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.
207func (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.
238func (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.
247func (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.
311func (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}