-
Notifications
You must be signed in to change notification settings - Fork 24
feat(sdk): Include auth token in grpc #367
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
Merged
Merged
Changes from 18 commits
Commits
Show all changes
104 commits
Select commit
Hold shift + click to select a range
5f4b064
feat: verify and validate access tokens on service calls
strantalis c952554
add autnInterceptor
strantalis 5358a10
save
strantalis 6a1c022
Merge branch 'main' into feat/authn-support
strantalis f6b84c1
save progress
strantalis 2d38e69
remove testIDP var
strantalis d7e4b82
cleanup
strantalis 7aa3273
comments
strantalis e99dcfc
comment
strantalis 68d96f9
unit tests for access token verification and validation
strantalis f361f20
Merge branch 'main' into feat/authn-support
strantalis 10be45c
updated configuration docs
strantalis 7bb68e3
registered authn check as handler in mux chain
strantalis 717485f
rename authN config field and remove left over log line
strantalis 43db635
Merge branch 'main' into feat/authn-support
strantalis 7d783cd
Merge branch 'main' into feat/authn-support
strantalis 42f7daf
move authn to internal
strantalis 72791c7
Merge branch 'main' into feat/authn-support
strantalis 3394384
fix authn test
strantalis 0c54866
only set issuer in platform welknown config
strantalis 99c3153
fix loading authn with handler
strantalis f2947b3
fix grpccurl step
strantalis 510562f
fix healthcheck grpccurl call
strantalis a0f6e75
didn't save
strantalis 58d209c
disable auth for service extension test
strantalis 0f8ce82
need to set mux to handler on server start
strantalis f29f471
Merge branch 'main' into feat/authn-support
strantalis 629ad2f
try nohub
strantalis 5091e2f
try just go start to see errors
strantalis 2c03635
pause on starting opentdf
strantalis b077792
disable auth in example config
strantalis 29abfbb
Merge branch 'main' into feat/authn-support
strantalis 1599c15
Update internal/auth/authn.go
strantalis d93e257
Merge branch 'main' into feat/authn-support
strantalis 1169de3
Merge branch 'feat/authn-support' of github.com:opentdf/opentdf-v2-po…
strantalis acd90fd
Update internal/server/server.go
strantalis b7b034d
Merge branch 'main' into feat/authn-support
strantalis eb3ee6f
take a function so that callers can use this the way that they want
mkleene fac35c5
rename
mkleene 83e2c42
add this
mkleene 12dc95b
Revert "fix(sdk): temporarily move unwrapper creation into options fu…
mkleene bdace36
this seems a little cleaner to me
mkleene 1d7d5cd
Merge remote-tracking branch 'origin/main' into change-unwrapper-crea…
mkleene 48042b9
add comment and better assertion
mkleene 169fdb7
Merge remote-tracking branch 'origin/change-unwrapper-creation' into …
mkleene c4dd18f
adding a token appears to work from the client side
mkleene cf2ce7b
Merge remote-tracking branch 'origin/main' into include-auth-token-in…
mkleene 24a6727
Merge branch 'main' into feat/authn-support
strantalis 0b8bf9d
add interceptor to add tokens to outgoing client requests
mkleene 7200b61
we don't need duplicates
mkleene 7558c97
move this
mkleene 3a83cef
lint
mkleene 0cb810d
Update token_adding_interceptor_test.go
mkleene 8fdc8f6
Merge branch 'main' into include-auth-token-in-grpc
mkleene 23eec93
code review points
mkleene 673a1a1
Merge remote-tracking branch 'origin/include-auth-token-in-grpc' into…
mkleene 78002c9
rename
mkleene 96961c4
add DPoP validation
mkleene 63edb54
Merge remote-tracking branch 'origin/main' into include-auth-token-in…
mkleene 161b356
Merge branch 'feat/authn-support' into include-auth-token-in-grpc
mkleene 4402da2
Merge remote-tracking branch 'origin/main' into include-auth-token-in…
mkleene f4ee37d
hide the `AsymDecryption` so that we can move the interface
mkleene d6bc47f
move the interface
mkleene 4f7a68e
add some testing
mkleene 5af9bdf
Merge remote-tracking branch 'origin/main' into include-auth-token-in…
mkleene 479a477
not needed
mkleene 020124c
do not want to change this
mkleene 5149e25
not needed
mkleene a962511
add new files
mkleene c3af3e9
Delete sdk/token_adding_interceptor.go
mkleene 74127ac
Delete sdk/token_adding_interceptor_test.go
mkleene 0223827
name change
mkleene 706106b
go mod tidy
mkleene 8b86be9
wire this into the sdk
mkleene 3a71108
oops
mkleene 611cb80
missed a rename
mkleene 369ae7d
see if this works
mkleene 253c285
lint
mkleene c740e4a
lint
mkleene f2b50d8
more lint
mkleene d2b6706
more lint
mkleene 5c655f4
Merge branch 'main' into include-auth-token-in-grpc
mkleene 8e67b1a
Merge branch 'main' into include-auth-token-in-grpc
mkleene d8b3b9f
if they give us a token with a `cnf` reject it
mkleene c6588df
Merge remote-tracking branch 'origin/include-auth-token-in-grpc' into…
mkleene 52dcf76
no need to add the DPoP token here
mkleene 72427db
Merge branch 'main' into include-auth-token-in-grpc
mkleene 3706ee6
move stuff and change the tests
mkleene 6736b4a
use better methods
mkleene bbbc34d
add e2e test
mkleene 6234d40
Merge remote-tracking branch 'origin/main' into include-auth-token-in…
mkleene 72f4042
lint
mkleene 75f7263
test
mkleene a7b06e5
see if that shows the line numbers
mkleene e3fe2ff
Revert "see if that shows the line numbers"
mkleene 6dbe49f
just do not cast
mkleene 573e4cf
Merge branch 'main' into include-auth-token-in-grpc
mkleene 86bccba
refactor: Remove deps on backend-go
dmihalcik-virtru b7f1087
chore: DPoP capitalization nits
dmihalcik-virtru cf6425c
refactor: lets a function be a method
dmihalcik-virtru 1a4e78d
refactor: reduce a warn logline to info
dmihalcik-virtru 2dba7b8
chore: lint cleanup
dmihalcik-virtru 4b7d915
chore: lint fixes
dmihalcik-virtru 17e7046
ci: Fix invalid test
dmihalcik-virtru File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| package sdk | ||
|
|
||
| import ( | ||
| "context" | ||
| "crypto/rand" | ||
| "crypto/sha256" | ||
| "encoding/base64" | ||
| "fmt" | ||
| "log/slog" | ||
| "time" | ||
|
|
||
| "github.com/lestrrat-go/jwx/v2/jwk" | ||
| "github.com/lestrrat-go/jwx/v2/jws" | ||
| "github.com/lestrrat-go/jwx/v2/jwt" | ||
| "google.golang.org/grpc" | ||
| "google.golang.org/grpc/metadata" | ||
| ) | ||
|
|
||
| const ( | ||
| JTILength = 14 | ||
| JWTExpirationMinutes = 10 | ||
| ) | ||
|
|
||
| func newOutgoingInterceptor(t AccessTokenSource) tokenAddingInterceptor { | ||
| return tokenAddingInterceptor{tokenSource: t} | ||
| } | ||
|
|
||
| type tokenAddingInterceptor struct { | ||
| tokenSource AccessTokenSource | ||
| } | ||
|
|
||
| func (i tokenAddingInterceptor) addCredentials(ctx context.Context, | ||
| method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { | ||
| newMetadata := make([]string, 0) | ||
| accessToken, err := i.tokenSource.AccessToken() | ||
| if err == nil { | ||
| newMetadata = append(newMetadata, "Authorization", fmt.Sprintf("DPoP %s", accessToken)) | ||
| } else { | ||
| slog.Error("error getting access token: %w. Request will be unauthenticated", err) | ||
mkleene marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| dpopTok, err := i.getDPOPToken(method, string(accessToken)) | ||
mkleene marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if err == nil { | ||
| newMetadata = append(newMetadata, "DPoP", dpopTok) | ||
| } else { | ||
| slog.Error("error adding dpop token to outgoing request. Request will not have DPoP token", err) | ||
| } | ||
|
|
||
| newCtx := metadata.AppendToOutgoingContext(ctx, newMetadata...) | ||
|
|
||
| err = invoker(newCtx, method, req, reply, cc, opts...) | ||
|
|
||
| // this is the error from the RPC service. we can determine when the current token is no longer valid | ||
| // by inspecting this error | ||
| return err | ||
| } | ||
|
|
||
| func (i tokenAddingInterceptor) getDPOPToken(method, accessToken string) (string, error) { | ||
| tok, err := i.tokenSource.MakeToken(func(key jwk.Key) ([]byte, error) { | ||
| jtiBytes := make([]byte, JTILength) | ||
| _, err := rand.Read(jtiBytes) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("error creating jti for dpop jwt: %w", err) | ||
| } | ||
|
|
||
| publicKey, err := key.PublicKey() | ||
| if err != nil { | ||
| return nil, fmt.Errorf("error getting public key from DPOP key: %w", err) | ||
| } | ||
|
|
||
| headers := jws.NewHeaders() | ||
| err = headers.Set(jws.JWKKey, publicKey) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("error setting the key on the DPOP token: %w", err) | ||
| } | ||
| err = headers.Set(jws.TypeKey, "dpop+jwt") | ||
| if err != nil { | ||
| return nil, fmt.Errorf("error setting the type on the DPOP token: %w", err) | ||
| } | ||
| err = headers.Set(jws.AlgorithmKey, key.Algorithm()) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("error setting the algorithm on the DPOP token: %w", err) | ||
| } | ||
|
|
||
| h := sha256.New() | ||
| h.Write([]byte(accessToken)) | ||
| ath := h.Sum(nil) | ||
|
|
||
| dpopTok, err := jwt.NewBuilder(). | ||
| Claim("htu", method). | ||
| Claim("htm", "POST"). | ||
| Claim("ath", base64.StdEncoding.EncodeToString(ath)). | ||
| Claim("jti", base64.StdEncoding.EncodeToString(jtiBytes)). | ||
| IssuedAt(time.Now()). | ||
| Expiration(time.Now().Add(time.Minute * JWTExpirationMinutes)). | ||
| Build() | ||
|
|
||
| if err != nil { | ||
| return nil, fmt.Errorf("error creating dpop jwt: %w", err) | ||
| } | ||
|
|
||
| signedToken, err := jwt.Sign(dpopTok, jwt.WithKey(key.Algorithm(), key, jws.WithProtectedHeaders(headers))) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("error signing dpop jwt: %w", err) | ||
| } | ||
|
|
||
| return signedToken, nil | ||
| }) | ||
|
|
||
| if err != nil { | ||
| return "", fmt.Errorf("error creating DPOP token in interceptor: %w", err) | ||
| } | ||
|
|
||
| return string(tok), nil | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,187 @@ | ||
| package sdk | ||
|
|
||
| import ( | ||
| "context" | ||
| "crypto/sha256" | ||
| "encoding/base64" | ||
| "errors" | ||
| "net" | ||
| "slices" | ||
| "testing" | ||
|
|
||
| gocrypto "crypto" | ||
|
|
||
| "github.com/lestrrat-go/jwx/v2/jwa" | ||
| "github.com/lestrrat-go/jwx/v2/jwk" | ||
| "github.com/lestrrat-go/jwx/v2/jws" | ||
| "github.com/lestrrat-go/jwx/v2/jwt" | ||
| kas "github.com/opentdf/backend-go/pkg/access" | ||
| "github.com/opentdf/platform/sdk/internal/crypto" | ||
|
|
||
| "google.golang.org/grpc" | ||
| "google.golang.org/grpc/codes" | ||
| "google.golang.org/grpc/credentials/insecure" | ||
| "google.golang.org/grpc/metadata" | ||
| "google.golang.org/grpc/status" | ||
| "google.golang.org/grpc/test/bufconn" | ||
| "google.golang.org/protobuf/types/known/wrapperspb" | ||
| ) | ||
|
|
||
| func TestAddingTokensToOutgoingRequest(t *testing.T) { | ||
| _, key, _, _ := getNewDPoPKey() | ||
| ts := FakeTokenSource{ | ||
| key: key, | ||
| accessToken: "thisisafakeaccesstoken", | ||
| } | ||
| server := FakeAccessServiceServer{} | ||
| oo := newOutgoingInterceptor(&ts) | ||
|
|
||
| client, stop := runServer(context.Background(), &server, oo) | ||
| defer stop() | ||
|
|
||
| _, err := client.Info(context.Background(), &kas.InfoRequest{}) | ||
| if err != nil { | ||
| t.Fatalf("error making call: %v", err) | ||
| } | ||
|
|
||
| if len(server.accessToken) != 1 || server.accessToken[0] != "DPoP thisisafakeaccesstoken" { | ||
| t.Fatalf("got incorrect access token: %v", server.accessToken) | ||
| } | ||
|
|
||
| if len(server.dpopToken) != 1 { | ||
| t.Fatalf("Got incorrect dpop token headers: %v", server.dpopToken) | ||
| } | ||
|
|
||
| dpopToken := server.dpopToken[0] | ||
|
|
||
| alg, ok := key.Algorithm().(jwa.SignatureAlgorithm) | ||
| if !ok { | ||
| t.Fatalf("got a bad signing algorithm") | ||
| } | ||
|
|
||
| _, err = jws.Verify([]byte(dpopToken), jws.WithKey(alg, key)) | ||
| if err != nil { | ||
| t.Fatalf("error verifying signature: %v", err) | ||
| } | ||
|
|
||
| parsedSignature, _ := jws.Parse([]byte(dpopToken)) | ||
|
|
||
| if len(parsedSignature.Signatures()) == 0 { | ||
| t.Fatalf("didn't get signature from jwt") | ||
| } | ||
|
|
||
| sig := parsedSignature.Signatures()[0] | ||
| tokenKey, ok := sig.ProtectedHeaders().Get("jwk") | ||
| if !ok { | ||
| t.Fatalf("didn't get error getting key from token") | ||
| } | ||
|
|
||
| tp, _ := tokenKey.(jwk.Key).Thumbprint(gocrypto.SHA256) | ||
| ktp, _ := key.Thumbprint(gocrypto.SHA256) | ||
| if !slices.Equal(tp, ktp) { | ||
| t.Fatalf("got the wrong key from the token") | ||
| } | ||
|
|
||
| parsedToken, _ := jwt.Parse([]byte(dpopToken), jwt.WithVerify(false)) | ||
|
|
||
| if method, _ := parsedToken.Get("htm"); method.(string) != "POST" { | ||
| t.Fatalf("we got a bad method: %v", method) | ||
| } | ||
|
|
||
| if path, _ := parsedToken.Get("htu"); path.(string) != "/access.AccessService/Info" { | ||
| t.Fatalf("we got a bad method: %v", path) | ||
| } | ||
|
|
||
| h := sha256.New() | ||
| h.Write([]byte("thisisafakeaccesstoken")) | ||
| expectedHash := base64.URLEncoding.EncodeToString(h.Sum(nil)) | ||
|
|
||
| if ath, _ := parsedToken.Get("ath"); ath.(string) != expectedHash { | ||
| t.Fatalf("got invalid ath claim in token: %v", ath) | ||
| } | ||
| } | ||
|
|
||
| func Test_InvalidCredentials_StillSendMessage(t *testing.T) { | ||
| ts := FakeTokenSource{key: nil} | ||
| server := FakeAccessServiceServer{} | ||
| oo := newOutgoingInterceptor(&ts) | ||
|
|
||
| client, stop := runServer(context.Background(), &server, oo) | ||
| defer stop() | ||
|
|
||
| _, err := client.Info(context.Background(), &kas.InfoRequest{}) | ||
|
|
||
| if err != nil { | ||
| t.Fatalf("got an error when sending the message") | ||
| } | ||
| } | ||
|
|
||
| type FakeAccessServiceServer struct { | ||
| accessToken []string | ||
| dpopToken []string | ||
| kas.UnimplementedAccessServiceServer | ||
| } | ||
|
|
||
| func (f *FakeAccessServiceServer) Info(ctx context.Context, _ *kas.InfoRequest) (*kas.InfoResponse, error) { | ||
| if md, ok := metadata.FromIncomingContext(ctx); ok { | ||
| f.accessToken = md.Get("authorization") | ||
| f.dpopToken = md.Get("dpop") | ||
| } | ||
|
|
||
| return &kas.InfoResponse{}, nil | ||
| } | ||
| func (f *FakeAccessServiceServer) PublicKey(context.Context, *kas.PublicKeyRequest) (*kas.PublicKeyResponse, error) { | ||
| return &kas.PublicKeyResponse{}, status.Error(codes.Unauthenticated, "no public key for you") | ||
| } | ||
| func (f *FakeAccessServiceServer) LegacyPublicKey(context.Context, *kas.LegacyPublicKeyRequest) (*wrapperspb.StringValue, error) { | ||
| return &wrapperspb.StringValue{}, nil | ||
| } | ||
| func (f *FakeAccessServiceServer) Rewrap(context.Context, *kas.RewrapRequest) (*kas.RewrapResponse, error) { | ||
| return &kas.RewrapResponse{}, nil | ||
| } | ||
|
|
||
| type FakeTokenSource struct { | ||
| key jwk.Key | ||
| accessToken string | ||
| } | ||
|
|
||
| func (fts *FakeTokenSource) AccessToken() (AccessToken, error) { | ||
| return AccessToken(fts.accessToken), nil | ||
| } | ||
| func (*FakeTokenSource) AsymDecryption() crypto.AsymDecryption { | ||
| return crypto.AsymDecryption{} | ||
| } | ||
| func (fts *FakeTokenSource) MakeToken(f func(jwk.Key) ([]byte, error)) ([]byte, error) { | ||
| if fts.key == nil { | ||
| return nil, errors.New("no such key") | ||
| } | ||
| return f(fts.key) | ||
| } | ||
| func (*FakeTokenSource) DPOPPublicKeyPEM() string { | ||
| return "" | ||
| } | ||
| func (*FakeTokenSource) RefreshAccessToken() error { | ||
| return nil | ||
| } | ||
|
|
||
| func runServer(ctx context.Context, //nolint:ireturn // this is pretty concrete | ||
| f *FakeAccessServiceServer, oo tokenAddingInterceptor) (kas.AccessServiceClient, func()) { | ||
| buffer := 1024 * 1024 | ||
| listener := bufconn.Listen(buffer) | ||
|
|
||
| s := grpc.NewServer() | ||
| kas.RegisterAccessServiceServer(s, f) | ||
| go func() { | ||
| if err := s.Serve(listener); err != nil { | ||
| panic(err) | ||
| } | ||
| }() | ||
|
|
||
| conn, _ := grpc.DialContext(ctx, "", grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { | ||
| return listener.Dial() | ||
| }), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock(), grpc.WithUnaryInterceptor(oo.addCredentials)) | ||
|
|
||
| client := kas.NewAccessServiceClient(conn) | ||
|
|
||
| return client, s.Stop | ||
| } |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.