Skip to content

Commit

Permalink
auth: add www-authenticate based on user agent
Browse files Browse the repository at this point in the history
  • Loading branch information
labkode committed Dec 1, 2020
1 parent 1e6ca25 commit f846b3d
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 17 deletions.
14 changes: 11 additions & 3 deletions docs/content/en/docs/config/http/services/dataprovider/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,23 @@ description: >
# _struct: config_

{{% dir name="prefix" type="string" default="data" %}}
The prefix to be used for this HTTP service [[Ref]](https://github.com/cs3org/reva/tree/master/internal/http/services/dataprovider/dataprovider.go#L39)
The prefix to be used for this HTTP service [[Ref]](https://github.com/cs3org/reva/tree/master/internal/http/services/dataprovider/dataprovider.go#L40)
{{< highlight toml >}}
[http.services.dataprovider]
prefix = "data"
{{< /highlight >}}
{{% /dir %}}

{{% dir name="driver" type="string" default="localhome" %}}
The storage driver to be used. [[Ref]](https://github.com/cs3org/reva/tree/master/internal/http/services/dataprovider/dataprovider.go#L40)
The storage driver to be used. [[Ref]](https://github.com/cs3org/reva/tree/master/internal/http/services/dataprovider/dataprovider.go#L41)
{{< highlight toml >}}
[http.services.dataprovider]
driver = "localhome"
{{< /highlight >}}
{{% /dir %}}

{{% dir name="drivers" type="map[string]map[string]interface{}" default="localhome" %}}
The configuration for the storage driver [[Ref]](https://github.com/cs3org/reva/tree/master/internal/http/services/dataprovider/dataprovider.go#L41)
The configuration for the storage driver [[Ref]](https://github.com/cs3org/reva/tree/master/internal/http/services/dataprovider/dataprovider.go#L42)
{{< highlight toml >}}
[http.services.dataprovider.drivers.localhome]
root = "/var/tmp/reva/"
Expand All @@ -35,3 +35,11 @@ user_layout = "{{.Username}}"
{{< /highlight >}}
{{% /dir %}}

{{% dir name="data_txs" type="map[string]map[string]interface{}" default="simple" %}}
The configuration for the data tx protocols [[Ref]](https://github.com/cs3org/reva/tree/master/internal/http/services/dataprovider/dataprovider.go#L43)
{{< highlight toml >}}
[http.services.dataprovider.data_txs.simple]

{{< /highlight >}}
{{% /dir %}}

87 changes: 73 additions & 14 deletions internal/http/interceptors/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package auth
import (
"fmt"
"net/http"
"strings"

gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
Expand All @@ -47,15 +48,16 @@ type config struct {
Priority int `mapstructure:"priority"`
GatewaySvc string `mapstructure:"gatewaysvc"`
// TODO(jdf): Realm is optional, will be filled with request host if not given?
Realm string `mapstructure:"realm"`
CredentialChain []string `mapstructure:"credential_chain"`
CredentialStrategies map[string]map[string]interface{} `mapstructure:"credential_strategies"`
TokenStrategy string `mapstructure:"token_strategy"`
TokenStrategies map[string]map[string]interface{} `mapstructure:"token_strategies"`
TokenManager string `mapstructure:"token_manager"`
TokenManagers map[string]map[string]interface{} `mapstructure:"token_managers"`
TokenWriter string `mapstructure:"token_writer"`
TokenWriters map[string]map[string]interface{} `mapstructure:"token_writers"`
Realm string `mapstructure:"realm"`
CredentialsByUserAgent map[string]string `mapstructure:"credentials_by_user_agent"`
CredentialChain []string `mapstructure:"credential_chain"`
CredentialStrategies map[string]map[string]interface{} `mapstructure:"credential_strategies"`
TokenStrategy string `mapstructure:"token_strategy"`
TokenStrategies map[string]map[string]interface{} `mapstructure:"token_strategies"`
TokenManager string `mapstructure:"token_manager"`
TokenManagers map[string]map[string]interface{} `mapstructure:"token_managers"`
TokenWriter string `mapstructure:"token_writer"`
TokenWriters map[string]map[string]interface{} `mapstructure:"token_writers"`
}

func parseConfig(m map[string]interface{}) (*config, error) {
Expand Down Expand Up @@ -93,8 +95,12 @@ func New(m map[string]interface{}, unprotected []string) (global.Middleware, err
conf.CredentialChain = []string{"basic", "bearer"}
}

credChain := []auth.CredentialStrategy{}
for i := range conf.CredentialChain {
if conf.CredentialsByUserAgent == nil {
conf.CredentialsByUserAgent = map[string]string{}
}

credChain := map[string]auth.CredentialStrategy{}
for i, key := range conf.CredentialChain {
f, ok := registry.NewCredentialFuncs[conf.CredentialChain[i]]
if !ok {
return nil, fmt.Errorf("credential strategy not found: %s", conf.CredentialChain[i])
Expand All @@ -104,7 +110,7 @@ func New(m map[string]interface{}, unprotected []string) (global.Middleware, err
if err != nil {
return nil, err
}
credChain = append(credChain, credStrategy)
credChain[key] = credStrategy
}

g, ok := tokenregistry.NewTokenFuncs[conf.TokenStrategy]
Expand Down Expand Up @@ -176,10 +182,43 @@ func New(m map[string]interface{}, unprotected []string) (global.Middleware, err
if creds == nil {
// TODO read realm from forwarded for header?
// see https://github.com/stanvit/go-forwarded as middleware

// indicate all possible authentications to the client
for i := range credChain {
credChain[i].AddWWWAuthenticate(w, r, conf.Realm)
// if the CredentialsByUserAgent is set, forward only configured credential
// challenge.

userAgent := r.UserAgent()
if len(conf.CredentialsByUserAgent) == 0 || userAgent == "" {
// set all available credentials challenges
for i := range credChain {
credChain[i].AddWWWAuthenticate(w, r, conf.Realm)
}

} else {
// set credentials depending on user agent.
// if not match, set all available credentials.
var match bool
for k, cred := range conf.CredentialsByUserAgent {
if strings.Contains(userAgent, k) {
if challenge, ok := credChain[cred]; ok {
challenge.AddWWWAuthenticate(w, r, conf.Realm)
match = true
continue
} else {
// warm that configured credential is not loaded
log.Warn().Msgf("auth: configured user-agent credential is not loaded: %s", cred)
}
}

}
// if no user agent is match, return all available challengues
if !match {
for i := range credChain {
credChain[i].AddWWWAuthenticate(w, r, conf.Realm)
}
}
}

w.WriteHeader(http.StatusUnauthorized)
return
}
Expand Down Expand Up @@ -247,3 +286,23 @@ func New(m map[string]interface{}, unprotected []string) (global.Middleware, err
}
return chain, nil
}

// applyWWWAuthenticate returns the WWW Authenticate challenges keys to use given an http request
// and available credentials.
func applyWWWAuthenticate(ua string, uam map[string]string, creds []string) []string {
if ua == "" || len(uam) == 0 {
return creds
}

cred, ok := uam[ua]
if ok {
for _, v := range creds {
if v == cred {
return []string{cred}
}
}
return creds
}

return nil
}
101 changes: 101 additions & 0 deletions internal/http/interceptors/auth/auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Copyright 2018-2020 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.

package auth

import (
"testing"
)

func TestApplyWWWAuthenticate(t *testing.T) {
type test struct {
userAgent string
userAgentMap map[string]string
availableCredentials []string
expected []string
}

tests := []*test{
// no user agent we return all available credentials
&test{
userAgent: "",
userAgentMap: map[string]string{},
availableCredentials: []string{},
expected: []string{},
},

// no user map we return all available credentials
&test{
userAgent: "mirall",
userAgentMap: map[string]string{},
availableCredentials: []string{"basic"},
expected: []string{"basic"},
},

// user agent set but no mapping set we return all credentials
&test{
userAgent: "mirall",
userAgentMap: map[string]string{},
availableCredentials: []string{"basic"},
expected: []string{"basic"},
},

// user mapping set to non available credential, we return all available
&test{
userAgent: "mirall",
userAgentMap: map[string]string{"mirall": "notfound"},
availableCredentials: []string{"basic", "bearer"},
expected: []string{"basic", "bearer"},
},

// user mapping set and we return only desired credential
&test{
userAgent: "mirall",
userAgentMap: map[string]string{"mirall": "bearer"},
availableCredentials: []string{"basic", "bearer"},
expected: []string{"bearer"},
},
}

for _, test := range tests {
got := applyWWWAuthenticate(
test.userAgent,
test.userAgentMap,
test.availableCredentials)

if !match(got, test.expected) {
fail(t, got, test.expected)
}
}
}

func match(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i, v := range a {
if v != b[i] {
return false
}
}
return true
}

func fail(t *testing.T, got, expected []string) {
t.Fatalf("got: %+v expected: %+v", got, expected)
}

0 comments on commit f846b3d

Please sign in to comment.