-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
-
23
-
24
-
25
-
26
-
27
-
28
-
29
-
30
-
31
-
32
-
33
-
34
-
35
-
36
-
37
-
38
-
39
-
40
-
41
-
42
-
43
-
44
-
45
-
46
-
47
-
48
-
49
-
50
-
51
-
52
-
53
-
54
-
55
-
56
-
57
-
58
-
59
-
60
-
61
-
62
-
63
-
64
-
65
-
66
-
67
-
68
-
69
-
70
-
71
-
72
-
73
-
74
-
75
-
76
-
77
-
78
-
79
-
80
-
81
-
82
-
83
-
84
-
85
-
86
-
87
-
88
-
89
-
90
-
91
-
92
-
93
-
94
-
95
-
96
-
97
-
98
-
99
-
100
-
101
-
102
-
103
-
104
-
105
-
106
-
107
-
108
-
109
-
110
-
111
-
112
-
113
-
114
-
115
-
116
-
117
-
118
-
119
-
120
-
121
-
122
-
123
-
124
-
125
-
126
-
127
-
128
-
129
-
130
-
131
-
132
-
133
-
134
-
135
-
136
-
137
-
138
-
139
-
140
-
141
-
142
-
143
-
144
-
145
-
146
-
147
-
148
-
149
-
150
-
151
-
152
-
153
-
154
-
155
-
156
-
157
-
158
-
159
-
160
-
161
-
162
-
163
-
164
-
165
-
166
-
167
-
168
-
169
-
170
-
171
-
172
-
173
-
174
-
175
-
176
-
177
-
178
-
179
-
180
-
181
-
182
-
183
-
184
-
185
-
186
-
187
-
188
-
189
-
190
-
191
-
192
-
193
-
194
-
195
-
196
-
197
-
198
-
199
-
200
-
201
-
202
-
203
-
204
-
205
-
206
-
207
-
208
-
209
-
210
-
211
-
212
-
213
-
214
-
215
-
216
-
217
-
218
-
219
-
220
-
221
-
222
-
223
-
224
-
225
-
226
-
227
-
228
-
229
-
230
-
231
-
232
-
233
-
234
-
235
-
236
-
237
-
238
-
239
-
240
-
241
-
242
-
243
-
244
-
245
-
246
-
247
-
248
-
249
-
250
-
251
-
252
-
253
-
254
-
255
-
256
-
257
-
258
-
259
-
260
-
261
-
262
-
263
-
264
-
265
-
266
-
267
-
268
-
269
-
270
-
271
-
272
-
273
-
274
-
275
-
276
-
277
-
278
-
279
-
280
-
281
-
282
-
283
-
284
-
285
-
286
-
287
-
288
-
289
-
290
-
291
-
292
-
293
-
294
-
295
-
296
-
297
-
298
-
299
-
300
-
301
-
302
-
303
-
304
-
305
-
306
-
307
-
308
-
309
-
310
-
311
-
312
-
313
-
314
-
315
-
316
-
317
-
318
-
319
-
320
-
321
-
322
-
323
-
324
-
325
-
326
-
327
-
328
-
329
-
330
-
331
-
332
-
333
-
334
-
335
-
336
-
337
-
338
-
339
-
340
-
341
-
342
-
343
-
344
-
345
-
346
-
347
-
348
-
349
-
350
-
351
-
352
-
353
-
354
-
355
-
356
-
357
-
358
-
359
-
360
-
361
-
362
-
363
-
364
-
365
-
366
-
367
-
368
-
369
-
370
-
371
-
372
-
373
-
374
-
375
-
376
-
377
-
378
-
379
-
380
-
381
-
382
-
383
-
384
-
385
-
386
-
387
-
388
-
389
-
390
-
391
-
392
-
393
-
394
-
395
-
396
-
397
-
398
-
399
-
400
-
401
-
402
-
403
-
404
-
405
-
406
-
407
-
408
-
409
-
410
-
411
-
412
-
413
-
414
-
415
-
416
-
417
-
418
-
419
-
420
-
421
-
422
-
423
-
424
-
425
-
426
-
427
-
428
-
429
-
430
-
431
-
432
-
433
-
434
-
435
-
436
-
437
-
438
-
439
-
440
-
441
-
442
-
443
-
444
-
445
-
446
-
447
-
448
-
449
-
450
-
451
-
452
-
453
-
454
-
455
-
456
-
457
-
458
-
459
-
460
-
461
-
462
-
463
-
464
-
465
-
466
-
467
-
468
-
469
-
470
-
471
-
472
-
473
-
474
-
475
-
476
-
477
-
478
-
479
-
480
-
481
-
482
-
483
-
484
-
485
-
486
-
487
-
488
-
489
-
490
-
491
-
492
-
493
-
494
-
495
-
496
-
497
-
498
-
499
-
500
-
501
-
502
-
503
-
504
-
505
-
506
-
507
-
508
-
509
-
510
-
511
-
512
-
513
-
514
-
515
-
516
-
517
-
518
-
519
-
520
-
521
-
522
-
523
-
524
-
525
-
526
-
527
-
528
-
529
-
530
-
531
-
532
-
533
-
534
-
535
-
536
-
537
-
538
-
539
-
540
-
541
-
542
-
543
-
544
-
545
-
546
-
547
-
548
-
549
-
550
-
551
-
552
-
553
-
554
-
555
-
556
-
557
-
558
-
559
-
560
-
561
-
562
-
563
-
564
-
565
-
566
-
567
-
568
-
569
-
570
-
571
-
572
-
573
-
574
-
575
-
576
-
577
-
578
-
579
-
580
-
581
-
582
-
583
-
584
-
585
-
586
-
587
-
588
-
589
-
590
-
591
-
592
-
593
-
594
-
595
-
596
-
597
-
598
-
599
-
600
-
601
-
602
-
603
-
604
-
605
-
606
-
607
-
608
-
609
-
610
-
611
-
612
-
613
-
614
-
615
package websspi
import (
"context"
"encoding/base64"
"encoding/gob"
"errors"
"fmt"
"log"
"net/http"
"strings"
"sync"
"syscall"
"time"
"unsafe"
"github.com/quasoft/websspi/secctx"
)
// The Config object determines the behaviour of the Authenticator.
type Config struct {
contextStore secctx.Store
authAPI API
KrbPrincipal string // Name of Kerberos principle used by the service (optional).
AuthUserKey string // Key of header to fill with authenticated username, eg. "X-Authenticated-User" or "REMOTE_USER" (optional).
EnumerateGroups bool // If true, groups the user is a member of are enumerated and stored in request context (default false)
ServerName string // Specifies the DNS or NetBIOS name of the remote server which to query about user groups. Ignored if EnumerateGroups is false.
}
// NewConfig creates a configuration object with default values.
func NewConfig() *Config {
return &Config{
contextStore: secctx.NewCookieStore(),
authAPI: &Win32{},
}
}
// Validate makes basic validation of configuration to make sure that important and required fields
// have been set with values in expected format.
func (c *Config) Validate() error {
if c.contextStore == nil {
return errors.New("Store for context handles not specified in Config")
}
if c.authAPI == nil {
return errors.New("Authentication API not specified in Config")
}
return nil
}
// contextKey represents a custom key for values stored in context.Context
type contextKey string
func (c contextKey) String() string {
return "websspi-key-" + string(c)
}
var (
UserInfoKey = contextKey("UserInfo")
)
// The Authenticator type provides middleware methods for authentication of http requests.
// A single authenticator object can be shared by concurrent goroutines.
type Authenticator struct {
Config Config
serverCred *CredHandle
credExpiry *time.Time
ctxList []CtxtHandle
ctxListMux *sync.Mutex
}
// New creates a new Authenticator object with the given configuration options.
func New(config *Config) (*Authenticator, error) {
err := config.Validate()
if err != nil {
return nil, fmt.Errorf("invalid config: %v", err)
}
var auth = &Authenticator{
Config: *config,
ctxListMux: &sync.Mutex{},
}
err = auth.PrepareCredentials(config.KrbPrincipal)
if err != nil {
return nil, fmt.Errorf("could not acquire credentials handle for the service: %v", err)
}
log.Printf("Credential handle expiry: %v\n", *auth.credExpiry)
return auth, nil
}
// PrepareCredentials method acquires a credentials handle for the specified principal
// for use during the live of the application.
// On success stores the handle in the serverCred field and its expiry time in the
// credExpiry field.
// This method must be called once - when the application is starting or when the first
// request from a client is received.
func (a *Authenticator) PrepareCredentials(principal string) error {
var principalPtr *uint16
if principal != "" {
var err error
principalPtr, err = syscall.UTF16PtrFromString(principal)
if err != nil {
return err
}
}
credentialUsePtr, err := syscall.UTF16PtrFromString(NEGOSSP_NAME)
if err != nil {
return err
}
var handle CredHandle
var expiry syscall.Filetime
status := a.Config.authAPI.AcquireCredentialsHandle(
principalPtr,
credentialUsePtr,
SECPKG_CRED_INBOUND,
nil, // logonId
nil, // authData
0, // getKeyFn
0, // getKeyArgument
&handle,
&expiry,
)
if status != SEC_E_OK {
return fmt.Errorf("call to AcquireCredentialsHandle failed with code 0x%x", status)
}
expiryTime := time.Unix(0, expiry.Nanoseconds())
a.credExpiry = &expiryTime
a.serverCred = &handle
return nil
}
// Free method should be called before shutting down the server to let
// it release allocated Win32 resources
func (a *Authenticator) Free() error {
var status SECURITY_STATUS
a.ctxListMux.Lock()
for _, ctx := range a.ctxList {
// TODO: Also check for stale security contexts and delete them periodically
status = a.Config.authAPI.DeleteSecurityContext(&ctx)
if status != SEC_E_OK {
return fmt.Errorf("call to DeleteSecurityContext failed with code 0x%x", status)
}
}
a.ctxList = nil
a.ctxListMux.Unlock()
if a.serverCred != nil {
status = a.Config.authAPI.FreeCredentialsHandle(a.serverCred)
if status != SEC_E_OK {
return fmt.Errorf("call to FreeCredentialsHandle failed with code 0x%x", status)
}
a.serverCred = nil
}
return nil
}
// StoreCtxHandle stores the specified context to the internal list (ctxList)
func (a *Authenticator) StoreCtxHandle(handle *CtxtHandle) {
if handle == nil || *handle == (CtxtHandle{}) {
// Should not add nil or empty handle
return
}
a.ctxListMux.Lock()
defer a.ctxListMux.Unlock()
a.ctxList = append(a.ctxList, *handle)
}
// ReleaseCtxHandle deletes a context handle and removes it from the internal list (ctxList)
func (a *Authenticator) ReleaseCtxHandle(handle *CtxtHandle) error {
if handle == nil || *handle == (CtxtHandle{}) {
// Removing a nil or empty handle is not an error condition
return nil
}
a.ctxListMux.Lock()
defer a.ctxListMux.Unlock()
// First, try to delete the handle
status := a.Config.authAPI.DeleteSecurityContext(handle)
if status != SEC_E_OK {
return fmt.Errorf("call to DeleteSecurityContext failed with code 0x%x", status)
}
// Then remove it from the internal list
foundAt := -1
for i, ctx := range a.ctxList {
if ctx == *handle {
foundAt = i
break
}
}
if foundAt > -1 {
a.ctxList[foundAt] = a.ctxList[len(a.ctxList)-1]
a.ctxList = a.ctxList[:len(a.ctxList)-1]
}
return nil
}
// AcceptOrContinue tries to validate the auth-data token by calling the AcceptSecurityContext
// function and returns and error if validation failed or continuation of the negotiation is needed.
// No error is returned if the token was validated (user was authenticated).
func (a *Authenticator) AcceptOrContinue(context *CtxtHandle, authData []byte) (newCtx *CtxtHandle, out []byte, exp *time.Time, err error) {
if authData == nil {
err = errors.New("input token cannot be nil")
return
}
var inputDesc SecBufferDesc
var inputBuf SecBuffer
inputDesc.BuffersCount = 1
inputDesc.Version = SECBUFFER_VERSION
inputDesc.Buffers = &inputBuf
inputBuf.BufferSize = uint32(len(authData))
inputBuf.BufferType = SECBUFFER_TOKEN
inputBuf.Buffer = &authData[0]
var outputDesc SecBufferDesc
var outputBuf SecBuffer
outputDesc.BuffersCount = 1
outputDesc.Version = SECBUFFER_VERSION
outputDesc.Buffers = &outputBuf
outputBuf.BufferSize = 0
outputBuf.BufferType = SECBUFFER_TOKEN
outputBuf.Buffer = nil
var expiry syscall.Filetime
var contextAttr uint32
var newContextHandle CtxtHandle
var status = a.Config.authAPI.AcceptSecurityContext(
a.serverCred,
context,
&inputDesc,
ASC_REQ_ALLOCATE_MEMORY|ASC_REQ_MUTUAL_AUTH|ASC_REQ_CONFIDENTIALITY|
ASC_REQ_INTEGRITY|ASC_REQ_REPLAY_DETECT|ASC_REQ_SEQUENCE_DETECT, // contextReq uint32,
SECURITY_NATIVE_DREP, // targDataRep uint32,
&newContextHandle,
&outputDesc, // *SecBufferDesc
&contextAttr, // contextAttr *uint32,
&expiry, // *syscall.Filetime
)
if newContextHandle.Lower != 0 || newContextHandle.Upper != 0 {
newCtx = &newContextHandle
}
tm := time.Unix(0, expiry.Nanoseconds())
exp = &tm
if status == SEC_E_OK || status == SEC_I_CONTINUE_NEEDED {
// Copy outputBuf.Buffer to out and free the outputBuf.Buffer
out = make([]byte, outputBuf.BufferSize)
var bufPtr = uintptr(unsafe.Pointer(outputBuf.Buffer))
for i := 0; i < len(out); i++ {
out[i] = *(*byte)(unsafe.Pointer(bufPtr))
bufPtr++
}
}
if outputBuf.Buffer != nil {
freeStatus := a.Config.authAPI.FreeContextBuffer(outputBuf.Buffer)
if freeStatus != SEC_E_OK {
status = freeStatus
err = fmt.Errorf("could not free output buffer; FreeContextBuffer() failed with code: 0x%x", freeStatus)
return
}
}
if status == SEC_I_CONTINUE_NEEDED {
err = errors.New("Negotiation should continue")
return
} else if status != SEC_E_OK {
err = fmt.Errorf("call to AcceptSecurityContext failed with code 0x%x", status)
return
}
// TODO: Check contextAttr?
return
}
// GetCtxHandle retrieves the context handle for this client from request's cookies
func (a *Authenticator) GetCtxHandle(r *http.Request) (*CtxtHandle, error) {
sessionHandle, err := a.Config.contextStore.GetHandle(r)
if err != nil {
return nil, fmt.Errorf("could not get context handle from session: %s", err)
}
if contextHandle, ok := sessionHandle.(*CtxtHandle); ok {
log.Printf("CtxHandle: 0x%x\n", *contextHandle)
if contextHandle.Lower == 0 && contextHandle.Upper == 0 {
return nil, nil
}
return contextHandle, nil
}
log.Printf("CtxHandle: nil\n")
return nil, nil
}
// SetCtxHandle stores the context handle for this client to cookie of response
func (a *Authenticator) SetCtxHandle(r *http.Request, w http.ResponseWriter, newContext *CtxtHandle) error {
// Store can't store nil value, so if newContext is nil, store an empty CtxHandle
ctx := &CtxtHandle{}
if newContext != nil {
ctx = newContext
}
err := a.Config.contextStore.SetHandle(r, w, ctx)
if err != nil {
return fmt.Errorf("could not save context to cookie: %s", err)
}
log.Printf("New context: 0x%x\n", *ctx)
return nil
}
// GetFlags returns the negotiated context flags
func (a *Authenticator) GetFlags(context *CtxtHandle) (uint32, error) {
var flags SecPkgContext_Flags
status := a.Config.authAPI.QueryContextAttributes(context, SECPKG_ATTR_FLAGS, (*byte)(unsafe.Pointer(&flags)))
if status != SEC_E_OK {
return 0, fmt.Errorf("QueryContextAttributes failed with status 0x%x", status)
}
return flags.Flags, nil
}
// GetUsername returns the name of the user associated with the specified security context
func (a *Authenticator) GetUsername(context *CtxtHandle) (username string, err error) {
var names SecPkgContext_Names
status := a.Config.authAPI.QueryContextAttributes(context, SECPKG_ATTR_NAMES, (*byte)(unsafe.Pointer(&names)))
if status != SEC_E_OK {
err = fmt.Errorf("QueryContextAttributes failed with status 0x%x", status)
return
}
if names.UserName != nil {
username = UTF16PtrToString(names.UserName, 2048)
status = a.Config.authAPI.FreeContextBuffer((*byte)(unsafe.Pointer(names.UserName)))
if status != SEC_E_OK {
err = fmt.Errorf("FreeContextBuffer failed with status 0x%x", status)
}
return
}
err = errors.New("QueryContextAttributes returned empty name")
return
}
// GetUserGroups returns the groups the user is a member of
func (a *Authenticator) GetUserGroups(userName string) (groups []string, err error) {
var serverNamePtr *uint16
if a.Config.ServerName != "" {
serverNamePtr, err = syscall.UTF16PtrFromString(a.Config.ServerName)
if err != nil {
return
}
}
userNamePtr, err := syscall.UTF16PtrFromString(userName)
if err != nil {
return
}
var buf *byte
var entriesRead uint32
var totalEntries uint32
err = a.Config.authAPI.NetUserGetGroups(
serverNamePtr,
userNamePtr,
0,
&buf,
MAX_PREFERRED_LENGTH,
&entriesRead,
&totalEntries,
)
if buf == nil {
err = fmt.Errorf("NetUserGetGroups(): returned nil buffer, error: %s", err)
return
}
defer func() {
freeErr := a.Config.authAPI.NetApiBufferFree(buf)
if freeErr != nil {
err = freeErr
}
}()
if err != nil {
return
}
if entriesRead < totalEntries {
err = fmt.Errorf("NetUserGetGroups(): could not read all entries, read only %d entries of %d", entriesRead, totalEntries)
return
}
ptr := uintptr(unsafe.Pointer(buf))
for i := uint32(0); i < entriesRead; i++ {
groupInfo := (*GroupUsersInfo0)(unsafe.Pointer(ptr))
groupName := UTF16PtrToString(groupInfo.Grui0_name, MAX_GROUP_NAME_LENGTH)
if groupName != "" {
groups = append(groups, groupName)
}
ptr += unsafe.Sizeof(GroupUsersInfo0{})
}
return
}
// GetUserInfo returns a structure containing the name of the user associated with the
// specified security context and the groups to which they are a member of (if Config.EnumerateGroups)
// is enabled
func (a *Authenticator) GetUserInfo(context *CtxtHandle) (*UserInfo, error) {
// Get username
username, err := a.GetUsername(context)
if err != nil {
return nil, err
}
info := UserInfo{
Username: username,
}
// Get groups
if a.Config.EnumerateGroups {
info.Groups, err = a.GetUserGroups(username)
if err != nil {
return nil, err
}
}
return &info, nil
}
// GetAuthData parses the "Authorization" header received from the client,
// extracts the auth-data token (input token) and decodes it to []byte
func (a *Authenticator) GetAuthData(r *http.Request, w http.ResponseWriter) (authData []byte, err error) {
// 1. Check if Authorization header is present
headers := r.Header["Authorization"]
if len(headers) == 0 {
err = errors.New("the Authorization header is not provided")
return
}
if len(headers) > 1 {
err = errors.New("received multiple Authorization headers, but expected only one")
return
}
authzHeader := strings.TrimSpace(headers[0])
if authzHeader == "" {
err = errors.New("the Authorization header is empty")
return
}
// 1.1. Make sure header starts with "Negotiate"
if !strings.HasPrefix(strings.ToLower(authzHeader), "negotiate") {
err = errors.New("the Authorization header does not start with 'Negotiate'")
return
}
// 2. Extract token from Authorization header
authzParts := strings.Split(authzHeader, " ")
if len(authzParts) < 2 {
err = errors.New("the Authorization header does not contain token (gssapi-data)")
return
}
token := authzParts[len(authzParts)-1]
if token == "" {
err = errors.New("the token (gssapi-data) in the Authorization header is empty")
return
}
// 3. Decode token
authData, err = base64.StdEncoding.DecodeString(token)
if err != nil {
err = errors.New("could not decode token as base64 string")
return
}
return
}
// Authenticate tries to authenticate the HTTP request and returns nil
// if authentication was successful.
// Returns error and data for continuation if authentication was not successful.
func (a *Authenticator) Authenticate(r *http.Request, w http.ResponseWriter) (userInfo *UserInfo, outToken string, err error) {
// 1. Extract auth-data from Authorization header
authData, err := a.GetAuthData(r, w)
if err != nil {
err = fmt.Errorf("could not get auth data: %s", err)
return
}
// 2. Authenticate user with provided token
contextHandle, err := a.GetCtxHandle(r)
if err != nil {
return
}
newCtx, output, _, err := a.AcceptOrContinue(contextHandle, authData)
// If a new context was created, make sure to delete it or store it
// both in internal list and response Cookie
defer func() {
// Negotiation is ending if we don't expect further responses from the client
// (authentication was successful or no output token is going to be sent back),
// clear client cookie
endOfNegotiation := err == nil || len(output) == 0
// Current context (contextHandle) is not needed anymore and should be deleted if:
// - we don't expect further responses from the client
// - a new context has been returned by AcceptSecurityContext
currCtxNotNeeded := endOfNegotiation || newCtx != nil
if !currCtxNotNeeded {
// Release current context only if its different than the new context
if contextHandle != nil && *contextHandle != *newCtx {
remErr := a.ReleaseCtxHandle(contextHandle)
if remErr != nil {
err = remErr
return
}
}
}
if endOfNegotiation {
// Clear client cookie
setErr := a.SetCtxHandle(r, w, nil)
if setErr != nil {
err = fmt.Errorf("could not clear context, error: %s", setErr)
return
}
// Delete any new context handle
remErr := a.ReleaseCtxHandle(newCtx)
if remErr != nil {
err = remErr
return
}
// Exit defer func
return
}
if newCtx != nil {
// Store new context handle to internal list and response Cookie
a.StoreCtxHandle(newCtx)
setErr := a.SetCtxHandle(r, w, newCtx)
if setErr != nil {
err = setErr
return
}
}
}()
outToken = base64.StdEncoding.EncodeToString(output)
if err != nil {
err = fmt.Errorf("AcceptOrContinue failed: %s", err)
return
}
// 3. Get username and user groups
currentCtx := newCtx
if currentCtx == nil {
currentCtx = contextHandle
}
userInfo, err = a.GetUserInfo(currentCtx)
if err != nil {
err = fmt.Errorf("could not get username, error: %s", err)
return
}
return
}
// AppendAuthenticateHeader populates WWW-Authenticate header,
// indicating to client that authentication is required and returns a 401 (Unauthorized)
// response code.
// The data parameter can be empty for the first 401 response from the server.
// For subsequent 401 responses the data parameter should contain the gssapi-data,
// which is required for continuation of the negotiation.
func (a *Authenticator) AppendAuthenticateHeader(w http.ResponseWriter, data string) {
value := "Negotiate"
if data != "" {
value += " " + data
}
w.Header().Set("WWW-Authenticate", value)
}
// Return401 populates WWW-Authenticate header, indicating to client that authentication
// is required and returns a 401 (Unauthorized) response code.
// The data parameter can be empty for the first 401 response from the server.
// For subsequent 401 responses the data parameter should contain the gssapi-data,
// which is required for continuation of the negotiation.
func (a *Authenticator) Return401(w http.ResponseWriter, data string) {
a.AppendAuthenticateHeader(w, data)
http.Error(w, "Error!", http.StatusUnauthorized)
}
// WithAuth authenticates the request. On successful authentication the request
// is passed down to the next http handler. The next handler can access information
// about the authenticated user via the GetUserName method.
// If authentication was not successful, the server returns 401 response code with
// a WWW-Authenticate, indicating that authentication is required.
func (a *Authenticator) WithAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("Authenticating request to %s\n", r.RequestURI)
user, data, err := a.Authenticate(r, w)
if err != nil {
log.Printf("Authentication failed with error: %v\n", err)
a.Return401(w, data)
return
}
log.Print("Authenticated\n")
// Add the UserInfo value to the reqest's context
r = r.WithContext(context.WithValue(r.Context(), UserInfoKey, user))
// and to the request header with key Config.AuthUserKey
if a.Config.AuthUserKey != "" {
r.Header.Set(a.Config.AuthUserKey, user.Username)
}
// The WWW-Authenticate header might need to be sent back even
// on successful authentication (eg. in order to let the client complete
// mutual authentication).
if data != "" {
a.AppendAuthenticateHeader(w, data)
}
next.ServeHTTP(w, r)
})
}
func init() {
gob.Register(&CtxtHandle{})
gob.Register(&UserInfo{})
}