Skip to content

Commit

Permalink
Add HTTP authentication based on the "Accept" header
Browse files Browse the repository at this point in the history
It may be desirable in certain cases to only perform OpenID Connect
based authentication in case users visit a page through the web browser.
For non-interactive API access it may be desirable to return HTTP 401
instead.

This change solves this by adding a decorator for Authenticator that can
conditionalize its invocation based on whether a supported media type is
provided.
  • Loading branch information
EdSchouten committed Sep 22, 2023
1 parent ee3fc0b commit 02a681e
Show file tree
Hide file tree
Showing 7 changed files with 311 additions and 67 deletions.
10 changes: 10 additions & 0 deletions internal/mock/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,15 @@ gomock(
package = "mock",
)

gomock(
name = "http",
out = "http.go",
interfaces = ["Authenticator"],
library = "//pkg/http",
mock_names = {"Authenticator": "MockHTTPAuthenticator"},
package = "mock",
)

gomock(
name = "jwt",
out = "jwt.go",
Expand Down Expand Up @@ -304,6 +313,7 @@ go_library(
"filesystem_path.go",
"grpc.go",
"grpc_go.go",
"http.go",
"jwt.go",
"random.go",
"redis.go",
Expand Down
3 changes: 3 additions & 0 deletions pkg/http/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "http",
srcs = [
"accept_header_authenticator.go",
"allow_authenticator.go",
"any_authenticator.go",
"authenticating_handler.go",
Expand All @@ -28,6 +29,7 @@ go_library(
"//pkg/proto/http/oidc",
"//pkg/random",
"//pkg/util",
"@com_github_aohorodnyk_mimeheader//:mimeheader",
"@com_github_jmespath_go_jmespath//:go-jmespath",
"@com_github_prometheus_client_golang//prometheus",
"@com_github_prometheus_client_golang//prometheus/promhttp",
Expand All @@ -43,6 +45,7 @@ go_library(
go_test(
name = "http_test",
srcs = [
"accept_header_authenticator_test.go",
"allow_authenticator_test.go",
"deny_authenticator_test.go",
"oidc_authenticator_test.go",
Expand Down
38 changes: 38 additions & 0 deletions pkg/http/accept_header_authenticator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package http

import (
"net/http"

"github.com/aohorodnyk/mimeheader"
"github.com/buildbarn/bb-storage/pkg/auth"

"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

type acceptHeaderAuthenticator struct {
base Authenticator
mediaTypes []string
mismatchErr error
}

// NewAcceptHeaderAuthenticator creates a decorator for Authenticator
// that only performs authentication if the HTTP request's "Accept" header
// contains a matching media type. This can, for example, be used to
// limit OpenID Connect authentication to requests originating from a
// web browser.
func NewAcceptHeaderAuthenticator(base Authenticator, mediaTypes []string) Authenticator {
return &acceptHeaderAuthenticator{
base: base,
mediaTypes: mediaTypes,
mismatchErr: status.Errorf(codes.Unauthenticated, "Client does not accept media types %v", mediaTypes),
}
}

func (a *acceptHeaderAuthenticator) Authenticate(w http.ResponseWriter, r *http.Request) (*auth.AuthenticationMetadata, error) {
acceptHeader := mimeheader.ParseAcceptHeader(r.Header.Get("Accept"))
if _, _, ok := acceptHeader.Negotiate(a.mediaTypes, ""); ok {
return a.base.Authenticate(w, r)
}
return nil, a.mismatchErr
}
70 changes: 70 additions & 0 deletions pkg/http/accept_header_authenticator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package http_test

import (
"net/http"
"testing"

"github.com/buildbarn/bb-storage/internal/mock"
"github.com/buildbarn/bb-storage/pkg/auth"
bb_http "github.com/buildbarn/bb-storage/pkg/http"
auth_pb "github.com/buildbarn/bb-storage/pkg/proto/auth"
"github.com/buildbarn/bb-storage/pkg/testutil"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"

"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/structpb"
)

func TestAcceptHeaderAuthenticator(t *testing.T) {
ctrl := gomock.NewController(t)

baseAuthenticator := mock.NewMockHTTPAuthenticator(ctrl)
authenticator := bb_http.NewAcceptHeaderAuthenticator(baseAuthenticator, []string{"text/html", "font/*"})

t.Run("MissingHeader", func(t *testing.T) {
w := mock.NewMockResponseWriter(ctrl)
r, err := http.NewRequest(http.MethodGet, "/path", nil)
require.NoError(t, err)

_, err = authenticator.Authenticate(w, r)
testutil.RequireEqualStatus(t, status.Error(codes.Unauthenticated, "Client does not accept media types [text/html font/*]"), err)
})

t.Run("MismatchingHeader", func(t *testing.T) {
w := mock.NewMockResponseWriter(ctrl)
r, err := http.NewRequest(http.MethodGet, "/path", nil)
require.NoError(t, err)
r.Header.Set("Accept", "application/xml")

_, err = authenticator.Authenticate(w, r)
testutil.RequireEqualStatus(t, status.Error(codes.Unauthenticated, "Client does not accept media types [text/html font/*]"), err)
})

t.Run("MatchingHeader", func(t *testing.T) {
for _, header := range []string{
"font/otf",
"text/*",
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
} {
w := mock.NewMockResponseWriter(ctrl)
r, err := http.NewRequest(http.MethodGet, "/path", nil)
require.NoError(t, err)
r.Header.Set("Accept", header)

expectedMetadata := auth.MustNewAuthenticationMetadataFromProto(&auth_pb.AuthenticationMetadata{
Public: structpb.NewStructValue(&structpb.Struct{
Fields: map[string]*structpb.Value{
"username": structpb.NewStringValue("John Doe"),
},
}),
})
baseAuthenticator.EXPECT().Authenticate(w, r).Return(expectedMetadata, nil)

actualMetadata, err := authenticator.Authenticate(w, r)
require.NoError(t, err)
require.Equal(t, expectedMetadata, actualMetadata)
}
})
}
6 changes: 6 additions & 0 deletions pkg/http/authenticator.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@ func NewAuthenticatorFromConfiguration(policy *configuration.AuthenticationPolic
cookieName,
cookieAEAD,
clock.SystemClock)
case *configuration.AuthenticationPolicy_AcceptHeader:
base, err := NewAuthenticatorFromConfiguration(policyKind.AcceptHeader.Policy)
if err != nil {
return nil, err
}
return NewAcceptHeaderAuthenticator(base, policyKind.AcceptHeader.MediaTypes), nil
default:
return nil, status.Error(codes.InvalidArgument, "Configuration did not contain an authentication policy type")
}
Expand Down
Loading

0 comments on commit 02a681e

Please sign in to comment.