-
Notifications
You must be signed in to change notification settings - Fork 4
/
radius.go
148 lines (134 loc) · 4.91 KB
/
radius.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
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
// Package radiusauth provides HTTP Basic Authentication for Caddy against
// RFC2865 RADIUS Servers
//
// Uses standard HTTP Basic Authentication authorization headers with user
// credential authentication performed by a RADIUS server. Path filtering
// [except|only] allows toggling authentication on a per path basis.
//
// A local authentication cache is utilized to reduce repeat RADIUS calls.
package radiusauth
import (
"errors"
"fmt"
"net/http"
"time"
"github.com/boltdb/bolt"
"github.com/jamesboswell/radius"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
// RADIUS is middleware to protect resources with a username and password.
// HTTP Basic Authentication is performed with username and password being
// authenticated against a RADIUS server. Local caching is performed to reduce
// the number of RADIUS calls
//
// Note that HTTP Basic Authentication is not secure by itself and should
// not be used to protect important assets without HTTPS. Even then, the
// security of HTTP Basic Auth is disputed. Use discretion when deciding
// what to protect with BasicAuth.
type RADIUS struct {
// Connection
Next httpserver.Handler
SiteRoot string
Config radiusConfig
db *bolt.DB
}
type radiusConfig struct {
Server []string
Secret string
Timeout int
Retries int
nasid string
realm string
requestFilter filter
cache string
cachetimeout time.Duration
}
// ServeHTTP implements the httpserver.Handler interface.
func (a RADIUS) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
// Pass-through when no paths match filter or no filters
// if filter not nil and auth is NOT required, then just return
if a.Config.requestFilter != nil && !a.Config.requestFilter.shouldAuthenticate(r) {
return a.Next.ServeHTTP(w, r)
}
// Check for HTTP Basic Authorization Headers and valid username, password
username, password, ok := r.BasicAuth()
realm := "Basic realm=" + fmt.Sprintf("\"%s\"", a.Config.realm)
if !ok {
w.Header().Set("WWW-Authenticate", realm)
return http.StatusUnauthorized, nil
}
if username == "" || password == "" {
w.Header().Set("WWW-Authenticate", realm)
return http.StatusUnauthorized, errors.New("[radiusauth] Blank username or password")
}
// Capture username into {user} placeholder for caddyfile log directive
// ex: log / stdout "{remote} - {user} [{when}] {method} {uri} {proto} {status} {size}"
// or: log / stdout "{combined}"
repl := httpserver.NewReplacer(r, nil, "-")
repl.Set("user", username)
// cacheseek checks if provided Basic Auth credentials are cached and match
// if credentials do not match cached entry, force RADIUS authentication
isCached, err := cacheSeek(a, username, password)
if isCached == true && err == nil {
return a.Next.ServeHTTP(w, r)
}
if err != nil {
fmt.Println(err)
}
// send username, password to RADIUS server(s) for authentication
// returns isAuthenticated if authentication successful
// err if no RADIUS servers respond
isAuthenticated, err := auth(a.Config, username, password)
// Return 500 Internal Server Error
// if connection to all RADIUS servers has failed
if err != nil {
return http.StatusInternalServerError, err
}
// if RADIUS authentication failed, return 401
if !isAuthenticated {
w.Header().Set("WWW-Authenticate", realm)
return http.StatusUnauthorized, nil
}
// if RADIUS authenticated, cache the username, password entry return Handler
if isAuthenticated {
if err := cacheWrite(a, username, password); err != nil {
return http.StatusInternalServerError, fmt.Errorf("[radiusauth] cache-write for %s FAILED: %s", username, err)
}
}
return a.Next.ServeHTTP(w, r)
}
// auth generates a RADIUS authentication request for username
func auth(r radiusConfig, username string, password string) (bool, error) {
// Create a new RADIUS packet for Access-Request
// NAS-Identifier required by some servers such as CiscoSecure ACS
packet := radius.New(radius.CodeAccessRequest, []byte(r.Secret))
packet.Add("User-Name", username)
packet.Add("User-Password", password)
packet.Add("NAS-Identifier", r.nasid)
client := radius.Client{
DialTimeout: 3 * time.Second, // TODO user defined timeouts
ReadTimeout: 3 * time.Second,
}
// Loop through all configured RADIUS servers until
// Access-Accept or Access-Reject or all servers exhausted
for s, radiusServer := range r.Server {
reply, err := client.Exchange(packet, radiusServer)
if reply != nil && reply.Code == radius.CodeAccessReject {
return false, nil
}
if err != nil {
fmt.Println(err) // TODO need a way to hook into Caddy error log
// Return err if all servers in pool have failed
if s == len(r.Server)-1 {
return false, fmt.Errorf("[radiusauth] All RADIUS servers failed")
}
continue
}
// RADIUS Access-Accept is a successful authentication
if reply.Code == radius.CodeAccessAccept {
return true, nil
}
}
// Any other reply is a failed authentication
return false, nil
}