-
Notifications
You must be signed in to change notification settings - Fork 5
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
Support persistent session in IntrospectToken method of GRPC client, PLTFRM-72444 #33
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,12 +8,17 @@ package idptoken_test | |
|
||
import ( | ||
"context" | ||
"fmt" | ||
"strconv" | ||
gotesting "testing" | ||
"time" | ||
|
||
jwtgo "github.com/golang-jwt/jwt/v5" | ||
"github.com/google/uuid" | ||
"github.com/stretchr/testify/require" | ||
"google.golang.org/grpc/codes" | ||
"google.golang.org/grpc/credentials/insecure" | ||
"google.golang.org/grpc/metadata" | ||
|
||
"github.com/acronis/go-authkit/idptest" | ||
"github.com/acronis/go-authkit/idptoken" | ||
|
@@ -23,6 +28,213 @@ import ( | |
"github.com/acronis/go-authkit/jwt" | ||
) | ||
|
||
func TestGRPCClient_IntrospectToken(t *gotesting.T) { | ||
const validAccessToken = "access-token-with-introspection-permission" | ||
var validSessionID = testing.GenerateSessionID(validAccessToken) | ||
|
||
opaqueToken := "opaque-token-" + uuid.NewString() | ||
opaqueTokenScope := []jwt.AccessPolicy{{ | ||
TenantUUID: uuid.NewString(), | ||
ResourceNamespace: "account-server", | ||
Role: "admin", | ||
ResourcePath: "resource-" + uuid.NewString(), | ||
}} | ||
opaqueTokenRegClaims := jwtgo.RegisteredClaims{ | ||
ExpiresAt: jwtgo.NewNumericDate(time.Now().Add(time.Hour)), | ||
} | ||
|
||
jwtScopeToGRPC := func(jwtScope []jwt.AccessPolicy) []*pb.AccessTokenScope { | ||
grpcScope := make([]*pb.AccessTokenScope, len(jwtScope)) | ||
for i, scope := range jwtScope { | ||
grpcScope[i] = &pb.AccessTokenScope{ | ||
TenantUuid: scope.TenantUUID, | ||
ResourceNamespace: scope.ResourceNamespace, | ||
RoleName: scope.Role, | ||
ResourcePath: scope.ResourcePath, | ||
} | ||
} | ||
return grpcScope | ||
} | ||
|
||
grpcServerTokenIntrospector := testing.NewGRPCServerTokenIntrospectorMock() | ||
grpcServerTokenIntrospector.SetAccessTokenForIntrospection(validAccessToken) | ||
grpcServerTokenIntrospector.SetResultForToken(opaqueToken, &pb.IntrospectTokenResponse{ | ||
Active: true, | ||
TokenType: idputil.TokenTypeBearer, | ||
Aud: opaqueTokenRegClaims.Audience, | ||
Exp: opaqueTokenRegClaims.ExpiresAt.Unix(), | ||
Scope: jwtScopeToGRPC(opaqueTokenScope), | ||
}) | ||
|
||
grpcIDPSrv := idptest.NewGRPCServer(idptest.WithGRPCTokenIntrospector(grpcServerTokenIntrospector)) | ||
require.NoError(t, grpcIDPSrv.StartAndWaitForReady(time.Second)) | ||
defer func() { grpcIDPSrv.GracefulStop() }() | ||
|
||
type introspectionRequest struct { | ||
requestNumber int | ||
tokenToIntrospect string | ||
accessToken string | ||
serverRespCode codes.Code // ask server for a specific resp code despite actual auth info | ||
expectedResult idptoken.IntrospectionResult | ||
serverLastAuthorizationMetaExpected string | ||
serverLastSessionMetaExpected string | ||
checkError func(t *gotesting.T, err error) | ||
} | ||
|
||
tCases := []struct { | ||
name string | ||
requestSeries []introspectionRequest | ||
}{ | ||
{ | ||
name: "Send valid access token to server on 1st introspection request", | ||
requestSeries: []introspectionRequest{ | ||
{ | ||
requestNumber: 1, | ||
tokenToIntrospect: opaqueToken, | ||
accessToken: validAccessToken, | ||
expectedResult: &idptoken.DefaultIntrospectionResult{ | ||
Active: true, | ||
TokenType: idputil.TokenTypeBearer, | ||
DefaultClaims: jwt.DefaultClaims{ | ||
RegisteredClaims: jwtgo.RegisteredClaims{ExpiresAt: opaqueTokenRegClaims.ExpiresAt}, | ||
Scope: opaqueTokenScope, | ||
}, | ||
}, | ||
serverLastAuthorizationMetaExpected: "Bearer " + validAccessToken, | ||
serverLastSessionMetaExpected: "", | ||
}, | ||
}, | ||
}, | ||
{ | ||
name: "Send valid session id to server upon 2nd introspection request", | ||
requestSeries: []introspectionRequest{ | ||
{ | ||
requestNumber: 1, | ||
tokenToIntrospect: opaqueToken, | ||
accessToken: validAccessToken, | ||
expectedResult: &idptoken.DefaultIntrospectionResult{ | ||
Active: true, | ||
TokenType: idputil.TokenTypeBearer, | ||
DefaultClaims: jwt.DefaultClaims{ | ||
RegisteredClaims: jwtgo.RegisteredClaims{ExpiresAt: opaqueTokenRegClaims.ExpiresAt}, | ||
Scope: opaqueTokenScope, | ||
}, | ||
}, | ||
serverLastAuthorizationMetaExpected: "Bearer " + validAccessToken, | ||
serverLastSessionMetaExpected: "", | ||
}, | ||
{ | ||
requestNumber: 2, | ||
tokenToIntrospect: opaqueToken, | ||
accessToken: validAccessToken, | ||
expectedResult: &idptoken.DefaultIntrospectionResult{ | ||
Active: true, | ||
TokenType: idputil.TokenTypeBearer, | ||
DefaultClaims: jwt.DefaultClaims{ | ||
RegisteredClaims: jwtgo.RegisteredClaims{ExpiresAt: opaqueTokenRegClaims.ExpiresAt}, | ||
Scope: opaqueTokenScope, | ||
}, | ||
}, | ||
serverLastAuthorizationMetaExpected: "", | ||
serverLastSessionMetaExpected: validSessionID, | ||
}, | ||
}, | ||
}, | ||
{ | ||
name: "Drop session id when 3rd of 4 introspection requests receives 401 from server", | ||
requestSeries: []introspectionRequest{ | ||
{ | ||
requestNumber: 1, | ||
tokenToIntrospect: opaqueToken, | ||
accessToken: validAccessToken, | ||
expectedResult: &idptoken.DefaultIntrospectionResult{ | ||
Active: true, | ||
TokenType: idputil.TokenTypeBearer, | ||
DefaultClaims: jwt.DefaultClaims{ | ||
RegisteredClaims: jwtgo.RegisteredClaims{ExpiresAt: opaqueTokenRegClaims.ExpiresAt}, | ||
Scope: opaqueTokenScope, | ||
}, | ||
}, | ||
serverLastAuthorizationMetaExpected: "Bearer " + validAccessToken, | ||
serverLastSessionMetaExpected: "", | ||
}, | ||
{ | ||
requestNumber: 2, | ||
tokenToIntrospect: opaqueToken, | ||
accessToken: validAccessToken, | ||
expectedResult: &idptoken.DefaultIntrospectionResult{ | ||
Active: true, | ||
TokenType: idputil.TokenTypeBearer, | ||
DefaultClaims: jwt.DefaultClaims{ | ||
RegisteredClaims: jwtgo.RegisteredClaims{ExpiresAt: opaqueTokenRegClaims.ExpiresAt}, | ||
Scope: opaqueTokenScope, | ||
}, | ||
}, | ||
serverLastAuthorizationMetaExpected: "", | ||
serverLastSessionMetaExpected: validSessionID, | ||
}, | ||
{ | ||
requestNumber: 3, | ||
tokenToIntrospect: opaqueToken, | ||
accessToken: validAccessToken, | ||
serverRespCode: codes.Unauthenticated, // ask server for 401 to invalidate session id in client | ||
checkError: func(t *gotesting.T, err error) { | ||
require.ErrorIs(t, err, idptoken.ErrUnauthenticated) | ||
}, | ||
serverLastAuthorizationMetaExpected: "", | ||
serverLastSessionMetaExpected: validSessionID, | ||
}, | ||
{ | ||
requestNumber: 4, | ||
tokenToIntrospect: opaqueToken, | ||
accessToken: validAccessToken, | ||
expectedResult: &idptoken.DefaultIntrospectionResult{ | ||
Active: true, | ||
TokenType: idputil.TokenTypeBearer, | ||
DefaultClaims: jwt.DefaultClaims{ | ||
RegisteredClaims: jwtgo.RegisteredClaims{ExpiresAt: opaqueTokenRegClaims.ExpiresAt}, | ||
Scope: opaqueTokenScope, | ||
}, | ||
}, | ||
serverLastAuthorizationMetaExpected: "Bearer " + validAccessToken, | ||
serverLastSessionMetaExpected: "", // prev 401 drops session id in client so access token ust be used | ||
}, | ||
}, | ||
}, | ||
} | ||
|
||
for _, tc := range tCases { | ||
t.Run(tc.name, func(t *gotesting.T) { | ||
grpcClient, err := idptoken.NewGRPCClient(grpcIDPSrv.Addr(), insecure.NewCredentials()) | ||
require.NoError(t, err) | ||
defer func() { require.NoError(t, grpcClient.Close()) }() | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please call |
||
for _, req := range tc.requestSeries { | ||
ctx := context.Background() | ||
if req.serverRespCode != 0 { | ||
ctx = metadata.AppendToOutgoingContext( | ||
ctx, testing.TestMetaRequestedRespCode, strconv.FormatUint(uint64(req.serverRespCode), 10), | ||
) | ||
} | ||
result, introspectErr := grpcClient.IntrospectToken(ctx, req.tokenToIntrospect, nil, req.accessToken) | ||
if req.checkError != nil { | ||
req.checkError(t, introspectErr) | ||
} else { | ||
require.Equal(t, req.expectedResult, result) | ||
} | ||
require.Equal(t, req.serverLastAuthorizationMetaExpected, grpcServerTokenIntrospector.LastAuthorizationMeta, | ||
fmt.Sprintf("unexpected server auth meta with introspection request number %d", req.requestNumber)) | ||
require.Equal(t, req.serverLastSessionMetaExpected, grpcServerTokenIntrospector.LastSessionMeta, | ||
fmt.Sprintf("unexpected server session meta with introspection request number %d", req.requestNumber)) | ||
if req.expectedResult != nil { | ||
require.Equal(t, req.tokenToIntrospect, grpcServerTokenIntrospector.LastRequest.Token, | ||
fmt.Sprintf("unexpected introspection result with introspection request number %d", req.requestNumber)) | ||
} | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestGRPCClient_ExchangeToken(t *gotesting.T) { | ||
tokenExpiresIn := time.Hour | ||
tokenExpiresAt := time.Now().Add(time.Hour) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -43,10 +43,6 @@ func TestIntrospector_IntrospectToken(t *gotesting.T) { | |
require.NoError(t, grpcIDPSrv.StartAndWaitForReady(time.Second)) | ||
defer func() { grpcIDPSrv.GracefulStop() }() | ||
|
||
grpcClient, err := idptoken.NewGRPCClient(grpcIDPSrv.Addr(), insecure.NewCredentials()) | ||
require.NoError(t, err) | ||
defer func() { require.NoError(t, grpcClient.Close()) }() | ||
|
||
jwtScopeToGRPC := func(jwtScope []jwt.AccessPolicy) []*pb.AccessTokenScope { | ||
grpcScope := make([]*pb.AccessTokenScope, len(jwtScope)) | ||
for i, scope := range jwtScope { | ||
|
@@ -159,6 +155,7 @@ func TestIntrospector_IntrospectToken(t *gotesting.T) { | |
|
||
tests := []struct { | ||
name string | ||
useGRPCClient bool | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The gRPC client can be built unconditionally for all tests. Could you remove |
||
introspectorOpts idptoken.IntrospectorOpts | ||
tokenToIntrospect string | ||
accessToken string | ||
|
@@ -390,9 +387,9 @@ func TestIntrospector_IntrospectToken(t *gotesting.T) { | |
expectedHTTPSrvCalled: true, | ||
}, | ||
{ | ||
name: "ok, grpc introspection endpoint, opaque token", | ||
name: "ok, grpc introspection endpoint, opaque token", | ||
useGRPCClient: true, | ||
introspectorOpts: idptoken.IntrospectorOpts{ | ||
GRPCClient: grpcClient, | ||
ScopeFilter: jwt.ScopeFilter{ | ||
{ResourceNamespace: "account-server"}, | ||
{ResourceNamespace: "tenant-manager"}, | ||
|
@@ -414,10 +411,9 @@ func TestIntrospector_IntrospectToken(t *gotesting.T) { | |
}, | ||
}, | ||
{ | ||
name: "error, grpc introspection endpoint, opaque token, unauthenticated", | ||
introspectorOpts: idptoken.IntrospectorOpts{ | ||
GRPCClient: grpcClient, | ||
}, | ||
name: "error, grpc introspection endpoint, opaque token, unauthenticated", | ||
useGRPCClient: true, | ||
introspectorOpts: idptoken.IntrospectorOpts{}, | ||
tokenToIntrospect: opaqueToken, | ||
accessToken: "invalid-access-token", | ||
checkError: func(t *gotesting.T, err error) { | ||
|
@@ -426,9 +422,9 @@ func TestIntrospector_IntrospectToken(t *gotesting.T) { | |
expectedGRPCSrvCalled: true, | ||
}, | ||
{ | ||
name: "error, grpc introspection endpoint, jwt token, invalid audience", | ||
name: "error, grpc introspection endpoint, jwt token, invalid audience", | ||
useGRPCClient: true, | ||
introspectorOpts: idptoken.IntrospectorOpts{ | ||
GRPCClient: grpcClient, | ||
RequireAudience: true, | ||
ExpectedAudience: []string{"https://rs.my-service.com"}, | ||
}, | ||
|
@@ -440,9 +436,9 @@ func TestIntrospector_IntrospectToken(t *gotesting.T) { | |
expectedGRPCSrvCalled: true, | ||
}, | ||
{ | ||
name: "error, grpc introspection endpoint, opaque token, audience is missing", | ||
name: "error, grpc introspection endpoint, opaque token, audience is missing", | ||
useGRPCClient: true, | ||
introspectorOpts: idptoken.IntrospectorOpts{ | ||
GRPCClient: grpcClient, | ||
RequireAudience: true, | ||
ExpectedAudience: []string{"https://rs.my-service.com"}, | ||
}, | ||
|
@@ -458,6 +454,15 @@ func TestIntrospector_IntrospectToken(t *gotesting.T) { | |
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *gotesting.T) { | ||
if tt.useGRPCClient { | ||
// gRPC client is created and used by condition to avoid preserving its state (sessionID) between tests | ||
grpcClient, err := idptoken.NewGRPCClient(grpcIDPSrv.Addr(), insecure.NewCredentials()) | ||
require.NoError(t, err) | ||
defer func() { require.NoError(t, grpcClient.Close()) }() | ||
|
||
tt.introspectorOpts.GRPCClient = grpcClient | ||
} | ||
|
||
if tt.accessToken == "" { | ||
tt.accessToken = validAccessToken | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's better to use the metadata.MD wrapper here when making multiple calls to AppendToOutgoingContext() or ValueFromIncomingContext(), as otherwise, redundant (de)serialization work occurs. Additionally, with AppendToOutgoingContext(), you will have +N redundant memory allocations, where N is the number of AppendToOutgoingContext() calls.