242 lines
8.2 KiB
Go
242 lines
8.2 KiB
Go
|
// Package client provides a client library and methods for Kerberos 5 authentication.
|
||
|
package client
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"time"
|
||
|
|
||
|
"gopkg.in/jcmturner/gokrb5.v7/config"
|
||
|
"gopkg.in/jcmturner/gokrb5.v7/credentials"
|
||
|
"gopkg.in/jcmturner/gokrb5.v7/crypto"
|
||
|
"gopkg.in/jcmturner/gokrb5.v7/crypto/etype"
|
||
|
"gopkg.in/jcmturner/gokrb5.v7/iana/errorcode"
|
||
|
"gopkg.in/jcmturner/gokrb5.v7/iana/nametype"
|
||
|
"gopkg.in/jcmturner/gokrb5.v7/keytab"
|
||
|
"gopkg.in/jcmturner/gokrb5.v7/krberror"
|
||
|
"gopkg.in/jcmturner/gokrb5.v7/messages"
|
||
|
"gopkg.in/jcmturner/gokrb5.v7/types"
|
||
|
)
|
||
|
|
||
|
// Client side configuration and state.
|
||
|
type Client struct {
|
||
|
Credentials *credentials.Credentials
|
||
|
Config *config.Config
|
||
|
settings *Settings
|
||
|
sessions *sessions
|
||
|
cache *Cache
|
||
|
}
|
||
|
|
||
|
// NewClientWithPassword creates a new client from a password credential.
|
||
|
// Set the realm to empty string to use the default realm from config.
|
||
|
func NewClientWithPassword(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(),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// NewClientWithKeytab creates a new client from a keytab credential.
|
||
|
func NewClientWithKeytab(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(),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// NewClientFromCCache 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 NewClientFromCCache(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.
|
||
|
// 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, krberr *messages.KRBError) (types.EncryptionKey, error) {
|
||
|
if cl.Credentials.HasKeytab() && etype != nil {
|
||
|
return cl.Credentials.Keytab().GetEncryptionKey(cl.Credentials.CName(), cl.Credentials.Domain(), 0, 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{}, 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, err
|
||
|
}
|
||
|
key, _, err := crypto.GetKeyFromPassword(cl.Credentials.Password(), cl.Credentials.CName(), cl.Credentials.Domain(), etype.GetETypeID(), types.PADataSequence{})
|
||
|
return key, err
|
||
|
}
|
||
|
return types.EncryptionKey{}, 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.NewKrberror(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")
|
||
|
}
|