[management] Enforce reverse proxy group access before minting and when honouring a session cookie (#7240)

This commit is contained in:
Viktor Liu
2026-09-04 07:15:20 +02:00
committed by GitHub
parent 798e4a0546
commit c2b5d211d9
13 changed files with 815 additions and 454 deletions
+16 -2
View File
@@ -1651,6 +1651,10 @@ var (
// ErrUserBlocked reports a blocked user, who may not hold a proxy session.
ErrUserBlocked = errors.New("user blocked")
// ErrUserNotInGroup reports a user outside the service's distribution
// groups, who may not hold a proxy session for it.
ErrUserNotInGroup = errors.New("user not in allowed groups")
errUserUnresolved = errors.New("user could not be resolved")
)
@@ -1689,8 +1693,10 @@ func sameAccount(userAccountID, serviceAccountID string) bool {
// GenerateSessionToken creates a signed session JWT for the given domain and
// user. The user's group memberships are embedded in the token so policy-aware
// middlewares on the proxy can authorise without an extra management round-trip.
// A user the store cannot resolve, or whose account is pending approval or
// blocked, gets no token at all, so the browser never receives a session cookie.
// A user the store cannot resolve, whose account is pending approval or blocked,
// or who is outside the service's distribution groups, gets no token at all: the
// token is a bearer credential for the service, so authorisation has to run
// before it is signed rather than only when the proxy presents it back.
func (s *ProxyServiceServer) GenerateSessionToken(ctx context.Context, domain, userID string, method proxyauth.Method) (string, error) {
service, err := s.getServiceByDomain(ctx, domain)
if err != nil {
@@ -1726,6 +1732,14 @@ func (s *ProxyServiceServer) GenerateSessionToken(ctx context.Context, domain, u
return "", fmt.Errorf("session token for user %s: %w", userID, err)
}
if err := s.checkGroupAccess(service, user); err != nil {
log.WithContext(ctx).WithFields(log.Fields{
"domain": domain,
"user_id": userID,
}).Debug("GenerateSessionToken: user not in the service's distribution groups")
return "", fmt.Errorf("session token for user %s: %w", userID, ErrUserNotInGroup)
}
groupIDs, groupNames := pairGroupIDsAndNames(userGroups)
token, err := sessionkey.SignToken(
@@ -431,6 +431,57 @@ func TestValidateSession_MissingToken(t *testing.T) {
assert.Contains(t, resp.DeniedReason, "missing")
}
// TestGenerateSessionToken_UserNotInAllowedGroupGetsNoToken is the regression
// guard for the group-authorisation bypass: the callback used to hand a signed
// token to a user the service denies, and the proxy honoured that token as soon
// as the user moved it into the nb_session cookie themselves. Authorisation has
// to run before the token is signed.
func TestGenerateSessionToken_UserNotInAllowedGroupGetsNoToken(t *testing.T) {
setup := setupValidateSessionTest(t)
defer setup.cleanup()
token, err := setup.proxyService.GenerateSessionToken(context.Background(), "restricted-proxy.example.com", "nonGroupUserId", auth.MethodOIDC)
require.Error(t, err, "a user outside the distribution groups must not receive a token")
assert.ErrorIs(t, err, ErrUserNotInGroup, "the callback maps this sentinel onto the access denied page")
assert.Empty(t, token, "no token may reach the browser")
}
func TestGenerateSessionToken_UserInAllowedGroupGetsTokenWithGroups(t *testing.T) {
setup := setupValidateSessionTest(t)
defer setup.cleanup()
ctx := context.Background()
svc, err := setup.store.GetServiceByID(ctx, store.LockingStrengthNone, "testAccountId", "restrictedProxyId")
require.NoError(t, err)
token, err := setup.proxyService.GenerateSessionToken(ctx, "restricted-proxy.example.com", "allowedUserId", auth.MethodOIDC)
require.NoError(t, err)
require.NotEmpty(t, token)
pubKey, err := base64.StdEncoding.DecodeString(svc.SessionPublicKey)
require.NoError(t, err)
userID, _, method, groups, _, err := auth.ValidateSessionJWT(token, "restricted-proxy.example.com", pubKey)
require.NoError(t, err)
assert.Equal(t, "allowedUserId", userID)
assert.Equal(t, auth.MethodOIDC.String(), method)
assert.Equal(t, []string{"allowedGroupId"}, groups, "the proxy gates the cookie on this claim, so it must carry the matched group")
}
// TestGenerateSessionToken_UnrestrictedServiceAllowsAnyAccountUser keeps the new
// gate scoped: a service without distribution groups is open to every user of
// its account, as before.
func TestGenerateSessionToken_UnrestrictedServiceAllowsAnyAccountUser(t *testing.T) {
setup := setupValidateSessionTest(t)
defer setup.cleanup()
token, err := setup.proxyService.GenerateSessionToken(context.Background(), "test-proxy.example.com", "nonGroupUserId", auth.MethodOIDC)
require.NoError(t, err, "an unrestricted service must keep working for any user of the account")
assert.NotEmpty(t, token)
}
type testValidateSessionServiceManager struct {
store store.Store
}