-
Notifications
You must be signed in to change notification settings - Fork 3
/
auth.go
116 lines (99 loc) · 3.03 KB
/
auth.go
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
// Copyright (c) ClaceIO, LLC
// SPDX-License-Identifier: Apache-2.0
package server
import (
"crypto/sha512"
"crypto/subtle"
"encoding/base64"
"strings"
"sync"
"time"
"github.com/claceio/clace/internal/utils"
"golang.org/x/crypto/bcrypt"
)
// AdminBasicAuth implements basic auth for the admin user account.
// Cache the success auth header to avoid the bcrypt hash check penalty
// Basic auth is supported for admin user only, and changing it requires service restart.
// Caching the sha of the successful auth header allows us to skip the bcrypt check
// which significantly improves performance.
type AdminBasicAuth struct {
*utils.Logger
config *utils.ServerConfig
mu sync.RWMutex
authShaCached string
}
func NewAdminBasicAuth(logger *utils.Logger, config *utils.ServerConfig) *AdminBasicAuth {
return &AdminBasicAuth{
Logger: logger,
config: config,
}
}
func (a *AdminBasicAuth) authenticate(authHeader string) bool {
a.mu.RLock()
authShaCopy := a.authShaCached
a.mu.RUnlock()
if authShaCopy != "" {
inputSha := sha512.Sum512([]byte(authHeader))
inputShaSlice := inputSha[:]
if subtle.ConstantTimeCompare(inputShaSlice, []byte(authShaCopy)) != 1 {
a.Warn().Msg("Auth header cache check failed")
time.Sleep(300 * time.Millisecond) // slow down brute force attacks
return false
}
// Cached header matches, so we can skip the rest of the auth checks
return true
}
user, pass, ok := a.BasicAuth(authHeader)
if !ok {
return false
}
if a.config.AdminUser == "" {
a.Warn().Msg("No admin username specified, basic auth not available")
return false
}
if subtle.ConstantTimeCompare([]byte(a.config.AdminUser), []byte(user)) != 1 {
a.Warn().Msg("Admin username does not match")
return false
}
err := bcrypt.CompareHashAndPassword([]byte(a.config.Security.AdminPasswordBcrypt), []byte(pass))
if err != nil {
a.Warn().Err(err).Msg("Password match failed")
time.Sleep(100 * time.Millisecond) // slow down brute force attacks
return false
}
a.mu.RLock()
authShaCopy = a.authShaCached
a.mu.RUnlock()
if authShaCopy == "" {
// Successful request, so we can cache the auth header
a.mu.Lock()
inputSha := sha512.Sum512([]byte(authHeader))
a.authShaCached = string(inputSha[:])
a.mu.Unlock()
}
return true
}
func (a *AdminBasicAuth) BasicAuth(authHeader string) (username, password string, ok bool) {
if authHeader == "" {
return "", "", false
}
return a.parseBasicAuth(authHeader)
}
// parseBasicAuth parses an HTTP Basic Authentication string.
// "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" returns ("Aladdin", "open sesame", true).
func (a *AdminBasicAuth) parseBasicAuth(auth string) (username, password string, ok bool) {
const prefix = "Basic "
if subtle.ConstantTimeCompare([]byte(auth[:len(prefix)]), []byte(prefix)) != 1 {
return "", "", false
}
c, err := base64.StdEncoding.DecodeString(auth[len(prefix):])
if err != nil {
return "", "", false
}
cs := string(c)
username, password, ok = strings.Cut(cs, ":")
if !ok {
return "", "", false
}
return username, password, true
}