Initial QSfera import
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/qsfera/server/services/proxy/pkg/router"
|
||||
"github.com/qsfera/server/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 КуСфера. 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()
|
||||
}
|
||||
Reference in New Issue
Block a user