Skip to content

Commit 77f53f9

Browse files
committed
new SSPI code
Signed-off-by: Monis Khan <[email protected]>
1 parent 1b69a9e commit 77f53f9

File tree

3 files changed

+206
-84
lines changed

3 files changed

+206
-84
lines changed

pkg/oc/util/tokencmd/negotiator_sspi.go

Lines changed: 202 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,51 @@ package tokencmd
44

55
import (
66
"fmt"
7+
"io"
8+
"os"
79
"strings"
10+
"syscall"
811

912
"k8s.io/apimachinery/pkg/util/errors"
1013
"k8s.io/apimachinery/pkg/util/runtime"
1114

15+
"github.com/openshift/origin/pkg/cmd/util/term"
16+
1217
"github.com/alexbrainman/sspi"
1318
"github.com/alexbrainman/sspi/negotiate"
1419
"github.com/golang/glog"
1520
)
1621

1722
const (
18-
// sane set of default flags, see sspiNegotiator.flags
23+
// sane set of default flags, see sspiNegotiator.desiredFlags
1924
// TODO make configurable?
20-
flags = sspi.ISC_REQ_CONFIDENTIALITY |
25+
desiredFlags = sspi.ISC_REQ_CONFIDENTIALITY |
2126
sspi.ISC_REQ_INTEGRITY |
2227
sspi.ISC_REQ_MUTUAL_AUTH |
2328
sspi.ISC_REQ_REPLAY_DETECT |
2429
sspi.ISC_REQ_SEQUENCE_DETECT
30+
// subset of desiredFlags that must be set, see sspiNegotiator.requiredFlags
31+
// TODO make configurable?
32+
requiredFlags = sspi.ISC_REQ_CONFIDENTIALITY |
33+
sspi.ISC_REQ_INTEGRITY |
34+
sspi.ISC_REQ_MUTUAL_AUTH
2535

26-
// separator used in fully qualified user name format
36+
// various windows user name formats
37+
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa380525(v=vs.85).aspx
38+
// https://msdn.microsoft.com/en-us/library/ms724268(VS.85).aspx
39+
// separator used in fully qualified user name or down-level logon name format (DOMAIN\Username)
2740
domainSeparator = `\`
41+
// https://msdn.microsoft.com/en-us/library/ms677605(v=vs.85).aspx#userPrincipalName
42+
// separator used in user principal name (UPN) format ([email protected])
43+
upnSeparator = "@"
44+
// https://msdn.microsoft.com/en-us/library/system.environment.userdomainname(v=vs.110).aspx
45+
// environment variable that holds the network domain name associated with the current user
46+
// this is the NetBIOS domain name which should fit within the length requirement (see maxDomain)
47+
shortDomainEnvVar = "USERDOMAIN"
2848

29-
// max lengths for various fields, see sspiNegotiator.principalName
49+
// max lengths for various fields, see sspiNegotiator.getDomainAndUsername and sspiNegotiator.getPassword
50+
// When using the Negotiate package, the maximum character lengths for user name, password, and domain are
51+
// 256, 256, and 15, respectively.
3052
maxUsername = 256
3153
maxPassword = 256
3254
maxDomain = 15
@@ -39,15 +61,22 @@ func SSPIEnabled() bool {
3961
// sspiNegotiator handles negotiate flows on windows via SSPI
4062
// It expects sspiNegotiator.InitSecContext to be called until sspiNegotiator.IsComplete returns true
4163
type sspiNegotiator struct {
42-
// optional DOMAIN\Username and password
64+
// principalName is an optional username (in fully qualified, user principal name or short format).
4365
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa374714(v=vs.85).aspx
4466
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa380131(v=vs.85).aspx
4567
// pAuthData [in]: If credentials are supplied, they are passed via a pointer to a sspi.SEC_WINNT_AUTH_IDENTITY
4668
// structure that includes those credentials.
47-
// When using the Negotiate package, the maximum character lengths for user name, password, and domain are
48-
// 256, 256, and 15, respectively.
4969
principalName string
50-
password string
70+
// password is an optional password used to log into a specific account if principalName is non-empty.
71+
// This allows logging in via username and password even when basic auth is not enabled.
72+
password string
73+
74+
// reader is used to prompt for a password if principalName is non-empty and password is empty.
75+
reader io.Reader
76+
// writer is used to output prompts when prompting for password.
77+
writer io.Writer
78+
// host is the server being authenticated to. Used only for displaying messages when prompting for password.
79+
host string
5180

5281
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms721572(v=vs.85).aspx#_security_credentials_gly
5382
// phCredential [in, optional]: A handle to the credentials returned by AcquireCredentialsHandle (Negotiate).
@@ -58,15 +87,25 @@ type sspiNegotiator struct {
5887
ctx *negotiate.ClientContext
5988
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa375509(v=vs.85).aspx
6089
// fContextReq [in]: Bit flags that indicate requests for the context.
61-
flags uint32
90+
desiredFlags uint32
91+
// requiredFlags is the subset of desiredFlags that must be set for flag verification to succeed
92+
requiredFlags uint32
6293
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa375509(v=vs.85).aspx
6394
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa374764(v=vs.85).aspx
6495
// Set to true once InitializeSecurityContext or CompleteAuthToken return sspi.SEC_E_OK
6596
complete bool
6697
}
6798

68-
func NewSSPINegotiator(principalName, password string) Negotiator {
69-
return &sspiNegotiator{principalName: principalName, password: password, flags: flags}
99+
func NewSSPINegotiator(principalName, password, host string, reader io.Reader) Negotiator {
100+
return &sspiNegotiator{
101+
principalName: principalName,
102+
password: password,
103+
reader: reader,
104+
writer: os.Stdout,
105+
host: host,
106+
desiredFlags: desiredFlags,
107+
requiredFlags: requiredFlags,
108+
}
70109
}
71110

72111
func (s *sspiNegotiator) Load() error {
@@ -75,47 +114,16 @@ func (s *sspiNegotiator) Load() error {
75114
return nil
76115
}
77116

78-
func (s *sspiNegotiator) InitSecContext(requestURL string, challengeToken []byte) ([]byte, error) {
117+
func (s *sspiNegotiator) InitSecContext(requestURL string, challengeToken []byte) (tokenToSend []byte, err error) {
79118
defer runtime.HandleCrash()
80-
if s.cred == nil || s.ctx == nil {
81-
glog.V(5).Infof("Start SSPI flow: %s", requestURL)
82119

83-
cred, err := s.getUserCredentials()
84-
if err != nil {
85-
glog.V(5).Infof("getUserCredentials returned error: %v", err)
86-
return nil, err
87-
}
88-
s.cred = cred
89-
glog.V(5).Info("getUserCredentials successful")
90-
91-
serviceName, err := getServiceName('/', requestURL)
92-
if err != nil {
93-
return nil, err
94-
}
95-
96-
glog.V(5).Infof("importing service name %s", serviceName)
97-
ctx, outputToken, err := negotiate.NewClientContext(s.cred, serviceName) // TODO send s.flags
98-
if err != nil {
99-
glog.V(5).Infof("NewClientContext returned error: %v", err)
100-
return nil, err
101-
}
102-
s.ctx = ctx
103-
glog.V(5).Info("NewClientContext successful")
104-
return outputToken, nil
120+
if needsInit := s.cred == nil || s.ctx == nil; needsInit {
121+
logSSPI("Start SSPI flow: %s", requestURL)
122+
return s.initContext(requestURL)
105123
}
106124

107125
glog.V(5).Info("Continue SSPI flow")
108-
109-
complete, outputToken, err := s.ctx.Update(challengeToken)
110-
if err != nil {
111-
glog.V(5).Infof("context Update returned error: %v", err)
112-
return nil, err
113-
}
114-
// TODO we need a way to verify s.ctx.sctxt.EstablishedFlags matches s.ctx.sctxt.RequestedFlags (s.flags)
115-
// we will need to update upstream to add the verification or use reflection hacks here
116-
s.complete = complete
117-
glog.V(5).Infof("context Update successful, complete=%v", s.complete)
118-
return outputToken, nil
126+
return s.updateContext(challengeToken)
119127
}
120128

121129
func (s *sspiNegotiator) IsComplete() bool {
@@ -126,63 +134,175 @@ func (s *sspiNegotiator) Release() error {
126134
defer runtime.HandleCrash()
127135
glog.V(5).Info("Attempt to release SSPI")
128136
var errs []error
129-
if s.ctx != nil {
130-
if err := s.ctx.Release(); err != nil {
131-
glog.V(5).Infof("SSPI context release failed: %v", err)
132-
errs = append(errs, err)
133-
}
137+
if err := s.ctx.Release(); err != nil {
138+
logSSPI("SSPI context release failed: %v", err)
139+
errs = append(errs, err)
134140
}
135-
if s.cred != nil {
136-
if err := s.cred.Release(); err != nil {
137-
glog.V(5).Infof("SSPI credential release failed: %v", err)
138-
errs = append(errs, err)
139-
}
141+
if err := s.cred.Release(); err != nil {
142+
logSSPI("SSPI credential release failed: %v", err)
143+
errs = append(errs, err)
140144
}
141-
if len(errs) == 1 {
142-
return errs[0]
145+
return errors.Reduce(errors.NewAggregate(errs))
146+
}
147+
148+
func (s *sspiNegotiator) initContext(requestURL string) (outputToken []byte, err error) {
149+
cred, err := s.getUserCredentials()
150+
if err != nil {
151+
logSSPI("getUserCredentials failed: %v", err)
152+
return nil, err
143153
}
144-
return errors.NewAggregate(errs)
154+
s.cred = cred
155+
glog.V(5).Info("getUserCredentials successful")
156+
157+
serviceName, err := getServiceName('/', requestURL)
158+
if err != nil {
159+
return nil, err
160+
}
161+
162+
logSSPI("importing service name %s", serviceName)
163+
ctx, outputToken, err := negotiate.NewClientContextWithFlags(s.cred, serviceName, s.desiredFlags)
164+
if err != nil {
165+
logSSPI("NewClientContextWithFlags failed: %v", err)
166+
return nil, err
167+
}
168+
s.ctx = ctx
169+
glog.V(5).Info("NewClientContextWithFlags successful")
170+
return outputToken, nil
145171
}
146172

147173
func (s *sspiNegotiator) getUserCredentials() (*sspi.Credentials, error) {
174+
if len(s.principalName) == 0 && len(s.password) > 0 {
175+
return nil, fmt.Errorf("username cannot be empty with non-empty password")
176+
}
177+
148178
// Try to use principalName if specified
149179
if len(s.principalName) > 0 {
150-
domain, username, err := s.splitDomainAndUsername()
180+
domain, username, err := s.getDomainAndUsername()
181+
if err != nil {
182+
return nil, err
183+
}
184+
password, err := s.getPassword(domain, username)
151185
if err != nil {
152186
return nil, err
153187
}
154-
glog.V(5).Infof(
155-
"Using AcquireUserCredentials because principalName is not empty, principalName=%s, username=%s, domain=%s",
188+
189+
logSSPI("Using AcquireUserCredentials because principalName is not empty, principalName=%s, username=%s, domain=%s",
156190
s.principalName, username, domain)
157-
cred, err := negotiate.AcquireUserCredentials(domain, username, s.password)
191+
// this call seems to never fail, even when domain / username / password are nonsense
192+
cred, err := negotiate.AcquireUserCredentials(domain, username, password)
158193
if err != nil {
159-
glog.V(5).Infof("AcquireUserCredentials failed: %v", err)
194+
logSSPI("AcquireUserCredentials failed: %v", err)
160195
return nil, err
161196
}
162197
glog.V(5).Info("AcquireUserCredentials successful")
163198
return cred, nil
164199
}
200+
165201
glog.V(5).Info("Using AcquireCurrentUserCredentials because principalName is empty")
166202
return negotiate.AcquireCurrentUserCredentials()
167203
}
168204

169-
func (s *sspiNegotiator) splitDomainAndUsername() (string, string, error) {
170-
data := strings.Split(s.principalName, domainSeparator)
171-
if len(data) != 2 {
172-
return "", "", fmt.Errorf(`invalid username %s, must be in Fully Qualified User Name format (ex: DOMAIN\Username)`,
173-
s.principalName)
174-
}
175-
domain := data[0]
176-
username := data[1]
177-
if domainLen,
178-
usernameLen,
179-
passwordLen := len(domain),
180-
len(username),
181-
len(s.password); domainLen > maxDomain || usernameLen > maxUsername || passwordLen > maxPassword {
182-
return "", "", fmt.Errorf(
183-
"the maximum character lengths for user name, password, and domain are 256, 256, and 15, respectively:\n"+
184-
"fully qualifed username=%s username=%s,len=%d domain=%s,len=%d password=<redacted>,len=%d",
185-
s.principalName, username, usernameLen, domain, domainLen, passwordLen)
205+
func (s *sspiNegotiator) getDomainAndUsername() (domain, username string, err error) {
206+
switch {
207+
case strings.Contains(s.principalName, domainSeparator):
208+
data := strings.Split(s.principalName, domainSeparator)
209+
// try to provide useful error messages
210+
if len(data) != 2 || len(data[1]) == 0 {
211+
return "", "", fmt.Errorf(
212+
`invalid username %s, fully qualified user name format must have single backslash and non-empty user (ex: DOMAIN\Username)`,
213+
s.principalName)
214+
}
215+
domain = data[0]
216+
username = data[1]
217+
218+
case strings.Contains(s.principalName, upnSeparator):
219+
// leave domain empty and assume it is qualified in the username (UPN format)
220+
username = s.principalName
221+
222+
default:
223+
// this is a short name meaning we will need to lookup the current user's domain
224+
// TODO should we use syscall.NetGetJoinInformation first and then fallback to the env var?
225+
domain, _ = os.LookupEnv(shortDomainEnvVar)
226+
username = s.principalName
186227
}
228+
229+
// try to provide useful error messages
230+
if domainLen, usernameLen := len(domain), len(username); domainLen > maxDomain || usernameLen > maxUsername {
231+
return "", "",
232+
fmt.Errorf("the maximum character lengths for user name and domain are %d and %d, respectively:\n"+
233+
"input username=%s username=%s,len=%d domain=%s,len=%d",
234+
maxUsername, maxDomain, s.principalName, username, usernameLen, domain, domainLen)
235+
}
236+
187237
return domain, username, nil
188238
}
239+
240+
func (s *sspiNegotiator) getPassword(domain, username string) (string, error) {
241+
password := s.password
242+
243+
if missingPassword := len(password) == 0; missingPassword {
244+
// mimic output from basic auth prompt
245+
if hasDomain := len(domain) > 0; hasDomain {
246+
fmt.Fprintf(s.writer, "Authentication required for %s (%s)\n", s.host, domain)
247+
} else {
248+
fmt.Fprintf(s.writer, "Authentication required for %s\n", s.host)
249+
}
250+
fmt.Fprintf(s.writer, "Username: %s\n", username)
251+
// empty password from prompt is ok
252+
// we do not need to worry about being stuck in a prompt loop because ClientContext.Update
253+
// will fail if the password is incorrect and that will end the challenge flow
254+
password = term.PromptForPasswordString(s.reader, s.writer, "Password: ")
255+
}
256+
257+
// try to provide useful error messages
258+
if passwordLen := len(password); passwordLen > maxPassword {
259+
return "", fmt.Errorf("the maximum character length for password is %d: password=<redacted>,len=%d",
260+
maxPassword, passwordLen)
261+
}
262+
263+
return password, nil
264+
}
265+
266+
func (s *sspiNegotiator) updateContext(challengeToken []byte) (outputToken []byte, err error) {
267+
// ClientContext.Update does not return errors for success codes:
268+
// 1. sspi.SEC_E_OK (complete=true and err=nil)
269+
// 2. sspi.SEC_I_CONTINUE_NEEDED (complete=false and err=nil)
270+
// 3. sspi.SEC_I_COMPLETE_AND_CONTINUE and sspi.SEC_I_COMPLETE_NEEDED
271+
// complete=false and err=nil as long as sspi.CompleteAuthToken returns sspi.SEC_E_OK
272+
// Thus we can safely assume that any error returned here is an error code
273+
authCompleted, outputToken, err := s.ctx.Update(challengeToken)
274+
if err != nil {
275+
logSSPI("ClientContext.Update failed: %v", err)
276+
return nil, err
277+
}
278+
s.complete = authCompleted
279+
logSSPI("ClientContext.Update successful, complete=%v", s.complete)
280+
281+
// TODO should we skip the flag check if complete = true?
282+
if nonFatalErr := s.ctx.VerifyFlags(); nonFatalErr == nil {
283+
glog.V(5).Info("ClientContext.VerifyFlags successful")
284+
} else {
285+
logSSPI("ClientContext.VerifyFlags failed: %v", nonFatalErr)
286+
if fatalErr := s.ctx.VerifySelectiveFlags(s.requiredFlags); fatalErr != nil {
287+
logSSPI("ClientContext.VerifySelectiveFlags failed: %v", fatalErr)
288+
return nil, fatalErr
289+
}
290+
glog.V(5).Info("ClientContext.VerifySelectiveFlags successful")
291+
}
292+
293+
return outputToken, nil
294+
}
295+
296+
// logSSPI is the equivalent of glog.V(5).Infof(format, args) except it
297+
// includes error code information for any syscall.Errno contained in args
298+
func logSSPI(format string, args ...interface{}) {
299+
if glog.V(5) {
300+
for i, arg := range args {
301+
if errno, ok := arg.(syscall.Errno); ok {
302+
args[i] = fmt.Sprintf("%v, code=%#v", errno, errno)
303+
}
304+
}
305+
s := fmt.Sprintf(format, args...)
306+
glog.InfoDepth(1, s)
307+
}
308+
}

pkg/oc/util/tokencmd/negotiator_sspi_unsupported.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
package tokencmd
44

5+
import "io"
6+
57
func SSPIEnabled() bool {
68
return false
79
}
810

9-
func NewSSPINegotiator(string, string) Negotiator {
11+
func NewSSPINegotiator(string, string, string, io.Reader) Negotiator {
1012
return newUnsupportedNegotiator("SSPI")
1113
}

pkg/oc/util/tokencmd/request_token.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ func NewRequestTokenOptions(clientCfg *restclient.Config, reader io.Reader, defa
8080
handlers = append(handlers, NewNegotiateChallengeHandler(NewGSSAPINegotiator(defaultUsername)))
8181
}
8282
if SSPIEnabled() {
83-
handlers = append(handlers, NewNegotiateChallengeHandler(NewSSPINegotiator(defaultUsername, defaultPassword)))
83+
handlers = append(handlers, NewNegotiateChallengeHandler(NewSSPINegotiator(defaultUsername, defaultPassword, clientCfg.Host, reader)))
8484
}
8585
if BasicEnabled() {
8686
handlers = append(handlers, &BasicChallengeHandler{Host: clientCfg.Host, Reader: reader, Username: defaultUsername, Password: defaultPassword})

0 commit comments

Comments
 (0)