Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bearer access token #118

Merged
merged 10 commits into from
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/authentication/http/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func main() {
auth := authentication.NewAuth0(f)

// Set up bearer token middleware
bearer := middleware.BearerToken(auth)
bearer := middleware.BearerJWT(auth)

// Define HTTP handler
h := http.Handler(http.HandlerFunc(handler))
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ require (
github.com/fatih/structtag v1.2.0
github.com/form3tech-oss/jwt-go v3.2.5+incompatible
github.com/fsouza/fake-gcs-server v1.47.7
github.com/gazebo-web/auth v0.8.0
github.com/gazebo-web/auth v0.8.1-0.20240304193427-135e58a101d6
github.com/go-chi/chi/v5 v5.0.11
github.com/go-chi/render v1.0.3
github.com/golang-jwt/jwt/v5 v5.2.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ github.com/fsouza/fake-gcs-server v1.47.7 h1:56/U4rKY081TaNbq0gHWi7/71UxC2KROqcn
github.com/fsouza/fake-gcs-server v1.47.7/go.mod h1:4vPUynN8/zZlxk5Jpy6LvvTTxItdTAObK4DYnp89Jys=
github.com/gazebo-web/auth v0.8.0 h1:TwpFAX9RyZ0uthZ2ubj/svAl7/RzwbxAZIbqim2+OYQ=
github.com/gazebo-web/auth v0.8.0/go.mod h1:uvecK4glAnlhq+HIKafL/nJdRbrVtw9TKUVQcXhYYJY=
github.com/gazebo-web/auth v0.8.1-0.20240304193427-135e58a101d6 h1:WPw3pren4cv+Hh1Ivfkqisr1kToT8aFnpUoeKWtii6c=
github.com/gazebo-web/auth v0.8.1-0.20240304193427-135e58a101d6/go.mod h1:uvecK4glAnlhq+HIKafL/nJdRbrVtw9TKUVQcXhYYJY=
github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA=
github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
Expand Down
27 changes: 27 additions & 0 deletions middleware/auth_access_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package middleware

import (
"context"

"github.com/gazebo-web/auth/pkg/authentication"
grpc_auth "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/auth"
)

// BearerAccessTokenAuthFuncGRPC returns a grpc_auth.AuthFunc that allows to validate
// incoming access tokens found in the Authorization header. These tokens are
// bearer tokens signed by different authentication providers.
// The validator function received as an argument performs the validation for
// every incoming bearer token.
func BearerAccessTokenAuthFuncGRPC(validator authentication.AccessTokenAuthentication) grpc_auth.AuthFunc {
return func(ctx context.Context) (context.Context, error) {
token, err := grpc_auth.AuthFromMD(ctx, "bearer")
if err != nil {
return nil, err
}
err = validator(ctx, token)
if err != nil {
return nil, err
}
return ctx, nil
}
}
103 changes: 103 additions & 0 deletions middleware/auth_access_token_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package middleware

import (
"context"
"testing"

grpc_auth "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/auth"
"github.com/grpc-ecosystem/go-grpc-middleware/v2/metadata"
grpc_test "github.com/grpc-ecosystem/go-grpc-middleware/v2/testing/testpb"
"github.com/stretchr/testify/suite"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
grpc_metadata "google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)

func TestAuthFuncGRPC_AccessToken(t *testing.T) {
ss := &TestAuthAccessTokenSuite{
InterceptorTestSuite: &grpc_test.InterceptorTestSuite{
TestService: newTestAuthenticationAccessToken(),
ServerOpts: []grpc.ServerOption{
grpc.StreamInterceptor(grpc_auth.StreamServerInterceptor(BearerAccessTokenAuthFuncGRPC(validateAccessToken))),
grpc.UnaryInterceptor(grpc_auth.UnaryServerInterceptor(BearerAccessTokenAuthFuncGRPC(validateAccessToken))),
},
},
}
suite.Run(t, ss)
}

func validateAccessToken(ctx context.Context, token string) error {
if token != "ey.LfexACqSU5qgYgp9EXSdR4rtnD7BJ0oOCNi8BKIkZ4vt25jRxyu6AXAKVNrtItb1" {
return status.Error(codes.Unauthenticated, "Invalid access token")
}
return nil
}

type TestAuthAccessTokenSuite struct {
*grpc_test.InterceptorTestSuite
}

func (suite *TestAuthAccessTokenSuite) TestNoBearer() {
ctx := context.Background()
client := suite.NewClient()
res, err := client.Ping(ctx, &grpc_test.PingRequest{})
suite.Assert().Error(err)
suite.Assert().ErrorIs(err, status.Error(codes.Unauthenticated, "Request unauthenticated with bearer"))
suite.Assert().Nil(res)
}

func (suite *TestAuthAccessTokenSuite) TestNoToken() {
ctx := context.Background()
md := grpc_metadata.Pairs("authorization", "bearer")
ctx = metadata.MD(md).ToOutgoing(ctx)

client := suite.NewClient()
res, err := client.Ping(ctx, &grpc_test.PingRequest{})
suite.Assert().Error(err)
suite.Assert().ErrorIs(err, status.Error(codes.Unauthenticated, "Bad authorization string"))
suite.Assert().Nil(res)
}

func (suite *TestAuthAccessTokenSuite) TestInvalidScheme() {
ctx := context.Background()
ctx = ctxWithToken(ctx, "basic_auth", "test:test")

client := suite.NewClient()
res, err := client.Ping(ctx, &grpc_test.PingRequest{})
suite.Assert().Error(err)
suite.Assert().ErrorIs(err, status.Error(codes.Unauthenticated, "Request unauthenticated with bearer"))
suite.Assert().Nil(res)
}

func (suite *TestAuthAccessTokenSuite) TestInvalidToken() {
ctx := context.Background()
ctx = ctxWithToken(ctx, "bearer", "ey.FfexACqSU5qgYgp9EXSdR4rtnD7BJ0oOCNi8BKIkZ4vt25jRxyu6AXAKVNrtItb1")

client := suite.NewClient()
res, err := client.Ping(ctx, &grpc_test.PingRequest{})
suite.Assert().Error(err)
suite.Assert().ErrorIs(err, status.Error(codes.Unauthenticated, "Invalid access token"))
suite.Assert().Nil(res)
}

func (suite *TestAuthAccessTokenSuite) TestValidToken() {
ctx := context.Background()
ctx = ctxWithToken(ctx, "bearer", "ey.LfexACqSU5qgYgp9EXSdR4rtnD7BJ0oOCNi8BKIkZ4vt25jRxyu6AXAKVNrtItb1")

client := suite.NewClient()
res, err := client.Ping(ctx, &grpc_test.PingRequest{})
suite.Assert().NoError(err)
suite.Assert().NotNil(res)
}

type testAuthAccessToken struct {
grpc_test.UnimplementedTestServiceServer
}

func (t *testAuthAccessToken) Ping(ctx context.Context, request *grpc_test.PingRequest) (*grpc_test.PingResponse, error) {
return &grpc_test.PingResponse{}, nil
}
func newTestAuthenticationAccessToken() grpc_test.TestServiceServer {
return &testAuthAccessToken{}
}
12 changes: 6 additions & 6 deletions middleware/auth_bearer.go → middleware/auth_jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,23 @@ import (
"google.golang.org/grpc/status"
)

// BearerToken returns a Middleware for authenticating users using Bearer Tokens in JWT format.
func BearerToken(authentication authentication.Authentication) Middleware {
// BearerJWT returns a Middleware for authenticating users using Bearer Tokens in JWT format.
func BearerJWT(authentication authentication.Authentication) Middleware {
return newTokenMiddleware(authentication.VerifyJWT, request.BearerExtractor{})
}

// BearerAuthFuncGRPC returns a new grpc_auth.AuthFunc to use with the gazebo-web authentication library.
// BearerJWTAuthFuncGRPC returns a new grpc_auth.AuthFunc to use with the gazebo-web authentication library.
//
// The passed in context.Context will contain the gRPC metadata.MD object (for header-based authentication) and
// the peer.Peer information that can contain transport-based credentials (e.g. `credentials.AuthInfo`).
//
// auth := authentication.New[...]()
//
// srv := grpc.NewServer(
// grpc.StreamInterceptor(grpc_auth.StreamServerInterceptor(BearerAuthFuncGRPC(auth))),
// grpc.UnaryInterceptor(grpc_auth.UnaryServerInterceptor(BearerAuthFuncGRPC(auth))),
// grpc.StreamInterceptor(grpc_auth.StreamServerInterceptor(BearerJWTAuthFuncGRPC(auth))),
// grpc.UnaryInterceptor(grpc_auth.UnaryServerInterceptor(BearerJWTAuthFuncGRPC(auth))),
// )
func BearerAuthFuncGRPC(auth authentication.Authentication, claimInjector ClaimInjectorJWT) grpc_auth.AuthFunc {
func BearerJWTAuthFuncGRPC(auth authentication.Authentication, claimInjector ClaimInjectorJWT) grpc_auth.AuthFunc {
return func(ctx context.Context) (context.Context, error) {
raw, err := grpc_auth.AuthFromMD(ctx, "bearer")
if err != nil {
Expand Down
94 changes: 47 additions & 47 deletions middleware/auth_bearer_test.go → middleware/auth_jwt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import (
)

func TestBearerToken_NoAuthorizationHeader(t *testing.T) {
handler := setupBearerTokenTest(t)
handler := setupJWTTokenTest(t)

wr := httptest.NewRecorder()
r := httptest.NewRequest("GET", "https://gazebosim.org", nil)
Expand All @@ -37,7 +37,7 @@ func TestBearerToken_NoAuthorizationHeader(t *testing.T) {
}

func TestBearerToken_EmptyAuthorizationHeader(t *testing.T) {
handler := setupBearerTokenTest(t)
handler := setupJWTTokenTest(t)

wr := httptest.NewRecorder()
r := httptest.NewRequest("GET", "https://gazebosim.org", nil)
Expand All @@ -48,7 +48,7 @@ func TestBearerToken_EmptyAuthorizationHeader(t *testing.T) {
}

func TestBearerToken_InvalidAuthorizationHeader(t *testing.T) {
handler := setupBearerTokenTest(t)
handler := setupJWTTokenTest(t)

wr := httptest.NewRecorder()
r := httptest.NewRequest("GET", "https://gazebosim.org", nil)
Expand All @@ -59,7 +59,7 @@ func TestBearerToken_InvalidAuthorizationHeader(t *testing.T) {
}

func TestBearerToken_EmptyBearerToken(t *testing.T) {
handler := setupBearerTokenTest(t)
handler := setupJWTTokenTest(t)

wr := httptest.NewRecorder()
r := httptest.NewRequest("GET", "https://gazebosim.org", nil)
Expand All @@ -70,7 +70,7 @@ func TestBearerToken_EmptyBearerToken(t *testing.T) {
}

func TestBearerToken_InvalidBearerTokenRandomString(t *testing.T) {
handler := setupBearerTokenTest(t)
handler := setupJWTTokenTest(t)

wr := httptest.NewRecorder()
r := httptest.NewRequest("GET", "https://gazebosim.org", nil)
Expand All @@ -81,7 +81,7 @@ func TestBearerToken_InvalidBearerTokenRandomString(t *testing.T) {
}

func TestBearerToken_InvalidBearerTokenSignedByAnother(t *testing.T) {
handler := setupBearerTokenTest(t)
handler := setupJWTTokenTest(t)

wr := httptest.NewRecorder()
r := httptest.NewRequest("GET", "https://gazebosim.org", nil)
Expand All @@ -92,7 +92,7 @@ func TestBearerToken_InvalidBearerTokenSignedByAnother(t *testing.T) {
}

func TestBearerToken(t *testing.T) {
handler := setupBearerTokenTest(t)
handler := setupJWTTokenTest(t)

token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims(map[string]interface{}{
"sub": "gazebo-web",
Expand Down Expand Up @@ -128,13 +128,13 @@ func TestBearerToken(t *testing.T) {
assert.Equal(t, http.StatusOK, wr.Code)
}

func setupBearerTokenTest(t *testing.T) http.Handler {
func setupJWTTokenTest(t *testing.T) http.Handler {
// Set up public key for Authentication service
publicKey, err := os.ReadFile("./testdata/key.pem")
require.NoError(t, err)

// Set up a Bearer token middleware
mw := BearerToken(authentication.NewAuth0(publicKey))
mw := BearerJWT(authentication.NewAuth0(publicKey))

// Define handler that will be wrapped by the middleware
handler := mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand All @@ -147,18 +147,18 @@ func setupBearerTokenTest(t *testing.T) http.Handler {
return handler
}

func TestAuthFuncGRPC(t *testing.T) {
auth := newTestAuthentication()
ss := &TestAuthSuite{
func TestAuthFuncGRPC_JWT(t *testing.T) {
auth := newTestAuthenticationJWT()
ss := &TestAuthJWTSuite{
auth: auth,
InterceptorTestSuite: &grpc_test.InterceptorTestSuite{
TestService: auth,
ServerOpts: []grpc.ServerOption{
grpc.StreamInterceptor(grpc_auth.StreamServerInterceptor(BearerAuthFuncGRPC(auth, groupClaimInjectors(mandatoryInjection,
grpc.StreamInterceptor(grpc_auth.StreamServerInterceptor(BearerJWTAuthFuncGRPC(auth, groupClaimInjectors(mandatoryInjection,
groupClaimInjectors(mandatoryInjection, SubjectClaimer),
groupClaimInjectors(optionalInjection, EmailClaimer),
)))),
grpc.UnaryInterceptor(grpc_auth.UnaryServerInterceptor(BearerAuthFuncGRPC(auth, groupClaimInjectors(mandatoryInjection,
grpc.UnaryInterceptor(grpc_auth.UnaryServerInterceptor(BearerJWTAuthFuncGRPC(auth, groupClaimInjectors(mandatoryInjection,
groupClaimInjectors(mandatoryInjection, SubjectClaimer),
groupClaimInjectors(optionalInjection, EmailClaimer),
)))),
Expand All @@ -168,12 +168,12 @@ func TestAuthFuncGRPC(t *testing.T) {
suite.Run(t, ss)
}

type TestAuthSuite struct {
type TestAuthJWTSuite struct {
*grpc_test.InterceptorTestSuite
auth *testAuthService
auth *testAuthJWTService
}

func (suite *TestAuthSuite) TestVerifyJWT_FailsNoBearer() {
func (suite *TestAuthJWTSuite) TestVerifyJWT_FailsNoBearer() {
ctx := context.Background()

client := suite.NewClient()
Expand All @@ -183,7 +183,7 @@ func (suite *TestAuthSuite) TestVerifyJWT_FailsNoBearer() {
suite.Assert().Nil(res)
}

func (suite *TestAuthSuite) TestVerifyJWT_FailsVerifyJWTError() {
func (suite *TestAuthJWTSuite) TestVerifyJWT_FailsVerifyJWTError() {
ctx := ctxWithToken(context.Background(), "bearer", "1234")
expectedError := errors.New("failed to verify token")

Expand All @@ -197,7 +197,7 @@ func (suite *TestAuthSuite) TestVerifyJWT_FailsVerifyJWTError() {
suite.Assert().Nil(res)
}

func (suite *TestAuthSuite) TestVerifyJWT_Success() {
func (suite *TestAuthJWTSuite) TestVerifyJWT_Success() {
ctx := ctxWithToken(context.Background(), "bearer", "1234")

expectedCtx := mock.AnythingOfType("*context.valueCtx")
Expand All @@ -217,34 +217,6 @@ func ctxWithToken(ctx context.Context, scheme string, token string) context.Cont
return metadata.MD(md).ToOutgoing(ctx)
}

type testAuthService struct {
grpc_test.UnimplementedTestServiceServer
mock.Mock
}

func (s *testAuthService) Ping(ctx context.Context, _ *grpc_test.PingRequest) (*grpc_test.PingResponse, error) {
sub, err := ExtractGRPCAuthSubject(ctx)
if err != nil {
return nil, err
}
email, err := ExtractGRPCAuthEmail(ctx)
if err != nil {
return nil, err
}
return &grpc_test.PingResponse{
Value: strings.Join([]string{sub, email}, ";"),
}, nil
}

func (s *testAuthService) VerifyJWT(ctx context.Context, token string) (jwt.Claims, error) {
args := s.Called(ctx, token)
return args.Get(0).(jwt.Claims), args.Error(1)
}

func newTestAuthentication() *testAuthService {
return &testAuthService{}
}

func TestGroupClaimInjectors_Mandatory(t *testing.T) {
ctx := grpc_metadata.NewIncomingContext(context.Background(), nil)
c := groupClaimInjectors(mandatoryInjection, SubjectClaimer)
Expand Down Expand Up @@ -329,3 +301,31 @@ func TestGroupClaimInjectors_Combined_NoEmail(t *testing.T) {
emails := grpc_metadata.ValueFromIncomingContext(ctx, metadataEmailKey)
assert.Empty(t, emails)
}

type testAuthJWTService struct {
grpc_test.UnimplementedTestServiceServer
mock.Mock
}

func (s *testAuthJWTService) Ping(ctx context.Context, _ *grpc_test.PingRequest) (*grpc_test.PingResponse, error) {
sub, err := ExtractGRPCAuthSubject(ctx)
if err != nil {
return nil, err
}
email, err := ExtractGRPCAuthEmail(ctx)
if err != nil {
return nil, err
}
return &grpc_test.PingResponse{
Value: strings.Join([]string{sub, email}, ";"),
}, nil
}

func (s *testAuthJWTService) VerifyJWT(ctx context.Context, token string) (jwt.Claims, error) {
args := s.Called(ctx, token)
return args.Get(0).(jwt.Claims), args.Error(1)
}

func newTestAuthenticationJWT() *testAuthJWTService {
return &testAuthJWTService{}
}
Loading
Loading