@@ -4,29 +4,51 @@ package tokencmd
4
4
5
5
import (
6
6
"fmt"
7
+ "io"
8
+ "os"
7
9
"strings"
10
+ "syscall"
8
11
9
12
"k8s.io/apimachinery/pkg/util/errors"
10
13
"k8s.io/apimachinery/pkg/util/runtime"
11
14
15
+ "github.com/openshift/origin/pkg/cmd/util/term"
16
+
12
17
"github.com/alexbrainman/sspi"
13
18
"github.com/alexbrainman/sspi/negotiate"
14
19
"github.com/golang/glog"
15
20
)
16
21
17
22
const (
18
- // sane set of default flags, see sspiNegotiator.flags
23
+ // sane set of default flags, see sspiNegotiator.desiredFlags
19
24
// TODO make configurable?
20
- flags = sspi .ISC_REQ_CONFIDENTIALITY |
25
+ desiredFlags = sspi .ISC_REQ_CONFIDENTIALITY |
21
26
sspi .ISC_REQ_INTEGRITY |
22
27
sspi .ISC_REQ_MUTUAL_AUTH |
23
28
sspi .ISC_REQ_REPLAY_DETECT |
24
29
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
25
35
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)
27
40
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"
28
48
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.
30
52
maxUsername = 256
31
53
maxPassword = 256
32
54
maxDomain = 15
@@ -39,15 +61,22 @@ func SSPIEnabled() bool {
39
61
// sspiNegotiator handles negotiate flows on windows via SSPI
40
62
// It expects sspiNegotiator.InitSecContext to be called until sspiNegotiator.IsComplete returns true
41
63
type sspiNegotiator struct {
42
- // optional DOMAIN\Username and password
64
+ // principalName is an optional username (in fully qualified, user principal name or short format).
43
65
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa374714(v=vs.85).aspx
44
66
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa380131(v=vs.85).aspx
45
67
// pAuthData [in]: If credentials are supplied, they are passed via a pointer to a sspi.SEC_WINNT_AUTH_IDENTITY
46
68
// 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.
49
69
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
51
80
52
81
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms721572(v=vs.85).aspx#_security_credentials_gly
53
82
// phCredential [in, optional]: A handle to the credentials returned by AcquireCredentialsHandle (Negotiate).
@@ -58,15 +87,25 @@ type sspiNegotiator struct {
58
87
ctx * negotiate.ClientContext
59
88
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa375509(v=vs.85).aspx
60
89
// 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
62
93
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa375509(v=vs.85).aspx
63
94
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa374764(v=vs.85).aspx
64
95
// Set to true once InitializeSecurityContext or CompleteAuthToken return sspi.SEC_E_OK
65
96
complete bool
66
97
}
67
98
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
+ }
70
109
}
71
110
72
111
func (s * sspiNegotiator ) Load () error {
@@ -75,47 +114,16 @@ func (s *sspiNegotiator) Load() error {
75
114
return nil
76
115
}
77
116
78
- func (s * sspiNegotiator ) InitSecContext (requestURL string , challengeToken []byte ) ([]byte , error ) {
117
+ func (s * sspiNegotiator ) InitSecContext (requestURL string , challengeToken []byte ) (tokenToSend []byte , err error ) {
79
118
defer runtime .HandleCrash ()
80
- if s .cred == nil || s .ctx == nil {
81
- glog .V (5 ).Infof ("Start SSPI flow: %s" , requestURL )
82
119
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 )
105
123
}
106
124
107
125
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 )
119
127
}
120
128
121
129
func (s * sspiNegotiator ) IsComplete () bool {
@@ -126,63 +134,175 @@ func (s *sspiNegotiator) Release() error {
126
134
defer runtime .HandleCrash ()
127
135
glog .V (5 ).Info ("Attempt to release SSPI" )
128
136
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 )
134
140
}
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 )
140
144
}
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
143
153
}
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
145
171
}
146
172
147
173
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
+
148
178
// Try to use principalName if specified
149
179
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 )
151
185
if err != nil {
152
186
return nil , err
153
187
}
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" ,
156
190
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 )
158
193
if err != nil {
159
- glog . V ( 5 ). Infof ("AcquireUserCredentials failed: %v" , err )
194
+ logSSPI ("AcquireUserCredentials failed: %v" , err )
160
195
return nil , err
161
196
}
162
197
glog .V (5 ).Info ("AcquireUserCredentials successful" )
163
198
return cred , nil
164
199
}
200
+
165
201
glog .V (5 ).Info ("Using AcquireCurrentUserCredentials because principalName is empty" )
166
202
return negotiate .AcquireCurrentUserCredentials ()
167
203
}
168
204
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
186
227
}
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
+
187
237
return domain , username , nil
188
238
}
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
+ }
0 commit comments