Skip to content

Commit

Permalink
plugins: Add support to specify bearer token path
Browse files Browse the repository at this point in the history
This change updates the bearer token config to allow clients to specify
a path to the token. With this refreshing tokens becomes easier as OPA
will now reload the token from file.

Fixes open-policy-agent#2241

Signed-off-by: Ashutosh Narkar <anarkar4387@gmail.com>
  • Loading branch information
ashutosh-narkar committed Apr 11, 2020
1 parent a2e6f1b commit a5be4f4
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 17 deletions.
3 changes: 2 additions & 1 deletion docs/content/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,12 +232,13 @@ itself to the service.
#### Bearer token

OPA will authenticate using the specified bearer token and schema; to enable bearer token
authentication, the token must be specified. The schema is optional and will default to `Bearer`
authentication, either the token or the path to the token must be specified. The schema is optional and will default to `Bearer`
if unspecified.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `services[_].credentials.bearer.token` | `string` | Yes | Enables token-based authentication and supplies the bearer token to authenticate with. |
| `services[_].credentials.bearer.token_path` | `string` | Yes | Enables token-based authentication and supplies the path to the bearer token to authenticate with. |
| `services[_].credentials.bearer.scheme` | `string` | No | Bearer token scheme to specify. |

#### Client TLS certificate
Expand Down
24 changes: 21 additions & 3 deletions plugins/rest/rest_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"net"
"net/http"
"net/url"
"strings"
"time"

"github.com/sirupsen/logrus"
Expand Down Expand Up @@ -83,23 +84,40 @@ func (ap *defaultAuthPlugin) Prepare(req *http.Request) error {

// bearerAuthPlugin represents authentication via a bearer token in the HTTP Authorization header
type bearerAuthPlugin struct {
Scheme string `json:"scheme,omitempty"`
Token string `json:"token"`
Token string `json:"token"`
TokenPath string `json:"token_path"`
Scheme string `json:"scheme,omitempty"`
}

func (ap *bearerAuthPlugin) NewClient(c Config) (*http.Client, error) {
t, err := defaultTLSConfig(c)
if err != nil {
return nil, err
}

if ap.Token != "" && ap.TokenPath != "" {
return nil, errors.New("invalid config: specify a value for either the \"token\" or \"token_path\" field")
}

if ap.Scheme == "" {
ap.Scheme = "Bearer"
}

return defaultRoundTripperClient(t), nil
}

func (ap *bearerAuthPlugin) Prepare(req *http.Request) error {
req.Header.Add("Authorization", fmt.Sprintf("%v %v", ap.Scheme, ap.Token))
token := ap.Token

if ap.TokenPath != "" {
bytes, err := ioutil.ReadFile(ap.TokenPath)
if err != nil {
return err
}
token = strings.TrimSpace(string(bytes))
}

req.Header.Add("Authorization", fmt.Sprintf("%v %v", ap.Scheme, token))
return nil
}

Expand Down
154 changes: 141 additions & 13 deletions plugins/rest/rest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,121 @@ func TestBearerTokenCustomScheme(t *testing.T) {
testBearerToken(t, "Acmecorp-Token", "secret")
}

func TestBearerTokenPath(t *testing.T) {
ts := testServer{
t: t,
expBearerScheme: "",
expBearerToken: "secret",
expBearerTokenPath: true,
}
ts.start()
defer ts.stop()

files := map[string]string{
"token.txt": "secret",
}

test.WithTempFS(files, func(path string) {
tokenPath := filepath.Join(path, "token.txt")

client := newTestBearerClient(t, &ts, tokenPath)

ctx := context.Background()
if _, err := client.Do(ctx, "GET", "test"); err != nil {
t.Fatalf("Unexpected error: %v", err)
}

// Stop server and update the token
ts.stop()
ts.expBearerToken = "newsecret"
ts.start()

// check client cannot access the server
client = newTestBearerClient(t, &ts, tokenPath)

if resp, err := client.Do(ctx, "GET", "test"); err == nil {
bodyBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}

if resp.StatusCode != http.StatusUnauthorized {
t.Fatalf("Expected http status %v but got %v", http.StatusUnauthorized, resp.StatusCode)
}

expectedErrMsg := "Expected bearer token \"newsecret\", got authorization header \"Bearer secret\""

if string(bodyBytes) != expectedErrMsg {
t.Fatalf("Expected error message %v but got %v", expectedErrMsg, string(bodyBytes))
}
} else {
t.Fatalf("Unexpected error: %v", err)
}

// Update the token file and try again
if err := ioutil.WriteFile(filepath.Join(path, "token.txt"), []byte("newsecret"), 0644); err != nil {
t.Fatalf("Unexpected error: %s", err)
}

if _, err := client.Do(ctx, "GET", "test"); err != nil {
t.Fatalf("Unexpected error: %v", err)
}
})
}

