mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2025-12-28 08:38:12 -05:00
217 lines
7.0 KiB
Go
217 lines
7.0 KiB
Go
package middleware
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/opencloud-eu/opencloud/services/proxy/pkg/router"
|
|
"github.com/opencloud-eu/opencloud/services/proxy/pkg/webdav"
|
|
"go.opentelemetry.io/otel/attribute"
|
|
"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 OpenCloud. 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.middleware.authentication")
|
|
|
|
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 %s", r.Method, r.URL.Path), spanOpts...)
|
|
r = r.WithContext(ctx)
|
|
defer span.End()
|
|
|
|
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.
|
|
span.SetAttributes(attribute.Bool("routeunprotected", true))
|
|
span.End()
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
|
|
for _, a := range auths {
|
|
if req, ok := a.Authenticate(r); ok {
|
|
span.End()
|
|
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()
|
|
}
|