blob: 6e4c83c2c42815d357835aa5adc94ab7c1780d40 [file] [log] [blame]
Scott Bakered4efab2020-01-13 19:12:25 -08001// Package client provides a client library and methods for Kerberos 5 authentication.
2package client
3
4import (
5 "errors"
6 "fmt"
7 "time"
8
9 "gopkg.in/jcmturner/gokrb5.v7/config"
10 "gopkg.in/jcmturner/gokrb5.v7/credentials"
11 "gopkg.in/jcmturner/gokrb5.v7/crypto"
12 "gopkg.in/jcmturner/gokrb5.v7/crypto/etype"
13 "gopkg.in/jcmturner/gokrb5.v7/iana/errorcode"
14 "gopkg.in/jcmturner/gokrb5.v7/iana/nametype"
15 "gopkg.in/jcmturner/gokrb5.v7/keytab"
16 "gopkg.in/jcmturner/gokrb5.v7/krberror"
17 "gopkg.in/jcmturner/gokrb5.v7/messages"
18 "gopkg.in/jcmturner/gokrb5.v7/types"
19)
20
21// Client side configuration and state.
22type Client struct {
23 Credentials *credentials.Credentials
24 Config *config.Config
25 settings *Settings
26 sessions *sessions
27 cache *Cache
28}
29
30// NewClientWithPassword creates a new client from a password credential.
31// Set the realm to empty string to use the default realm from config.
32func NewClientWithPassword(username, realm, password string, krb5conf *config.Config, settings ...func(*Settings)) *Client {
33 creds := credentials.New(username, realm)
34 return &Client{
35 Credentials: creds.WithPassword(password),
36 Config: krb5conf,
37 settings: NewSettings(settings...),
38 sessions: &sessions{
39 Entries: make(map[string]*session),
40 },
41 cache: NewCache(),
42 }
43}
44
45// NewClientWithKeytab creates a new client from a keytab credential.
46func NewClientWithKeytab(username, realm string, kt *keytab.Keytab, krb5conf *config.Config, settings ...func(*Settings)) *Client {
47 creds := credentials.New(username, realm)
48 return &Client{
49 Credentials: creds.WithKeytab(kt),
50 Config: krb5conf,
51 settings: NewSettings(settings...),
52 sessions: &sessions{
53 Entries: make(map[string]*session),
54 },
55 cache: NewCache(),
56 }
57}
58
59// NewClientFromCCache create a client from a populated client cache.
60//
61// WARNING: A client created from CCache does not automatically renew TGTs and a failure will occur after the TGT expires.
62func NewClientFromCCache(c *credentials.CCache, krb5conf *config.Config, settings ...func(*Settings)) (*Client, error) {
63 cl := &Client{
64 Credentials: c.GetClientCredentials(),
65 Config: krb5conf,
66 settings: NewSettings(settings...),
67 sessions: &sessions{
68 Entries: make(map[string]*session),
69 },
70 cache: NewCache(),
71 }
72 spn := types.PrincipalName{
73 NameType: nametype.KRB_NT_SRV_INST,
74 NameString: []string{"krbtgt", c.DefaultPrincipal.Realm},
75 }
76 cred, ok := c.GetEntry(spn)
77 if !ok {
78 return cl, errors.New("TGT not found in CCache")
79 }
80 var tgt messages.Ticket
81 err := tgt.Unmarshal(cred.Ticket)
82 if err != nil {
83 return cl, fmt.Errorf("TGT bytes in cache are not valid: %v", err)
84 }
85 cl.sessions.Entries[c.DefaultPrincipal.Realm] = &session{
86 realm: c.DefaultPrincipal.Realm,
87 authTime: cred.AuthTime,
88 endTime: cred.EndTime,
89 renewTill: cred.RenewTill,
90 tgt: tgt,
91 sessionKey: cred.Key,
92 }
93 for _, cred := range c.GetEntries() {
94 var tkt messages.Ticket
95 err = tkt.Unmarshal(cred.Ticket)
96 if err != nil {
97 return cl, fmt.Errorf("cache entry ticket bytes are not valid: %v", err)
98 }
99 cl.cache.addEntry(
100 tkt,
101 cred.AuthTime,
102 cred.StartTime,
103 cred.EndTime,
104 cred.RenewTill,
105 cred.Key,
106 )
107 }
108 return cl, nil
109}
110
111// Key returns the client's encryption key for the specified encryption type.
112// The key can be retrieved either from the keytab or generated from the client's password.
113// If the client has both a keytab and a password defined the keytab is favoured as the source for the key
114// A KRBError can be passed in the event the KDC returns one of type KDC_ERR_PREAUTH_REQUIRED and is required to derive
115// the key for pre-authentication from the client's password. If a KRBError is not available, pass nil to this argument.
116func (cl *Client) Key(etype etype.EType, krberr *messages.KRBError) (types.EncryptionKey, error) {
117 if cl.Credentials.HasKeytab() && etype != nil {
118 return cl.Credentials.Keytab().GetEncryptionKey(cl.Credentials.CName(), cl.Credentials.Domain(), 0, etype.GetETypeID())
119 } else if cl.Credentials.HasPassword() {
120 if krberr != nil && krberr.ErrorCode == errorcode.KDC_ERR_PREAUTH_REQUIRED {
121 var pas types.PADataSequence
122 err := pas.Unmarshal(krberr.EData)
123 if err != nil {
124 return types.EncryptionKey{}, fmt.Errorf("could not get PAData from KRBError to generate key from password: %v", err)
125 }
126 key, _, err := crypto.GetKeyFromPassword(cl.Credentials.Password(), krberr.CName, krberr.CRealm, etype.GetETypeID(), pas)
127 return key, err
128 }
129 key, _, err := crypto.GetKeyFromPassword(cl.Credentials.Password(), cl.Credentials.CName(), cl.Credentials.Domain(), etype.GetETypeID(), types.PADataSequence{})
130 return key, err
131 }
132 return types.EncryptionKey{}, errors.New("credential has neither keytab or password to generate key")
133}
134
135// IsConfigured indicates if the client has the values required set.
136func (cl *Client) IsConfigured() (bool, error) {
137 if cl.Credentials.UserName() == "" {
138 return false, errors.New("client does not have a username")
139 }
140 if cl.Credentials.Domain() == "" {
141 return false, errors.New("client does not have a define realm")
142 }
143 // Client needs to have either a password, keytab or a session already (later when loading from CCache)
144 if !cl.Credentials.HasPassword() && !cl.Credentials.HasKeytab() {
145 authTime, _, _, _, err := cl.sessionTimes(cl.Credentials.Domain())
146 if err != nil || authTime.IsZero() {
147 return false, errors.New("client has neither a keytab nor a password set and no session")
148 }
149 }
150 if !cl.Config.LibDefaults.DNSLookupKDC {
151 for _, r := range cl.Config.Realms {
152 if r.Realm == cl.Credentials.Domain() {
153 if len(r.KDC) > 0 {
154 return true, nil
155 }
156 return false, errors.New("client krb5 config does not have any defined KDCs for the default realm")
157 }
158 }
159 }
160 return true, nil
161}
162
163// Login the client with the KDC via an AS exchange.
164func (cl *Client) Login() error {
165 if ok, err := cl.IsConfigured(); !ok {
166 return err
167 }
168 if !cl.Credentials.HasPassword() && !cl.Credentials.HasKeytab() {
169 _, endTime, _, _, err := cl.sessionTimes(cl.Credentials.Domain())
170 if err != nil {
171 return krberror.Errorf(err, krberror.KRBMsgError, "no user credentials available and error getting any existing session")
172 }
173 if time.Now().UTC().After(endTime) {
174 return krberror.NewKrberror(krberror.KRBMsgError, "cannot login, no user credentials available and no valid existing session")
175 }
176 // no credentials but there is a session with tgt already
177 return nil
178 }
179 ASReq, err := messages.NewASReqForTGT(cl.Credentials.Domain(), cl.Config, cl.Credentials.CName())
180 if err != nil {
181 return krberror.Errorf(err, krberror.KRBMsgError, "error generating new AS_REQ")
182 }
183 ASRep, err := cl.ASExchange(cl.Credentials.Domain(), ASReq, 0)
184 if err != nil {
185 return err
186 }
187 cl.addSession(ASRep.Ticket, ASRep.DecryptedEncPart)
188 return nil
189}
190
191// realmLogin obtains or renews a TGT and establishes a session for the realm specified.
192func (cl *Client) realmLogin(realm string) error {
193 if realm == cl.Credentials.Domain() {
194 return cl.Login()
195 }
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 tgt, skey, err := cl.sessionTGT(cl.Credentials.Domain())
204 if err != nil {
205 return err
206 }
207
208 spn := types.PrincipalName{
209 NameType: nametype.KRB_NT_SRV_INST,
210 NameString: []string{"krbtgt", realm},
211 }
212
213 _, tgsRep, err := cl.TGSREQGenerateAndExchange(spn, cl.Credentials.Domain(), tgt, skey, false)
214 if err != nil {
215 return err
216 }
217 cl.addSession(tgsRep.Ticket, tgsRep.DecryptedEncPart)
218
219 return nil
220}
221
222// Destroy stops the auto-renewal of all sessions and removes the sessions and cache entries from the client.
223func (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}