func TestBearerTokenInvalidConfig(t *testing.T) {
ts := testServer{
t: t,
expBearerScheme: "",
expBearerToken: "secret",
}
ts.start()
defer ts.stop()

config := fmt.Sprintf(`{
"name": "foo",
"url": %q,
"credentials": {
"bearer": {
"token_path": "%s",
"token": "%s"
}
}
}`, ts.server.URL, "token.txt", "secret")
client, err := New([]byte(config))
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
ctx := context.Background()

_, err = client.Do(ctx, "GET", "test")

if err == nil {
t.Fatalf("Expected error but got nil")
}

if !strings.HasPrefix(err.Error(), "invalid config") {
t.Fatalf("Unexpected error message %v\n", err)
}
}

func newTestBearerClient(t *testing.T, ts *testServer, tokenPath string) *Client {
config := fmt.Sprintf(`{
"name": "foo",
"url": %q,
"credentials": {
"bearer": {
"token_path": %q
}
}
}`, ts.server.URL, tokenPath)
client, err := New([]byte(config))
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
return &client
}

func TestClientCert(t *testing.T) {
ts := testServer{
t: t,
Expand Down Expand Up @@ -331,17 +446,18 @@ func newTestClient(t *testing.T, ts *testServer, certPath string, keypath string
}

type testServer struct {
t *testing.T
server *httptest.Server
expPath string
expMethod string
expBearerToken string
expBearerScheme string
tls bool
clientCertPem []byte
clientCertKey []byte
expectClientCert bool
serverCertPool *x509.CertPool
t *testing.T
server *httptest.Server
expPath string
expMethod string
expBearerToken string
expBearerScheme string
expBearerTokenPath bool
tls bool
clientCertPem []byte
clientCertKey []byte
expectClientCert bool
serverCertPool *x509.CertPool
}

func (t *testServer) handle(w http.ResponseWriter, r *http.Request) {
Expand All @@ -357,10 +473,22 @@ func (t *testServer) handle(w http.ResponseWriter, r *http.Request) {
if len(r.Header["Authorization"]) > 0 {
auth := r.Header["Authorization"][0]
if t.expBearerScheme != "" && !strings.HasPrefix(auth, t.expBearerScheme) {
t.t.Fatalf("Expected bearer scheme %q, got authorization header %q", t.expBearerScheme, auth)
errMsg := fmt.Sprintf("Expected bearer scheme %q, got authorization header %q", t.expBearerScheme, auth)
if t.expBearerTokenPath {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(errMsg))
return
}
t.t.Fatalf(errMsg)
}
if t.expBearerToken != "" && !strings.HasSuffix(auth, t.expBearerToken) {
t.t.Fatalf("Expected bearer token %q, got authorization header %q", t.expBearerToken, auth)
errMsg := fmt.Sprintf("Expected bearer token %q, got authorization header %q", t.expBearerToken, auth)
if t.expBearerTokenPath {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(errMsg))
return
}
t.t.Fatalf(errMsg)
}
}
if t.expectClientCert {
Expand Down

0 comments on commit a5be4f4

Please sign in to comment.