-
Notifications
You must be signed in to change notification settings - Fork 189
/
Copy pathauthentication.go
211 lines (183 loc) · 6.84 KB
/
authentication.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
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
package middleware
import (
"fmt"
"io"
"net/http"
"regexp"
"strings"
"github.com/owncloud/ocis/v2/services/proxy/pkg/router"
"github.com/owncloud/ocis/v2/services/proxy/pkg/webdav"
"go.opentelemetry.io/otel/trace"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
var (
// SupportedAuthStrategies stores configured challenges.
SupportedAuthStrategies []string
// ProxyWwwAuthenticate is a list of endpoints that do not rely on reva underlying authentication, such as ocs.
// services that fallback to reva authentication are declared in the "frontend" command on oCIS. It is a list of
// regexp.Regexp which are safe to use concurrently.
ProxyWwwAuthenticate = []regexp.Regexp{*regexp.MustCompile("/ocs/v[12].php/cloud/")}
_publicPaths = [...]string{
"/dav/public-files/",
"/remote.php/dav/ocm/",
"/dav/ocm/",
"/ocm/",
"/remote.php/dav/public-files/",
"/ocs/v1.php/apps/files_sharing/api/v1/tokeninfo/unprotected",
"/ocs/v2.php/apps/files_sharing/api/v1/tokeninfo/unprotected",
"/ocs/v1.php/cloud/capabilities",
}
)
const (
// WwwAuthenticate captures the Www-Authenticate header string.
WwwAuthenticate = "Www-Authenticate"
)
// Authenticator is the common interface implemented by all request authenticators.
type Authenticator interface {
// Authenticate is used to authenticate incoming HTTP requests.
// The Authenticator may augment the request with user info or anything related to the
// authentication and return the augmented request.
Authenticate(*http.Request) (*http.Request, bool)
}
// Authentication is a higher order authentication middleware.
func Authentication(auths []Authenticator, opts ...Option) func(next http.Handler) http.Handler {
options := newOptions(opts...)
configureSupportedChallenges(options)
tracer := getTraceProvider(options).Tracer("proxy")
spanOpts := []trace.SpanStartOption{
trace.WithSpanKind(trace.SpanKindServer),
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, span := tracer.Start(r.Context(), fmt.Sprintf("%s %v", r.Method, r.URL.Path), spanOpts...)
defer span.End()
r = r.WithContext(ctx)
ri := router.ContextRoutingInfo(ctx)
if isOIDCTokenAuth(r) || ri.IsRouteUnprotected() || r.Method == "OPTIONS" {
// Either this is a request that does not need any authentication or
// the authentication for this request is handled by the IdP.
next.ServeHTTP(w, r)
return
}
for _, a := range auths {
if req, ok := a.Authenticate(r); ok {
next.ServeHTTP(w, req)
return
}
}
if !isPublicPath(r.URL.Path) {
// Failed basic authentication attempts receive the Www-Authenticate header in the response
var touch bool
caser := cases.Title(language.Und)
for k, v := range options.CredentialsByUserAgent {
if strings.Contains(k, r.UserAgent()) {
removeSuperfluousAuthenticate(w)
w.Header().Add("Www-Authenticate", fmt.Sprintf("%v realm=\"%s\", charset=\"UTF-8\"", caser.String(v), r.Host))
touch = true
break
}
}
// if the request is not bound to any user agent, write all available challenges
if !touch {
writeSupportedAuthenticateHeader(w, r)
}
}
for _, s := range SupportedAuthStrategies {
userAgentAuthenticateLockIn(w, r, options.CredentialsByUserAgent, s)
}
w.WriteHeader(http.StatusUnauthorized)
// if the request is a PROPFIND return a WebDAV error code.
// TODO: The proxy has to be smart enough to detect when a request is directed towards a webdav server
// and react accordingly.
if webdav.IsWebdavRequest(r) {
b, err := webdav.Marshal(webdav.Exception{
Code: webdav.SabredavPermissionDenied,
Message: "Authentication error",
})
webdav.HandleWebdavError(w, b, err)
}
if r.ProtoMajor == 1 {
// https://github.com/owncloud/ocis/issues/5066
// https://github.com/golang/go/blob/d5de62df152baf4de6e9fe81933319b86fd95ae4/src/net/http/server.go#L1357-L1417
// https://github.com/golang/go/issues/15527
defer r.Body.Close()
_, _ = io.Copy(io.Discard, r.Body)
}
})
}
}
// The token auth endpoint uses basic auth for clients, see https://openid.net/specs/openid-connect-basic-1_0.html#TokenRequest
// > The Client MUST authenticate to the Token Endpoint using the HTTP Basic method, as described in 2.3.1 of OAuth 2.0.
func isOIDCTokenAuth(req *http.Request) bool {
return req.URL.Path == "/konnect/v1/token"
}
func isPublicPath(p string) bool {
for _, pp := range _publicPaths {
if strings.HasPrefix(p, pp) {
return true
}
}
return false
}
// configureSupportedChallenges adds known authentication challenges to the current session.
func configureSupportedChallenges(options Options) {
if options.OIDCIss != "" {
SupportedAuthStrategies = append(SupportedAuthStrategies, "bearer")
}
if options.EnableBasicAuth {
SupportedAuthStrategies = append(SupportedAuthStrategies, "basic")
}
}
func writeSupportedAuthenticateHeader(w http.ResponseWriter, r *http.Request) {
caser := cases.Title(language.Und)
for _, s := range SupportedAuthStrategies {
if r.Header.Get("X-Requested-With") != "XMLHttpRequest" {
w.Header().Add(WwwAuthenticate, fmt.Sprintf("%v realm=\"%s\", charset=\"UTF-8\"", caser.String(s), r.Host))
}
}
}
func removeSuperfluousAuthenticate(w http.ResponseWriter) {
w.Header().Del(WwwAuthenticate)
}
// userAgentLocker aids in dependency injection for helper methods. The set of fields is arbitrary and the only relation
// they share is to fulfill their duty and lock a User-Agent to its correct challenge if configured.
type userAgentLocker struct {
w http.ResponseWriter
r *http.Request
locks map[string]string // locks represents a reva user-agent:challenge mapping.
fallback string
}
// userAgentAuthenticateLockIn sets Www-Authenticate according to configured user agents. This is useful for the case of
// legacy clients that do not support protocols like OIDC or OAuth and want to lock a given user agent to a challenge
// such as basic. For more context check https://github.com/cs3org/reva/pull/1350
func userAgentAuthenticateLockIn(w http.ResponseWriter, r *http.Request, locks map[string]string, fallback string) {
u := userAgentLocker{
w: w,
r: r,
locks: locks,
fallback: fallback,
}
for _, r := range ProxyWwwAuthenticate {
evalRequestURI(u, r)
}
}
func evalRequestURI(l userAgentLocker, r regexp.Regexp) {
if !r.MatchString(l.r.RequestURI) {
return
}
caser := cases.Title(language.Und)
for k, v := range l.locks {
if strings.Contains(k, l.r.UserAgent()) {
removeSuperfluousAuthenticate(l.w)
l.w.Header().Add(WwwAuthenticate, fmt.Sprintf("%v realm=\"%s\", charset=\"UTF-8\"", caser.String(v), l.r.Host))
return
}
}
}
func getTraceProvider(o Options) trace.TracerProvider {
if o.TraceProvider != nil {
return o.TraceProvider
}
return trace.NewNoopTracerProvider()
}