diff --git a/services/proxy/pkg/command/server.go b/services/proxy/pkg/command/server.go index 81342945f..9e372e448 100644 --- a/services/proxy/pkg/command/server.go +++ b/services/proxy/pkg/command/server.go @@ -16,9 +16,9 @@ import ( "github.com/owncloud/ocis/v2/ocis-pkg/log" pkgmiddleware "github.com/owncloud/ocis/v2/ocis-pkg/middleware" "github.com/owncloud/ocis/v2/ocis-pkg/service/grpc" + "github.com/owncloud/ocis/v2/ocis-pkg/sync" "github.com/owncloud/ocis/v2/ocis-pkg/version" settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0" - storesvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/store/v0" "github.com/owncloud/ocis/v2/services/proxy/pkg/config" "github.com/owncloud/ocis/v2/services/proxy/pkg/config/parser" "github.com/owncloud/ocis/v2/services/proxy/pkg/cs3" @@ -149,7 +149,7 @@ func loadMiddlewares(ctx context.Context, logger log.Logger, cfg *config.Config) logger.Fatal().Msgf("Invalid accounts backend type '%s'", cfg.AccountBackend) } - storeClient := storesvc.NewStoreService("com.owncloud.api.store", grpc.DefaultClient) + // storeClient := storesvc.NewStoreService("com.owncloud.api.store", grpc.DefaultClient) if err != nil { logger.Error().Err(err). Str("gateway", cfg.Reva.Address). @@ -166,6 +166,38 @@ func loadMiddlewares(ctx context.Context, logger log.Logger, cfg *config.Config) Timeout: time.Second * 10, } + var authenticators []middleware.Authenticator + if cfg.EnableBasicAuth { + logger.Warn().Msg("basic auth enabled, use only for testing or development") + authenticators = append(authenticators, middleware.BasicAuthenticator{ + Logger: logger, + UserProvider: userProvider, + }) + } + tokenCache := sync.NewCache(cfg.OIDC.UserinfoCache.Size) + authenticators = append(authenticators, middleware.OIDCAuthenticator{ + Logger: logger, + TokenCache: &tokenCache, + TokenCacheTTL: time.Duration(cfg.OIDC.UserinfoCache.TTL), + HTTPClient: oidcHTTPClient, + OIDCIss: cfg.OIDC.Issuer, + ProviderFunc: func() (middleware.OIDCProvider, error) { + // Initialize a provider by specifying the issuer URL. + // it will fetch the keys from the issuer using the .well-known + // endpoint + return oidc.NewProvider( + context.WithValue(ctx, oauth2.HTTPClient, oidcHTTPClient), + cfg.OIDC.Issuer, + ) + }, + JWKSOptions: cfg.OIDC.JWKS, + AccessTokenVerifyMethod: cfg.OIDC.AccessTokenVerifyMethod, + }) + // authenticators = append(authenticators, middleware.PublicShareAuthenticator{ + // Logger: logger, + // RevaGatewayClient: revaClient, + // }) + return alice.New( // first make sure we log all requests and redirect to https if necessary pkgmiddleware.TraceContext, @@ -179,39 +211,44 @@ func loadMiddlewares(ctx context.Context, logger log.Logger, cfg *config.Config) oidcHTTPClient, ), - // now that we established the basics, on with authentication middleware - middleware.Authentication( - // OIDC Options - middleware.OIDCProviderFunc(func() (middleware.OIDCProvider, error) { - // Initialize a provider by specifying the issuer URL. - // it will fetch the keys from the issuer using the .well-known - // endpoint - return oidc.NewProvider( - context.WithValue(ctx, oauth2.HTTPClient, oidcHTTPClient), - cfg.OIDC.Issuer, - ) - }), - middleware.HTTPClient(oidcHTTPClient), - middleware.TokenCacheSize(cfg.OIDC.UserinfoCache.Size), - middleware.TokenCacheTTL(time.Second*time.Duration(cfg.OIDC.UserinfoCache.TTL)), - middleware.AccessTokenVerifyMethod(cfg.OIDC.AccessTokenVerifyMethod), - middleware.JWKSOptions(cfg.OIDC.JWKS), + // middleware.AuthenticationOld( + // // OIDC Options + // middleware.OIDCProviderFunc(func() (middleware.OIDCProvider, error) { + // // Initialize a provider by specifying the issuer URL. + // // it will fetch the keys from the issuer using the .well-known + // // endpoint + // return oidc.NewProvider( + // context.WithValue(ctx, oauth2.HTTPClient, oidcHTTPClient), + // cfg.OIDC.Issuer, + // ) + // }), - // basic Options - middleware.Logger(logger), - middleware.EnableBasicAuth(cfg.EnableBasicAuth), - middleware.UserProvider(userProvider), - middleware.OIDCIss(cfg.OIDC.Issuer), - middleware.UserOIDCClaim(cfg.UserOIDCClaim), - middleware.UserCS3Claim(cfg.UserCS3Claim), + middleware.Authentication( + authenticators, middleware.CredentialsByUserAgent(cfg.AuthMiddleware.CredentialsByUserAgent), - ), - middleware.SignedURLAuth( middleware.Logger(logger), - middleware.PreSignedURLConfig(cfg.PreSignedURL), - middleware.UserProvider(userProvider), - middleware.Store(storeClient), + middleware.OIDCIss(cfg.OIDC.Issuer), + middleware.EnableBasicAuth(cfg.EnableBasicAuth), ), + // middleware.HTTPClient(oidcHTTPClient), + // middleware.TokenCacheSize(cfg.OIDC.UserinfoCache.Size), + // middleware.TokenCacheTTL(time.Second*time.Duration(cfg.OIDC.UserinfoCache.TTL)), + // + // // basic Options + // middleware.Logger(logger), + // middleware.EnableBasicAuth(cfg.EnableBasicAuth), + // middleware.UserProvider(userProvider), + // middleware.OIDCIss(cfg.OIDC.Issuer), + // middleware.UserOIDCClaim(cfg.UserOIDCClaim), + // middleware.UserCS3Claim(cfg.UserCS3Claim), + // middleware.CredentialsByUserAgent(cfg.AuthMiddleware.CredentialsByUserAgent), + // ), + // middleware.SignedURLAuth( + // middleware.Logger(logger), + // middleware.PreSignedURLConfig(cfg.PreSignedURL), + // middleware.UserProvider(userProvider), + // middleware.Store(storeClient), + // ), middleware.AccountResolver( middleware.Logger(logger), middleware.UserProvider(userProvider), @@ -233,9 +270,9 @@ func loadMiddlewares(ctx context.Context, logger log.Logger, cfg *config.Config) middleware.TokenManagerConfig(*cfg.TokenManager), middleware.RevaGatewayClient(revaClient), ), - middleware.PublicShareAuth( - middleware.Logger(logger), - middleware.RevaGatewayClient(revaClient), - ), + // middleware.PublicShareAuth( + // middleware.Logger(logger), + // middleware.RevaGatewayClient(revaClient), + // ), ) } diff --git a/services/proxy/pkg/middleware/authentication.go b/services/proxy/pkg/middleware/authentication.go index 15fa4ec42..651876f35 100644 --- a/services/proxy/pkg/middleware/authentication.go +++ b/services/proxy/pkg/middleware/authentication.go @@ -5,6 +5,8 @@ import ( "net/http" "regexp" "strings" + + "github.com/owncloud/ocis/v2/services/proxy/pkg/webdav" ) var ( @@ -12,48 +14,97 @@ var ( 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 strings - // to be regexp compiled. - ProxyWwwAuthenticate = []string{"/ocs/v[12].php/cloud/"} + // 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/")} - // WWWAuthenticate captures the Www-Authenticate header string. - WWWAuthenticate = "Www-Authenticate" + _publicPaths = []string{ + "/dav/public-files/", + "/remote.php/dav/public-files/", + "/remote.php/ocs/apps/files_sharing/api/v1/tokeninfo/unprotected", + "/ocs/v1.php/cloud/capabilities", + "/data", + } ) -// 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 +const ( + // WwwAuthenticate captures the Www-Authenticate header string. + WwwAuthenticate = "Www-Authenticate" +) + +// Authenticator is the common interface implemented by all request authenticators. +// The Authenticator may augment the request with user info or anything related to the +// authentication and return the augmented request. +type Authenticator interface { + Authenticate(*http.Request) (*http.Request, bool) } // Authentication is a higher order authentication middleware. -func Authentication(opts ...Option) func(next http.Handler) http.Handler { +func Authentication(auths []Authenticator, opts ...Option) func(next http.Handler) http.Handler { options := newOptions(opts...) - configureSupportedChallenges(options) - oidc := newOIDCAuth(options) - basic := newBasicAuth(options) return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if options.OIDCIss != "" && options.EnableBasicAuth { - oidc(basic(next)).ServeHTTP(w, r) + if isOIDCTokenAuth(r) || + r.URL.Path == "/" || + strings.HasPrefix(r.URL.Path, "/.well-known") || + r.URL.Path == "/login" || + strings.HasPrefix(r.URL.Path, "/js") || + strings.HasPrefix(r.URL.Path, "/themes") || + strings.HasPrefix(r.URL.Path, "/signin") || + strings.HasPrefix(r.URL.Path, "/konnect") || + r.URL.Path == "/config.json" || + r.URL.Path == "/oidc-callback.html" || + r.URL.Path == "/oidc-callback" || + r.URL.Path == "/settings.js" { + // The authentication for this request is handled by the IdP. + next.ServeHTTP(w, r) + return } - if options.OIDCIss != "" && !options.EnableBasicAuth { - oidc(next).ServeHTTP(w, r) + for _, a := range auths { + if req, ok := a.Authenticate(r); ok { + next.ServeHTTP(w, req) + return + } } + if !isPublicPath(r.URL.Path) { + 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", + }) - if options.OIDCIss == "" && options.EnableBasicAuth { - basic(next).ServeHTTP(w, r) + webdav.HandleWebdavError(w, b, err) } }) } } +// 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 != "" { @@ -66,13 +117,22 @@ func configureSupportedChallenges(options Options) { } func writeSupportedAuthenticateHeader(w http.ResponseWriter, r *http.Request) { - for i := 0; i < len(SupportedAuthStrategies); i++ { - w.Header().Add(WWWAuthenticate, fmt.Sprintf("%v realm=\"%s\", charset=\"UTF-8\"", strings.Title(SupportedAuthStrategies[i]), r.Host)) + for _, s := range SupportedAuthStrategies { + w.Header().Add(WwwAuthenticate, fmt.Sprintf("%v realm=\"%s\", charset=\"UTF-8\"", strings.Title(s), r.Host)) } } func removeSuperfluousAuthenticate(w http.ResponseWriter) { - w.Header().Del(WWWAuthenticate) + 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 @@ -86,22 +146,48 @@ func userAgentAuthenticateLockIn(w http.ResponseWriter, r *http.Request, locks m fallback: fallback, } - for i := 0; i < len(ProxyWwwAuthenticate); i++ { - evalRequestURI(&u, i) + for _, r := range ProxyWwwAuthenticate { + evalRequestURI(u, r) } } -func evalRequestURI(l *userAgentLocker, i int) { - r := regexp.MustCompile(ProxyWwwAuthenticate[i]) - if r.Match([]byte(l.r.RequestURI)) { - 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\"", strings.Title(v), l.r.Host)) - return - } +func evalRequestURI(l userAgentLocker, r regexp.Regexp) { + if !r.MatchString(l.r.RequestURI) { + return + } + 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\"", strings.Title(v), l.r.Host)) + return } - l.w.Header().Add(WWWAuthenticate, fmt.Sprintf("%v realm=\"%s\", charset=\"UTF-8\"", strings.Title(l.fallback), l.r.Host)) + } + l.w.Header().Add(WwwAuthenticate, fmt.Sprintf("%v realm=\"%s\", charset=\"UTF-8\"", strings.Title(l.fallback), l.r.Host)) +} + +// AuthenticationOld is a higher order authentication middleware. +func AuthenticationOld(opts ...Option) func(next http.Handler) http.Handler { + options := newOptions(opts...) + + configureSupportedChallenges(options) + oidc := newOIDCAuth(options) + // basic := newBasicAuth(options) + + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if options.OIDCIss != "" && options.EnableBasicAuth { + //oidc(basic(next)).ServeHTTP(w, r) + oidc(next).ServeHTTP(w, r) + } + + if options.OIDCIss != "" && !options.EnableBasicAuth { + oidc(next).ServeHTTP(w, r) + } + + // if options.OIDCIss == "" && options.EnableBasicAuth { + // basic(next).ServeHTTP(w, r) + // } + }) } } @@ -120,15 +206,15 @@ func newOIDCAuth(options Options) func(http.Handler) http.Handler { ) } -// newBasicAuth returns a configured basic middleware -func newBasicAuth(options Options) func(http.Handler) http.Handler { - return BasicAuth( - UserProvider(options.UserProvider), - Logger(options.Logger), - EnableBasicAuth(options.EnableBasicAuth), - OIDCIss(options.OIDCIss), - UserOIDCClaim(options.UserOIDCClaim), - UserCS3Claim(options.UserCS3Claim), - CredentialsByUserAgent(options.CredentialsByUserAgent), - ) -} +// // newBasicAuth returns a configured basic middleware +// func newBasicAuth(options Options) func(http.Handler) http.Handler { +// return BasicAuth( +// UserProvider(options.UserProvider), +// Logger(options.Logger), +// EnableBasicAuth(options.EnableBasicAuth), +// OIDCIss(options.OIDCIss), +// UserOIDCClaim(options.UserOIDCClaim), +// UserCS3Claim(options.UserCS3Claim), +// CredentialsByUserAgent(options.CredentialsByUserAgent), +// ) +// } diff --git a/services/proxy/pkg/middleware/basic_auth.go b/services/proxy/pkg/middleware/basic_auth.go index b7d480d87..4771e57f2 100644 --- a/services/proxy/pkg/middleware/basic_auth.go +++ b/services/proxy/pkg/middleware/basic_auth.go @@ -1,145 +1,52 @@ package middleware import ( - "fmt" "net/http" - "strings" "github.com/owncloud/ocis/v2/ocis-pkg/log" "github.com/owncloud/ocis/v2/ocis-pkg/oidc" "github.com/owncloud/ocis/v2/services/proxy/pkg/user/backend" - "github.com/owncloud/ocis/v2/services/proxy/pkg/webdav" ) -// BasicAuth provides a middleware to check if BasicAuth is provided -func BasicAuth(optionSetters ...Option) func(next http.Handler) http.Handler { - options := newOptions(optionSetters...) - logger := options.Logger - - if options.EnableBasicAuth { - options.Logger.Warn().Msg("basic auth enabled, use only for testing or development") - } - - h := basicAuth{ - logger: logger, - enabled: options.EnableBasicAuth, - userProvider: options.UserProvider, - } - - return func(next http.Handler) http.Handler { - return http.HandlerFunc( - func(w http.ResponseWriter, req *http.Request) { - if h.isPublicLink(req) || !h.isBasicAuth(req) || h.isOIDCTokenAuth(req) { - if !h.isPublicLink(req) { - userAgentAuthenticateLockIn(w, req, options.CredentialsByUserAgent, "basic") - } - next.ServeHTTP(w, req) - return - } - - removeSuperfluousAuthenticate(w) - login, password, _ := req.BasicAuth() - user, _, err := h.userProvider.Authenticate(req.Context(), login, password) - - // touch is a user agent locking guard, when touched changes to true it indicates the User-Agent on the - // request is configured to support only one challenge, it it remains untouched, there are no considera- - // tions and we should write all available authentication challenges to the response. - touch := false - - if err != nil { - for k, v := range options.CredentialsByUserAgent { - if strings.Contains(k, req.UserAgent()) { - removeSuperfluousAuthenticate(w) - w.Header().Add("Www-Authenticate", fmt.Sprintf("%v realm=\"%s\", charset=\"UTF-8\"", strings.Title(v), req.Host)) - touch = true - break - } - } - - // if the request is not bound to any user agent, write all available challenges - if !touch { - writeSupportedAuthenticateHeader(w, req) - } - - // 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. - - w.WriteHeader(http.StatusUnauthorized) - - if webdav.IsWebdavRequest(req) { - b, err := webdav.Marshal(webdav.Exception{ - Code: webdav.SabredavPermissionDenied, - Message: "Authentication error", - }) - - webdav.HandleWebdavError(w, b, err) - return - } - - return - } - - // fake oidc claims - claims := map[string]interface{}{ - oidc.Iss: user.Id.Idp, - oidc.PreferredUsername: user.Username, - oidc.Email: user.Mail, - oidc.OwncloudUUID: user.Id.OpaqueId, - } - - if options.UserCS3Claim == "userid" { - // set the custom user claim only if users will be looked up by the userid on the CS3api - // OpaqueId contains the userid configured in STORAGE_LDAP_USER_SCHEMA_UID - claims[options.UserOIDCClaim] = user.Id.OpaqueId - - } - - next.ServeHTTP(w, req.WithContext(oidc.NewContext(req.Context(), claims))) - }, - ) - } +type BasicAuthenticator struct { + Logger log.Logger + UserProvider backend.UserBackend + UserCS3Claim string + UserOIDCClaim string } -type basicAuth struct { - logger log.Logger - enabled bool - userProvider backend.UserBackend -} - -func (m basicAuth) isPublicLink(req *http.Request) bool { - login, _, ok := req.BasicAuth() - - if !ok || login != "public" { - return false +func (m BasicAuthenticator) Authenticate(req *http.Request) (*http.Request, bool) { + if isPublicPath(req.URL.Path) { + // The authentication of public path requests is handled by another authenticator. + // Since we can't guarantee the order of execution of the authenticators, we better + // implement an early return here for paths we can't authenticate in this authenticator. + return nil, false } - publicPaths := []string{ - "/dav/public-files/", - "/remote.php/dav/public-files/", - "/remote.php/ocs/apps/files_sharing/api/v1/tokeninfo/unprotected", - "/ocs/v1.php/cloud/capabilities", - "/data", - } - isPublic := false - - for _, p := range publicPaths { - if strings.HasPrefix(req.URL.Path, p) { - isPublic = true - break - } + login, password, ok := req.BasicAuth() + if !ok { + return nil, false } - return isPublic -} + user, _, err := m.UserProvider.Authenticate(req.Context(), login, password) + if err != nil { + // TODO add log line + return nil, false + } -// 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 (m basicAuth) isOIDCTokenAuth(req *http.Request) bool { - return req.URL.Path == "/konnect/v1/token" -} + // fake oidc claims + claims := map[string]interface{}{ + oidc.Iss: user.Id.Idp, + oidc.PreferredUsername: user.Username, + oidc.Email: user.Mail, + oidc.OwncloudUUID: user.Id.OpaqueId, + } -func (m basicAuth) isBasicAuth(req *http.Request) bool { - _, _, ok := req.BasicAuth() - return m.enabled && ok + if m.UserCS3Claim == "userid" { + // set the custom user claim only if users will be looked up by the userid on the CS3api + // OpaqueId contains the userid configured in STORAGE_LDAP_USER_SCHEMA_UID + claims[m.UserOIDCClaim] = user.Id.OpaqueId + + } + return req.WithContext(oidc.NewContext(req.Context(), claims)), true } diff --git a/services/proxy/pkg/middleware/basic_auth_test.go b/services/proxy/pkg/middleware/basic_auth_test.go index 46c7f4828..77279d66b 100644 --- a/services/proxy/pkg/middleware/basic_auth_test.go +++ b/services/proxy/pkg/middleware/basic_auth_test.go @@ -23,8 +23,6 @@ func TestBasicAuth__isPublicLink(t *testing.T) { {url: "/ocs/v1.php/cloud/capabilities", username: "public", expected: true}, {url: "/ocs/v1.php/cloud/users/admin", username: "public", expected: false}, } - ba := basicAuth{} - for _, tt := range tests { req := httptest.NewRequest("", tt.url, nil) @@ -32,7 +30,7 @@ func TestBasicAuth__isPublicLink(t *testing.T) { req.SetBasicAuth(tt.username, "") } - result := ba.isPublicLink(req) + result := isPublicPath(req.URL.Path) if result != tt.expected { t.Errorf("with %s expected %t got %t", tt.url, tt.expected, result) } diff --git a/services/proxy/pkg/middleware/oidc_auth.go b/services/proxy/pkg/middleware/oidc_auth.go index ae3b313f0..97a71962f 100644 --- a/services/proxy/pkg/middleware/oidc_auth.go +++ b/services/proxy/pkg/middleware/oidc_auth.go @@ -25,80 +25,29 @@ type OIDCProvider interface { UserInfo(ctx context.Context, ts oauth2.TokenSource) (*gOidc.UserInfo, error) } -// OIDCAuth provides a middleware to check access secured by a static token. -func OIDCAuth(optionSetters ...Option) func(next http.Handler) http.Handler { - options := newOptions(optionSetters...) - tokenCache := osync.NewCache(options.UserinfoCacheSize) +type OIDCAuthenticator struct { + Logger log.Logger + HTTPClient *http.Client + OIDCIss string + TokenCache *osync.Cache + TokenCacheTTL time.Duration + ProviderFunc func() (OIDCProvider, error) + AccessTokenVerifyMethod string + JWKSOptions config.JWKS - h := oidcAuth{ - logger: options.Logger, - providerFunc: options.OIDCProviderFunc, - httpClient: options.HTTPClient, - oidcIss: options.OIDCIss, - tokenCache: &tokenCache, - tokenCacheTTL: options.UserinfoCacheTTL, - accessTokenVerifyMethod: options.AccessTokenVerifyMethod, - jwksOptions: options.JWKS, - jwksLock: &sync.Mutex{}, - providerLock: &sync.Mutex{}, - } + providerLock *sync.Mutex + provider OIDCProvider - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - // there is no bearer token on the request, - if !h.shouldServe(req) { - // oidc supported but token not present, add header and handover to the next middleware. - userAgentAuthenticateLockIn(w, req, options.CredentialsByUserAgent, "bearer") - next.ServeHTTP(w, req) - return - } - - if h.getProvider() == nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - - // Force init of jwks keyfunc if needed (contacts the .well-known and jwks endpoints on first call) - if h.accessTokenVerifyMethod == config.AccessTokenVerificationJWT && h.getKeyfunc() == nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - - token := strings.TrimPrefix(req.Header.Get("Authorization"), "Bearer ") - - claims, status := h.getClaims(token, req) - if status != 0 { - w.WriteHeader(status) - return - } - - // inject claims to the request context for the account_resolver middleware. - next.ServeHTTP(w, req.WithContext(oidc.NewContext(req.Context(), claims))) - }) - } + jwksLock *sync.Mutex + JWKS *keyfunc.JWKS } -type oidcAuth struct { - logger log.Logger - provider OIDCProvider - providerLock *sync.Mutex - jwksOptions config.JWKS - jwks *keyfunc.JWKS - jwksLock *sync.Mutex - providerFunc func() (OIDCProvider, error) - httpClient *http.Client - oidcIss string - tokenCache *osync.Cache - tokenCacheTTL time.Duration - accessTokenVerifyMethod string -} - -func (m oidcAuth) getClaims(token string, req *http.Request) (claims map[string]interface{}, status int) { - hit := m.tokenCache.Load(token) +func (m OIDCAuthenticator) getClaims(token string, req *http.Request) (claims map[string]interface{}, status int) { + hit := m.TokenCache.Load(token) if hit == nil { aClaims, err := m.verifyAccessToken(token) if err != nil { - m.logger.Error().Err(err).Msg("Failed to verify access token") + m.Logger.Error().Err(err).Msg("Failed to verify access token") status = http.StatusUnauthorized return } @@ -108,25 +57,25 @@ func (m oidcAuth) getClaims(token string, req *http.Request) (claims map[string] } userInfo, err := m.getProvider().UserInfo( - context.WithValue(req.Context(), oauth2.HTTPClient, m.httpClient), + context.WithValue(req.Context(), oauth2.HTTPClient, m.HTTPClient), oauth2.StaticTokenSource(oauth2Token), ) if err != nil { - m.logger.Error().Err(err).Msg("Failed to get userinfo") + m.Logger.Error().Err(err).Msg("Failed to get userinfo") status = http.StatusUnauthorized return } if err := userInfo.Claims(&claims); err != nil { - m.logger.Error().Err(err).Interface("userinfo", userInfo).Msg("failed to unmarshal userinfo claims") + m.Logger.Error().Err(err).Interface("userinfo", userInfo).Msg("failed to unmarshal userinfo claims") status = http.StatusInternalServerError return } expiration := m.extractExpiration(aClaims) - m.tokenCache.Store(token, claims, expiration) + m.TokenCache.Store(token, claims, expiration) - m.logger.Debug().Interface("claims", claims).Interface("userInfo", userInfo).Time("expiration", expiration.UTC()).Msg("unmarshalled and cached userinfo") + m.Logger.Debug().Interface("claims", claims).Interface("userInfo", userInfo).Time("expiration", expiration.UTC()).Msg("unmarshalled and cached userinfo") return } @@ -135,25 +84,25 @@ func (m oidcAuth) getClaims(token string, req *http.Request) (claims map[string] status = http.StatusInternalServerError return } - m.logger.Debug().Interface("claims", claims).Msg("cache hit for userinfo") + m.Logger.Debug().Interface("claims", claims).Msg("cache hit for userinfo") return } -func (m oidcAuth) verifyAccessToken(token string) (jwt.RegisteredClaims, error) { - switch m.accessTokenVerifyMethod { +func (m OIDCAuthenticator) verifyAccessToken(token string) (jwt.RegisteredClaims, error) { + switch m.AccessTokenVerifyMethod { case config.AccessTokenVerificationJWT: return m.verifyAccessTokenJWT(token) case config.AccessTokenVerificationNone: - m.logger.Debug().Msg("Access Token verification disabled") + m.Logger.Debug().Msg("Access Token verification disabled") return jwt.RegisteredClaims{}, nil default: - m.logger.Error().Str("access_token_verify_method", m.accessTokenVerifyMethod).Msg("Unknown Access Token verification setting") + m.Logger.Error().Str("access_token_verify_method", m.AccessTokenVerifyMethod).Msg("Unknown Access Token verification setting") return jwt.RegisteredClaims{}, errors.New("Unknown Access Token Verification method") } } // verifyAccessTokenJWT tries to parse and verify the access token as a JWT. -func (m oidcAuth) verifyAccessTokenJWT(token string) (jwt.RegisteredClaims, error) { +func (m OIDCAuthenticator) verifyAccessTokenJWT(token string) (jwt.RegisteredClaims, error) { var claims jwt.RegisteredClaims jwks := m.getKeyfunc() if jwks == nil { @@ -161,13 +110,13 @@ func (m oidcAuth) verifyAccessTokenJWT(token string) (jwt.RegisteredClaims, erro } _, err := jwt.ParseWithClaims(token, &claims, jwks.Keyfunc) - m.logger.Debug().Interface("access token", &claims).Msg("parsed access token") + m.Logger.Debug().Interface("access token", &claims).Msg("parsed access token") if err != nil { - m.logger.Info().Err(err).Msg("Failed to parse/verify the access token.") + m.Logger.Info().Err(err).Msg("Failed to parse/verify the access token.") return claims, err } - if !claims.VerifyIssuer(m.oidcIss, true) { + if !claims.VerifyIssuer(m.OIDCIss, true) { vErr := jwt.ValidationError{} vErr.Inner = jwt.ErrTokenInvalidIssuer vErr.Errors |= jwt.ValidationErrorIssuer @@ -180,19 +129,19 @@ func (m oidcAuth) verifyAccessTokenJWT(token string) (jwt.RegisteredClaims, erro // extractExpiration tries to extract the expriration time from the access token // If the access token does not have an exp claim it will fallback to the configured // default expiration -func (m oidcAuth) extractExpiration(aClaims jwt.RegisteredClaims) time.Time { - defaultExpiration := time.Now().Add(m.tokenCacheTTL) +func (m OIDCAuthenticator) extractExpiration(aClaims jwt.RegisteredClaims) time.Time { + defaultExpiration := time.Now().Add(m.TokenCacheTTL) if aClaims.ExpiresAt != nil { - m.logger.Debug().Str("exp", aClaims.ExpiresAt.String()).Msg("Expiration Time from access_token") + m.Logger.Debug().Str("exp", aClaims.ExpiresAt.String()).Msg("Expiration Time from access_token") return aClaims.ExpiresAt.Time } return defaultExpiration } -func (m oidcAuth) shouldServe(req *http.Request) bool { +func (m OIDCAuthenticator) shouldServe(req *http.Request) bool { header := req.Header.Get("Authorization") - if m.oidcIss == "" { + if m.OIDCIss == "" { return false } @@ -211,58 +160,58 @@ type jwksJSON struct { JWKSURL string `json:"jwks_uri"` } -func (m *oidcAuth) getKeyfunc() *keyfunc.JWKS { +func (m *OIDCAuthenticator) getKeyfunc() *keyfunc.JWKS { m.jwksLock.Lock() defer m.jwksLock.Unlock() - if m.jwks == nil { - wellKnown := strings.TrimSuffix(m.oidcIss, "/") + "/.well-known/openid-configuration" + if m.JWKS == nil { + wellKnown := strings.TrimSuffix(m.OIDCIss, "/") + "/.well-known/openid-configuration" - resp, err := m.httpClient.Get(wellKnown) + resp, err := m.HTTPClient.Get(wellKnown) if err != nil { - m.logger.Error().Err(err).Msg("Failed to set request for .well-known/openid-configuration") + m.Logger.Error().Err(err).Msg("Failed to set request for .well-known/openid-configuration") return nil } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { - m.logger.Error().Err(err).Msg("unable to read discovery response body") + m.Logger.Error().Err(err).Msg("unable to read discovery response body") return nil } if resp.StatusCode != http.StatusOK { - m.logger.Error().Str("status", resp.Status).Str("body", string(body)).Msg("error requesting openid-configuration") + m.Logger.Error().Str("status", resp.Status).Str("body", string(body)).Msg("error requesting openid-configuration") return nil } var j jwksJSON err = json.Unmarshal(body, &j) if err != nil { - m.logger.Error().Err(err).Msg("failed to decode provider openid-configuration") + m.Logger.Error().Err(err).Msg("failed to decode provider openid-configuration") return nil } - m.logger.Debug().Str("jwks", j.JWKSURL).Msg("discovered jwks endpoint") + m.Logger.Debug().Str("jwks", j.JWKSURL).Msg("discovered jwks endpoint") options := keyfunc.Options{ - Client: m.httpClient, + Client: m.HTTPClient, RefreshErrorHandler: func(err error) { - m.logger.Error().Err(err).Msg("There was an error with the jwt.Keyfunc") + m.Logger.Error().Err(err).Msg("There was an error with the jwt.Keyfunc") }, - RefreshInterval: time.Minute * time.Duration(m.jwksOptions.RefreshInterval), - RefreshRateLimit: time.Second * time.Duration(m.jwksOptions.RefreshRateLimit), - RefreshTimeout: time.Second * time.Duration(m.jwksOptions.RefreshTimeout), - RefreshUnknownKID: m.jwksOptions.RefreshUnknownKID, + RefreshInterval: time.Minute * time.Duration(m.JWKSOptions.RefreshInterval), + RefreshRateLimit: time.Second * time.Duration(m.JWKSOptions.RefreshRateLimit), + RefreshTimeout: time.Second * time.Duration(m.JWKSOptions.RefreshTimeout), + RefreshUnknownKID: m.JWKSOptions.RefreshUnknownKID, } - m.jwks, err = keyfunc.Get(j.JWKSURL, options) + m.JWKS, err = keyfunc.Get(j.JWKSURL, options) if err != nil { - m.jwks = nil - m.logger.Error().Err(err).Msg("Failed to create JWKS from resource at the given URL.") + m.JWKS = nil + m.Logger.Error().Err(err).Msg("Failed to create JWKS from resource at the given URL.") return nil } } - return m.jwks + return m.JWKS } -func (m *oidcAuth) getProvider() OIDCProvider { +func (m *OIDCAuthenticator) getProvider() OIDCProvider { m.providerLock.Lock() defer m.providerLock.Unlock() if m.provider == nil { @@ -271,9 +220,9 @@ func (m *oidcAuth) getProvider() OIDCProvider { // provider needs to be cached as when it is created // it will fetch the keys from the issuer using the .well-known // endpoint - provider, err := m.providerFunc() + provider, err := m.ProviderFunc() if err != nil { - m.logger.Error().Err(err).Msg("could not initialize oidcAuth provider") + m.Logger.Error().Err(err).Msg("could not initialize oidcAuth provider") return nil } @@ -281,3 +230,32 @@ func (m *oidcAuth) getProvider() OIDCProvider { } return m.provider } + +func (m OIDCAuthenticator) Authenticate(r *http.Request) (*http.Request, bool) { + // there is no bearer token on the request, + if !m.shouldServe(r) { + // // oidc supported but token not present, add header and handover to the next middleware. + // userAgentAuthenticateLockIn(w, r, options.CredentialsByUserAgent, "bearer") + // next.ServeHTTP(w, r) + return nil, false + } + + if m.getProvider() == nil { + // w.WriteHeader(http.StatusInternalServerError) + return nil, false + } + // Force init of jwks keyfunc if needed (contacts the .well-known and jwks endpoints on first call) + if m.AccessTokenVerifyMethod == config.AccessTokenVerificationJWT && m.getKeyfunc() == nil { + return nil, false + } + + token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") + + claims, status := m.getClaims(token, r) + if status != 0 { + // w.WriteHeader(status) + // TODO log + return nil, false + } + return r.WithContext(oidc.NewContext(r.Context(), claims)), true +} diff --git a/services/proxy/pkg/middleware/public_share_auth.go b/services/proxy/pkg/middleware/public_share_auth.go index 9b3ddcd5f..6d6d0fa74 100644 --- a/services/proxy/pkg/middleware/public_share_auth.go +++ b/services/proxy/pkg/middleware/public_share_auth.go @@ -5,6 +5,7 @@ import ( "strings" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + "github.com/owncloud/ocis/v2/ocis-pkg/log" ) const ( @@ -14,59 +15,58 @@ const ( authenticationType = "publicshares" ) -// PublicShareAuth ... -func PublicShareAuth(opts ...Option) func(next http.Handler) http.Handler { - options := newOptions(opts...) - logger := options.Logger - - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - shareToken := r.Header.Get(headerShareToken) - if shareToken == "" { - shareToken = r.URL.Query().Get(headerShareToken) - } - - // Currently we only want to authenticate app open request coming from public shares. - if shareToken == "" { - // Don't authenticate - next.ServeHTTP(w, r) - return - } - - var sharePassword string - if signature := r.URL.Query().Get("signature"); signature != "" { - expiration := r.URL.Query().Get("expiration") - if expiration == "" { - logger.Warn().Str("signature", signature).Msg("cannot do signature auth without the expiration") - next.ServeHTTP(w, r) - return - } - sharePassword = strings.Join([]string{"signature", signature, expiration}, "|") - } else { - // We can ignore the username since it is always set to "public" in public shares. - _, password, ok := r.BasicAuth() - - sharePassword = basicAuthPasswordPrefix - if ok { - sharePassword += password - } - } - - authResp, err := options.RevaGatewayClient.Authenticate(r.Context(), &gateway.AuthenticateRequest{ - Type: authenticationType, - ClientId: shareToken, - ClientSecret: sharePassword, - }) - - if err != nil { - logger.Debug().Err(err).Str("public_share_token", shareToken).Msg("could not authenticate public share") - // try another middleware - next.ServeHTTP(w, r) - return - } - - r.Header.Add(headerRevaAccessToken, authResp.Token) - next.ServeHTTP(w, r) - }) - } +type PublicShareAuthenticator struct { + Logger log.Logger + RevaGatewayClient gateway.GatewayAPIClient +} + +func (a PublicShareAuthenticator) Authenticate(r *http.Request) (*http.Request, bool) { + if !isPublicPath(r.URL.Path) { + return nil, false + } + + query := r.URL.Query() + shareToken := r.Header.Get(headerShareToken) + if shareToken == "" { + shareToken = query.Get(headerShareToken) + } + + // Currently we only want to authenticate app open request coming from public shares. + if shareToken == "" { + // Don't authenticate + return nil, false + } + + var sharePassword string + if signature := query.Get("signature"); signature != "" { + expiration := query.Get("expiration") + if expiration == "" { + a.Logger.Warn().Str("signature", signature).Msg("cannot do signature auth without the expiration") + return nil, false + } + sharePassword = strings.Join([]string{"signature", signature, expiration}, "|") + } else { + // We can ignore the username since it is always set to "public" in public shares. + _, password, ok := r.BasicAuth() + + sharePassword = basicAuthPasswordPrefix + if ok { + sharePassword += password + } + } + + authResp, err := a.RevaGatewayClient.Authenticate(r.Context(), &gateway.AuthenticateRequest{ + Type: authenticationType, + ClientId: shareToken, + ClientSecret: sharePassword, + }) + + if err != nil { + a.Logger.Debug().Err(err).Str("public_share_token", shareToken).Msg("could not authenticate public share") + // try another middleware + return nil, false + } + + r.Header.Add(headerRevaAccessToken, authResp.Token) + return r, false } diff --git a/services/proxy/pkg/middleware/signed_url_auth.go b/services/proxy/pkg/middleware/signed_url_auth.go index d71036d5a..094c279ad 100644 --- a/services/proxy/pkg/middleware/signed_url_auth.go +++ b/services/proxy/pkg/middleware/signed_url_auth.go @@ -25,7 +25,7 @@ func SignedURLAuth(optionSetters ...Option) func(next http.Handler) http.Handler options := newOptions(optionSetters...) return func(next http.Handler) http.Handler { - return &signedURLAuth{ + return &SignedURLAuthenticator{ next: next, logger: options.Logger, preSignedURLConfig: options.PreSignedURLConfig, @@ -35,7 +35,7 @@ func SignedURLAuth(optionSetters ...Option) func(next http.Handler) http.Handler } } -type signedURLAuth struct { +type SignedURLAuthenticator struct { next http.Handler logger log.Logger preSignedURLConfig config.PreSignedURL @@ -43,7 +43,7 @@ type signedURLAuth struct { store storesvc.StoreService } -func (m signedURLAuth) ServeHTTP(w http.ResponseWriter, req *http.Request) { +func (m SignedURLAuthenticator) ServeHTTP(w http.ResponseWriter, req *http.Request) { if !m.shouldServe(req) { m.next.ServeHTTP(w, req) return @@ -67,14 +67,14 @@ func (m signedURLAuth) ServeHTTP(w http.ResponseWriter, req *http.Request) { m.next.ServeHTTP(w, req) } -func (m signedURLAuth) shouldServe(req *http.Request) bool { +func (m SignedURLAuthenticator) shouldServe(req *http.Request) bool { if !m.preSignedURLConfig.Enabled { return false } return req.URL.Query().Get("OC-Signature") != "" } -func (m signedURLAuth) validate(req *http.Request) (err error) { +func (m SignedURLAuthenticator) validate(req *http.Request) (err error) { query := req.URL.Query() if ok, err := m.allRequiredParametersArePresent(query); !ok { @@ -100,7 +100,7 @@ func (m signedURLAuth) validate(req *http.Request) (err error) { return nil } -func (m signedURLAuth) allRequiredParametersArePresent(query url.Values) (ok bool, err error) { +func (m SignedURLAuthenticator) allRequiredParametersArePresent(query url.Values) (ok bool, err error) { // check if required query parameters exist in given request query parameters // OC-Signature - the computed signature - server will verify the request upon this REQUIRED // OC-Credential - defines the user scope (shall we use the owncloud user id here - this might leak internal data ....) REQUIRED @@ -122,7 +122,7 @@ func (m signedURLAuth) allRequiredParametersArePresent(query url.Values) (ok boo return true, nil } -func (m signedURLAuth) requestMethodMatches(meth string, query url.Values) (ok bool, err error) { +func (m SignedURLAuthenticator) requestMethodMatches(meth string, query url.Values) (ok bool, err error) { // check if given url query parameter OC-Verb matches given request method if !strings.EqualFold(meth, query.Get("OC-Verb")) { return false, errors.New("required OC-Verb parameter did not match request method") @@ -131,7 +131,7 @@ func (m signedURLAuth) requestMethodMatches(meth string, query url.Values) (ok b return true, nil } -func (m signedURLAuth) requestMethodIsAllowed(meth string) (ok bool, err error) { +func (m SignedURLAuthenticator) requestMethodIsAllowed(meth string) (ok bool, err error) { // check if given request method is allowed methodIsAllowed := false for _, am := range m.preSignedURLConfig.AllowedHTTPMethods { @@ -147,7 +147,7 @@ func (m signedURLAuth) requestMethodIsAllowed(meth string) (ok bool, err error) return true, nil } -func (m signedURLAuth) urlIsExpired(query url.Values, now func() time.Time) (expired bool, err error) { +func (m SignedURLAuthenticator) urlIsExpired(query url.Values, now func() time.Time) (expired bool, err error) { // check if url is expired by checking if given date (OC-Date) + expires in seconds (OC-Expires) is after now validFrom, err := time.Parse(time.RFC3339, query.Get("OC-Date")) if err != nil { @@ -164,7 +164,7 @@ func (m signedURLAuth) urlIsExpired(query url.Values, now func() time.Time) (exp return !(now().After(validFrom) && now().Before(validTo)), nil } -func (m signedURLAuth) signatureIsValid(req *http.Request) (ok bool, err error) { +func (m SignedURLAuthenticator) signatureIsValid(req *http.Request) (ok bool, err error) { u := revactx.ContextMustGetUser(req.Context()) signingKey, err := m.getSigningKey(req.Context(), u.Id.OpaqueId) if err != nil { @@ -187,7 +187,7 @@ func (m signedURLAuth) signatureIsValid(req *http.Request) (ok bool, err error) return m.createSignature(url, signingKey) == signature, nil } -func (m signedURLAuth) createSignature(url string, signingKey []byte) string { +func (m SignedURLAuthenticator) createSignature(url string, signingKey []byte) string { // the oc10 signature check: $hash = \hash_pbkdf2("sha512", $url, $signingKey, 10000, 64, false); // - sets the length of the output string to 64 // - sets raw output to false -> if raw_output is FALSE length corresponds to twice the byte-length of the derived key (as every byte of the key is returned as two hexits). @@ -197,7 +197,7 @@ func (m signedURLAuth) createSignature(url string, signingKey []byte) string { return hex.EncodeToString(hash) } -func (m signedURLAuth) getSigningKey(ctx context.Context, ocisID string) ([]byte, error) { +func (m SignedURLAuthenticator) getSigningKey(ctx context.Context, ocisID string) ([]byte, error) { res, err := m.store.Read(ctx, &storesvc.ReadRequest{ Options: &storemsg.ReadOptions{ Database: "proxy", @@ -211,3 +211,26 @@ func (m signedURLAuth) getSigningKey(ctx context.Context, ocisID string) ([]byte return res.Records[0].Value, nil } + +func (m SignedURLAuthenticator) Authenticate(r *http.Request) (*http.Request, bool) { + if !m.shouldServe(r) { + return nil, false + } + + user, _, err := m.userProvider.GetUserByClaims(r.Context(), "username", r.URL.Query().Get("OC-Credential"), true) + if err != nil { + m.logger.Error().Err(err).Msg("Could not get user by claim") + return nil, false + } + + ctx := revactx.ContextSetUser(r.Context(), user) + + r = r.WithContext(ctx) + + if err := m.validate(r); err != nil { + // http.Error(w, "Invalid url signature", http.StatusUnauthorized) + return nil, false + } + + return r, true +} diff --git a/services/proxy/pkg/middleware/signed_url_auth_test.go b/services/proxy/pkg/middleware/signed_url_auth_test.go index 856fcdb0e..01311b731 100644 --- a/services/proxy/pkg/middleware/signed_url_auth_test.go +++ b/services/proxy/pkg/middleware/signed_url_auth_test.go @@ -7,7 +7,7 @@ import ( ) func TestSignedURLAuth_shouldServe(t *testing.T) { - pua := signedURLAuth{} + pua := SignedURLAuthenticator{} tests := []struct { url string enabled bool @@ -31,7 +31,7 @@ func TestSignedURLAuth_shouldServe(t *testing.T) { } func TestSignedURLAuth_allRequiredParametersPresent(t *testing.T) { - pua := signedURLAuth{} + pua := SignedURLAuthenticator{} baseURL := "https://example.com/example.jpg?" tests := []struct { params string @@ -54,7 +54,7 @@ func TestSignedURLAuth_allRequiredParametersPresent(t *testing.T) { } func TestSignedURLAuth_requestMethodMatches(t *testing.T) { - pua := signedURLAuth{} + pua := SignedURLAuthenticator{} tests := []struct { method string url string @@ -75,7 +75,7 @@ func TestSignedURLAuth_requestMethodMatches(t *testing.T) { } func TestSignedURLAuth_requestMethodIsAllowed(t *testing.T) { - pua := signedURLAuth{} + pua := SignedURLAuthenticator{} tests := []struct { method string allowed []string @@ -99,7 +99,7 @@ func TestSignedURLAuth_requestMethodIsAllowed(t *testing.T) { } func TestSignedURLAuth_urlIsExpired(t *testing.T) { - pua := signedURLAuth{} + pua := SignedURLAuthenticator{} nowFunc := func() time.Time { t, _ := time.Parse(time.RFC3339, "2020-02-02T12:30:00.000Z") return t @@ -126,7 +126,7 @@ func TestSignedURLAuth_urlIsExpired(t *testing.T) { } func TestSignedURLAuth_createSignature(t *testing.T) { - pua := signedURLAuth{} + pua := SignedURLAuthenticator{} expected := "27d2ebea381384af3179235114801dcd00f91e46f99fca72575301cf3948101d" s := pua.createSignature("something", []byte("somerandomkey"))