Skip to content

Commit

Permalink
internal/access: add access package
Browse files Browse the repository at this point in the history
This change adds an access package which is intented to contain
functions which will handle Identity Aware Proxy authentication. It
may be extended to include authorization logic in the future.

Fixes golang/go#48729
Updates golang/go#47521

Change-Id: I68cd90c3e83066763e3194fcb58e324c3630f811
Reviewed-on: https://go-review.googlesource.com/c/build/+/358915
Reviewed-by: Heschi Kreinick <heschi@google.com>
Reviewed-by: Alexander Rakoczy <alex@golang.org>
Trust: Alexander Rakoczy <alex@golang.org>
Run-TryBot: Alexander Rakoczy <alex@golang.org>
TryBot-Result: Go Bot <gobot@golang.org>
  • Loading branch information
cagedmantis committed Nov 8, 2021
1 parent ba3bd24 commit a21315d
Show file tree
Hide file tree
Showing 5 changed files with 234 additions and 0 deletions.
1 change: 1 addition & 0 deletions go.mod
Expand Up @@ -27,6 +27,7 @@ require (
github.com/googleapis/gax-go/v2 v2.0.5
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4
github.com/jackc/pgconn v1.10.0
github.com/jackc/pgx/v4 v4.13.0
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1
Expand Down
1 change: 1 addition & 0 deletions go.sum
Expand Up @@ -382,6 +382,7 @@ github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4 h1:z53tR0945TRRQO/fLEVPI6SMv7ZflF0TEaTAoU7tOzg=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
Expand Down
127 changes: 127 additions & 0 deletions internal/access/access.go
@@ -0,0 +1,127 @@
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package access

import (
"context"
"fmt"
"log"

grpcauth "github.com/grpc-ecosystem/go-grpc-middleware/auth"
"google.golang.org/api/idtoken"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)

type contextKeyIAP string

const (
// contextIAP is the key used to store IAP provided fields in the context.
contextIAP contextKeyIAP = contextKeyIAP("IAP-JWT")

// IAPHeaderJWT is the header IAP stores the JWT token in.
iapHeaderJWT = "X-Goog-IAP-JWT-Assertion"
// iapHeaderEmail is the header IAP stores the email in.
iapHeaderEmail = "X-Goog-Authenticated-User-Email"
// iapHeaderID is the header IAP stores the user id in.
iapHeaderID = "X-Goog-Authenticated-User-Id"
)

// IAPFields contains the values for the headers retrieved from Identity Aware
// Proxy.
type IAPFields struct {
Email string
ID string
}

// IAPFromContext retrieves the IAPFields stored in the context if it exists.
func IAPFromContext(ctx context.Context) (*IAPFields, error) {
v := ctx.Value(contextIAP)
if v == nil {
return nil, fmt.Errorf("IAP fields not found in context")
}
iap, ok := v.(IAPFields)
if !ok {
return nil, fmt.Errorf("context value retrieved does not match expected type")
}
return &iap, nil
}

// iapAuthFunc creates an authentication function used to create a GRPC interceptor.
// It ensures that the caller has successfully authenticated via IAP. If the caller
// has authenticated, the headers created by IAP will be added to the request scope
// context passed down to the server implementation.
func iapAuthFunc(audience string, validatorFn validator) grpcauth.AuthFunc {
return func(ctx context.Context) (context.Context, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return ctx, status.Error(codes.Internal, codes.Internal.String())
}
jwt := md.Get(iapHeaderJWT)
if len(jwt) == 0 {
return ctx, status.Error(codes.Unauthenticated, "IAP JWT not found in request")
}
var err error
if _, err = validatorFn(ctx, jwt[0], audience); err != nil {
log.Printf("access: error validating JWT: %s", err)
return ctx, status.Error(codes.Unauthenticated, "unable to authenticate")
}
if ctx, err = contextWithIAPMD(ctx, md); err != nil {
log.Printf("access: unable to set IAP fields in context: %s", err)
return ctx, status.Error(codes.Unauthenticated, "unable to authenticate")
}
return ctx, nil
}
}

// contextWithIAPMD copies the headers set by IAP into the context.
func contextWithIAPMD(ctx context.Context, md metadata.MD) (context.Context, error) {
retrieveFn := func(fmd metadata.MD, mdKey string) (string, error) {
val := fmd.Get(mdKey)
if len(val) == 0 || val[0] == "" {
return "", fmt.Errorf("unable to retrieve %s from GRPC metadata", mdKey)
}
return val[0], nil
}
var iap IAPFields
var err error
if iap.Email, err = retrieveFn(md, iapHeaderEmail); err != nil {
return ctx, fmt.Errorf("unable to retrieve metadata field: %s", iapHeaderEmail)
}
if iap.ID, err = retrieveFn(md, iapHeaderID); err != nil {
return ctx, fmt.Errorf("unable to retrieve metadata field: %s", iapHeaderID)
}
return context.WithValue(ctx, contextIAP, iap), nil
}

// RequireIAPAuthUnaryInterceptor creates an authentication interceptor for a GRPC
// server. This requires Identity Aware Proxy authentication. Upon a successful authentication
// the associated headers will be copied into the request context.
func RequireIAPAuthUnaryInterceptor(audience string) grpc.UnaryServerInterceptor {
return grpcauth.UnaryServerInterceptor(iapAuthFunc(audience, idtoken.Validate))
}

// RequireIAPAuthStreamInterceptor creates an authentication interceptor for a GRPC
// streaming server. This requires Identity Aware Proxy authentication. Upon a successful
// authentication the associated headers will be copied into the request context.
func RequireIAPAuthStreamInterceptor(audience string) grpc.StreamServerInterceptor {
return grpcauth.StreamServerInterceptor(iapAuthFunc(audience, idtoken.Validate))
}

// validator is a function type for the validator function. The primary purpose is to be able to
// replace the validator function.
type validator func(ctx context.Context, token, audiance string) (*idtoken.Payload, error)

// IAPAudienceGCE returns the jwt audience for GCE and GKE services.
func IAPAudienceGCE(projectNumber int64, serviceID string) string {
return fmt.Sprintf("/projects/%d/global/backendServices/%s", projectNumber, serviceID)
}

// IAPAudienceAppEngine returns the JWT audience for App Engine services.
func IAPAudienceAppEngine(projectNumber int64, projectID string) string {
return fmt.Sprintf("/projects/%d/apps/%s", projectNumber, projectID)
}
98 changes: 98 additions & 0 deletions internal/access/access_test.go
@@ -0,0 +1,98 @@
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package access

import (
"context"
"fmt"
"testing"

"github.com/google/go-cmp/cmp"
"google.golang.org/api/idtoken"
"google.golang.org/grpc/metadata"
)

func TestIAPFromContextError(t *testing.T) {
ctx := context.WithValue(context.Background(), contextIAP, "dance party")
if got, err := IAPFromContext(ctx); got != nil || err == nil {
t.Errorf("IAPFromContext(ctx) = %v, %s; want error", got, err)
}
}

func TestIAPAuthFunc(t *testing.T) {
want := &IAPFields{
Email: "charlie@brown.com",
ID: "chaz.service.moo",
}
wantJWTToken := "eyJhb.eyJzdDIyfQ.Bh17Fl2gFjyLh6mo1GjqSPnGUg8MRLAE1Vdo3Z3gvdI"
wantAudience := "foo/bar/zar"
ctx := metadata.NewIncomingContext(context.Background(), metadata.New(map[string]string{
iapHeaderJWT: wantJWTToken,
iapHeaderEmail: want.Email,
iapHeaderID: want.ID,
}))
testValidator := func(ctx context.Context, token, audience string) (*idtoken.Payload, error) {
if token != wantJWTToken || audience != wantAudience {
return nil, fmt.Errorf("testValidator(%q, %q); want %q, %q", token, audience, wantJWTToken, wantAudience)
}
return &idtoken.Payload{}, nil
}
authFunc := iapAuthFunc(wantAudience, testValidator)
gotCtx, err := authFunc(ctx)
if err != nil {
t.Fatalf("authFunc(ctx) = %+v, %s; want ctx, no error", gotCtx, err)
}
got, err := IAPFromContext(gotCtx)
if err != nil {
t.Fatalf("IAPFromContext(ctx) = %+v, %s; want no error", got, err)
}
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("ctx.Value(%v) mismatch (-got, +want):\n%s", contextIAP, diff)
}
}

func TestContextWithIAPMDError(t *testing.T) {
testCases := []struct {
desc string
md metadata.MD
}{
{
desc: "missing email header",
md: metadata.New(map[string]string{
iapHeaderJWT: "jwt",
iapHeaderID: "id",
}),
},
{
desc: "missing id header",
md: metadata.New(map[string]string{
iapHeaderJWT: "jwt",
iapHeaderEmail: "email",
}),
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
ctx, err := contextWithIAPMD(context.Background(), tc.md)
if err == nil {
t.Errorf("contextWithIAPMD(ctx, %v) = %+v, %s; want ctx, error", tc.md, ctx, err)
}
})
}
}

func TestIAPAudienceGCE(t *testing.T) {
want := "/projects/11/global/backendServices/bar"
if got := IAPAudienceGCE(11, "bar"); got != want {
t.Errorf("IAPAudienceGCE(11, bar) = %s; want %s", got, want)
}
}

func TestIAPAudience(t *testing.T) {
want := "/projects/11/apps/bar"
if got := IAPAudienceAppEngine(11, "bar"); got != want {
t.Errorf("IAPAudienceAppEngine(11, bar) = %s; want %s", got, want)
}
}
7 changes: 7 additions & 0 deletions internal/access/doc.go
@@ -0,0 +1,7 @@
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// package access provides primatives for implementing authentication and
// authorization.
package access

0 comments on commit a21315d

Please sign in to comment.