mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-29 19:11:28 +02:00
A user in the Pending Approval state could complete SSO and reach any SSO-protected reverse proxy service distributed to a group they belong to, including the All Users group. The reverse proxy authorization path checked the session token signature, that the user exists, that the user's account matches the service's account, and group membership — never the user's account status. The REST API (`permissions/manager.go`) and peer registration both gate on that state, but the proxy gRPC service does not go through the permissions manager, so neither gate applied. A pending user is persisted as blocked and pending approval, so blocked users reached those services the same way. `ValidateSession` now denies on account status, reporting `pending_approval` or `user_blocked` so the proxy access log and the denied page carry the cause rather than a generic refusal. `GenerateSessionToken` refuses to mint a token for such a user at all, so the browser never receives a session cookie and the OIDC callback can tell the user why instead of showing "Service configuration error". `ValidateUserGroupAccess` and `ValidateTunnelPeer` close the same gap; for the tunnel path this covers a user blocked after their peer was registered, since peer group membership alone kept mesh-origin access open. A single helper produces both the denied reason for the RPC responses and the sentinel error for the error-returning callers, so the four entry points cannot drift apart. A user the store cannot resolve is denied rather than passed through. One thing deliberately left out: session cookies are validated locally by the proxy against the service public key with no management round-trip, so a cookie issued before a user is blocked stays valid until it expires (24h by default). That is a revocation-propagation problem rather than this authorization gap, and every option for it (per-request validation with a cache, short-lived tokens with refresh, push-based revocation) changes the proxy hot path or the proxy/management protocol. Worth its own ticket.
679 lines
19 KiB
Go
679 lines
19 KiB
Go
//go:build integration
|
|
|
|
package proxy
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/gorilla/mux"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/accesslogs"
|
|
nbproxy "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy"
|
|
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
|
|
nbgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc"
|
|
nbcache "github.com/netbirdio/netbird/management/server/cache"
|
|
"github.com/netbirdio/netbird/management/server/store"
|
|
"github.com/netbirdio/netbird/management/server/types"
|
|
"github.com/netbirdio/netbird/management/server/users"
|
|
"github.com/netbirdio/netbird/shared/management/proto"
|
|
)
|
|
|
|
// fakeOIDCServer creates a minimal OIDC provider for testing.
|
|
type fakeOIDCServer struct {
|
|
server *httptest.Server
|
|
issuer string
|
|
signingKey ed25519.PrivateKey
|
|
publicKey ed25519.PublicKey
|
|
keyID string
|
|
tokenSubject string
|
|
tokenExpiry time.Duration
|
|
failExchange bool
|
|
}
|
|
|
|
func newFakeOIDCServer() *fakeOIDCServer {
|
|
pub, priv, _ := ed25519.GenerateKey(rand.Reader)
|
|
f := &fakeOIDCServer{
|
|
signingKey: priv,
|
|
publicKey: pub,
|
|
keyID: "test-key-1",
|
|
tokenExpiry: time.Hour,
|
|
}
|
|
f.server = httptest.NewServer(f)
|
|
f.issuer = f.server.URL
|
|
return f
|
|
}
|
|
|
|
func (f *fakeOIDCServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/.well-known/openid-configuration":
|
|
f.handleDiscovery(w, r)
|
|
case "/token":
|
|
f.handleToken(w, r)
|
|
case "/keys":
|
|
f.handleJWKS(w, r)
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}
|
|
|
|
func (f *fakeOIDCServer) handleDiscovery(w http.ResponseWriter, _ *http.Request) {
|
|
discovery := map[string]interface{}{
|
|
"issuer": f.issuer,
|
|
"authorization_endpoint": f.issuer + "/auth",
|
|
"token_endpoint": f.issuer + "/token",
|
|
"jwks_uri": f.issuer + "/keys",
|
|
"response_types_supported": []string{
|
|
"code",
|
|
"id_token",
|
|
"token id_token",
|
|
},
|
|
"subject_types_supported": []string{"public"},
|
|
"id_token_signing_alg_values_supported": []string{"EdDSA"},
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(discovery)
|
|
}
|
|
|
|
func (f *fakeOIDCServer) handleToken(w http.ResponseWriter, r *http.Request) {
|
|
if f.failExchange {
|
|
http.Error(w, "invalid_grant", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Error(w, "bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
idToken := f.createIDToken()
|
|
|
|
response := map[string]interface{}{
|
|
"access_token": "test-access-token",
|
|
"token_type": "Bearer",
|
|
"expires_in": 3600,
|
|
"id_token": idToken,
|
|
"refresh_token": "test-refresh-token",
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(response)
|
|
}
|
|
|
|
func (f *fakeOIDCServer) createIDToken() string {
|
|
now := time.Now()
|
|
claims := jwt.MapClaims{
|
|
"iss": f.issuer,
|
|
"sub": f.tokenSubject,
|
|
"aud": "test-client-id",
|
|
"exp": now.Add(f.tokenExpiry).Unix(),
|
|
"iat": now.Unix(),
|
|
"nbf": now.Unix(),
|
|
}
|
|
|
|
token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims)
|
|
token.Header["kid"] = f.keyID
|
|
signed, _ := token.SignedString(f.signingKey)
|
|
return signed
|
|
}
|
|
|
|
func (f *fakeOIDCServer) handleJWKS(w http.ResponseWriter, _ *http.Request) {
|
|
jwks := map[string]interface{}{
|
|
"keys": []map[string]interface{}{
|
|
{
|
|
"kty": "OKP",
|
|
"crv": "Ed25519",
|
|
"kid": f.keyID,
|
|
"x": base64.RawURLEncoding.EncodeToString(f.publicKey),
|
|
"use": "sig",
|
|
},
|
|
},
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(jwks)
|
|
}
|
|
|
|
func (f *fakeOIDCServer) Close() {
|
|
f.server.Close()
|
|
}
|
|
|
|
// testSetup contains all test dependencies.
|
|
type testSetup struct {
|
|
store store.Store
|
|
oidcServer *fakeOIDCServer
|
|
proxyService *nbgrpc.ProxyServiceServer
|
|
handler *AuthCallbackHandler
|
|
router *mux.Router
|
|
cleanup func()
|
|
}
|
|
|
|
// testAccessLogManager is a minimal mock for accesslogs.Manager.
|
|
type testAccessLogManager struct{}
|
|
|
|
func (m *testAccessLogManager) CleanupOldAccessLogs(ctx context.Context, retentionDays int) (int64, error) {
|
|
return 0, nil
|
|
}
|
|
|
|
func (m *testAccessLogManager) StartPeriodicCleanup(ctx context.Context, retentionDays, cleanupIntervalHours int) {
|
|
return
|
|
}
|
|
|
|
func (m *testAccessLogManager) StopPeriodicCleanup() {
|
|
return
|
|
}
|
|
|
|
func (m *testAccessLogManager) SaveAccessLog(_ context.Context, _ *accesslogs.AccessLogEntry) error {
|
|
return nil
|
|
}
|
|
|
|
func (m *testAccessLogManager) GetAllAccessLogs(_ context.Context, _, _ string, _ *accesslogs.AccessLogFilter) ([]*accesslogs.AccessLogEntry, int64, error) {
|
|
return nil, 0, nil
|
|
}
|
|
|
|
func setupAuthCallbackTest(t *testing.T) *testSetup {
|
|
t.Helper()
|
|
|
|
ctx := context.Background()
|
|
|
|
testStore, cleanup, err := store.NewTestStoreFromSQL(ctx, "", t.TempDir())
|
|
require.NoError(t, err)
|
|
|
|
createTestAccountsAndUsers(t, ctx, testStore)
|
|
createTestReverseProxies(t, ctx, testStore)
|
|
|
|
oidcServer := newFakeOIDCServer()
|
|
|
|
cacheStore, err := nbcache.NewStore(ctx, 30*time.Minute, 10*time.Minute, 100)
|
|
require.NoError(t, err)
|
|
|
|
tokenStore := nbgrpc.NewOneTimeTokenStore(ctx, cacheStore)
|
|
pkceStore := nbgrpc.NewPKCEVerifierStore(ctx, cacheStore)
|
|
|
|
usersManager := users.NewManager(testStore)
|
|
|
|
oidcConfig := nbgrpc.ProxyOIDCConfig{
|
|
Issuer: oidcServer.issuer,
|
|
ClientID: "test-client-id",
|
|
Scopes: []string{"openid", "profile", "email"},
|
|
CallbackURL: "https://management.example.com/reverse-proxy/callback",
|
|
HMACKey: []byte("test-hmac-key-for-state-signing"),
|
|
}
|
|
|
|
proxyService := nbgrpc.NewProxyServiceServer(
|
|
&testAccessLogManager{},
|
|
tokenStore,
|
|
pkceStore,
|
|
oidcConfig,
|
|
nil,
|
|
usersManager,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
)
|
|
|
|
proxyService.SetServiceManager(&testServiceManager{store: testStore})
|
|
|
|
handler := NewAuthCallbackHandler(proxyService, nil)
|
|
|
|
router := mux.NewRouter()
|
|
handler.RegisterEndpoints(router)
|
|
|
|
return &testSetup{
|
|
store: testStore,
|
|
oidcServer: oidcServer,
|
|
proxyService: proxyService,
|
|
handler: handler,
|
|
router: router,
|
|
cleanup: func() {
|
|
cleanup()
|
|
oidcServer.Close()
|
|
},
|
|
}
|
|
}
|
|
|
|
func createTestReverseProxies(t *testing.T, ctx context.Context, testStore store.Store) {
|
|
t.Helper()
|
|
|
|
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
|
require.NoError(t, err)
|
|
|
|
pubKey := base64.StdEncoding.EncodeToString(pub)
|
|
privKey := base64.StdEncoding.EncodeToString(priv)
|
|
|
|
testProxy := &service.Service{
|
|
ID: "testProxyId",
|
|
AccountID: "testAccountId",
|
|
Name: "Test Proxy",
|
|
Domain: "test-proxy.example.com",
|
|
Targets: []*service.Target{{
|
|
Path: strPtr("/"),
|
|
Host: "localhost",
|
|
Port: 8080,
|
|
Protocol: "http",
|
|
TargetId: "peer1",
|
|
TargetType: "peer",
|
|
Enabled: true,
|
|
}},
|
|
Enabled: true,
|
|
Auth: service.AuthConfig{
|
|
BearerAuth: &service.BearerAuthConfig{
|
|
Enabled: true,
|
|
DistributionGroups: []string{"allowedGroupId"},
|
|
},
|
|
},
|
|
SessionPrivateKey: privKey,
|
|
SessionPublicKey: pubKey,
|
|
}
|
|
require.NoError(t, testStore.CreateService(ctx, testProxy))
|
|
|
|
restrictedProxy := &service.Service{
|
|
ID: "restrictedProxyId",
|
|
AccountID: "testAccountId",
|
|
Name: "Restricted Proxy",
|
|
Domain: "restricted-proxy.example.com",
|
|
Targets: []*service.Target{{
|
|
Path: strPtr("/"),
|
|
Host: "localhost",
|
|
Port: 8080,
|
|
Protocol: "http",
|
|
TargetId: "peer1",
|
|
TargetType: "peer",
|
|
Enabled: true,
|
|
}},
|
|
Enabled: true,
|
|
Auth: service.AuthConfig{
|
|
BearerAuth: &service.BearerAuthConfig{
|
|
Enabled: true,
|
|
DistributionGroups: []string{"restrictedGroupId"},
|
|
},
|
|
},
|
|
SessionPrivateKey: privKey,
|
|
SessionPublicKey: pubKey,
|
|
}
|
|
require.NoError(t, testStore.CreateService(ctx, restrictedProxy))
|
|
|
|
noAuthProxy := &service.Service{
|
|
ID: "noAuthProxyId",
|
|
AccountID: "testAccountId",
|
|
Name: "No Auth Proxy",
|
|
Domain: "no-auth-proxy.example.com",
|
|
Targets: []*service.Target{{
|
|
Path: strPtr("/"),
|
|
Host: "localhost",
|
|
Port: 8080,
|
|
Protocol: "http",
|
|
TargetId: "peer1",
|
|
TargetType: "peer",
|
|
Enabled: true,
|
|
}},
|
|
Enabled: true,
|
|
Auth: service.AuthConfig{
|
|
BearerAuth: &service.BearerAuthConfig{
|
|
Enabled: false,
|
|
},
|
|
},
|
|
SessionPrivateKey: privKey,
|
|
SessionPublicKey: pubKey,
|
|
}
|
|
require.NoError(t, testStore.CreateService(ctx, noAuthProxy))
|
|
}
|
|
|
|
func strPtr(s string) *string {
|
|
return &s
|
|
}
|
|
|
|
func createTestAccountsAndUsers(t *testing.T, ctx context.Context, testStore store.Store) {
|
|
t.Helper()
|
|
|
|
testAccount := &types.Account{
|
|
Id: "testAccountId",
|
|
Domain: "test.com",
|
|
DomainCategory: "private",
|
|
IsDomainPrimaryAccount: true,
|
|
CreatedAt: time.Now(),
|
|
}
|
|
require.NoError(t, testStore.SaveAccount(ctx, testAccount))
|
|
|
|
allowedGroup := &types.Group{
|
|
ID: "allowedGroupId",
|
|
AccountID: "testAccountId",
|
|
Name: "Allowed Group",
|
|
Issued: "api",
|
|
}
|
|
require.NoError(t, testStore.CreateGroup(ctx, allowedGroup))
|
|
|
|
allowedUser := &types.User{
|
|
Id: "allowedUserId",
|
|
AccountID: "testAccountId",
|
|
Role: types.UserRoleUser,
|
|
AutoGroups: []string{"allowedGroupId"},
|
|
CreatedAt: time.Now(),
|
|
Issued: "api",
|
|
}
|
|
require.NoError(t, testStore.SaveUser(ctx, allowedUser))
|
|
|
|
// A second tenant, whose users must never be issued a token signed with
|
|
// the first tenant's service session key.
|
|
otherAccount := &types.Account{
|
|
Id: "otherAccountId",
|
|
Domain: "other.com",
|
|
DomainCategory: "private",
|
|
IsDomainPrimaryAccount: true,
|
|
CreatedAt: time.Now(),
|
|
}
|
|
require.NoError(t, testStore.SaveAccount(ctx, otherAccount))
|
|
|
|
otherAccountUser := &types.User{
|
|
Id: "otherAccountUserId",
|
|
AccountID: "otherAccountId",
|
|
Role: types.UserRoleUser,
|
|
CreatedAt: time.Now(),
|
|
Issued: "api",
|
|
}
|
|
require.NoError(t, testStore.SaveUser(ctx, otherAccountUser))
|
|
|
|
// A user awaiting approval is stored as blocked and pending approval, and
|
|
// carries the same group membership as the approved one.
|
|
pendingUser := &types.User{
|
|
Id: "pendingUserId",
|
|
AccountID: "testAccountId",
|
|
Role: types.UserRoleUser,
|
|
AutoGroups: []string{"allowedGroupId"},
|
|
Blocked: true,
|
|
PendingApproval: true,
|
|
CreatedAt: time.Now(),
|
|
Issued: "api",
|
|
}
|
|
require.NoError(t, testStore.SaveUser(ctx, pendingUser))
|
|
|
|
blockedUser := &types.User{
|
|
Id: "blockedUserId",
|
|
AccountID: "testAccountId",
|
|
Role: types.UserRoleUser,
|
|
AutoGroups: []string{"allowedGroupId"},
|
|
Blocked: true,
|
|
CreatedAt: time.Now(),
|
|
Issued: "api",
|
|
}
|
|
require.NoError(t, testStore.SaveUser(ctx, blockedUser))
|
|
}
|
|
|
|
// testServiceManager is a minimal implementation for testing.
|
|
type testServiceManager struct {
|
|
store store.Store
|
|
}
|
|
|
|
func (m *testServiceManager) DeleteAllServices(ctx context.Context, accountID, userID string) error {
|
|
return nil
|
|
}
|
|
|
|
func (m *testServiceManager) GetAllServices(_ context.Context, _, _ string) ([]*service.Service, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *testServiceManager) GetService(_ context.Context, _, _, _ string) (*service.Service, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *testServiceManager) CreateService(_ context.Context, _, _ string, _ *service.Service) (*service.Service, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *testServiceManager) UpdateService(_ context.Context, _, _ string, _ *service.Service) (*service.Service, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *testServiceManager) DeleteService(_ context.Context, _, _, _ string) error {
|
|
return nil
|
|
}
|
|
|
|
func (m *testServiceManager) DeleteAccountCluster(_ context.Context, _, _, _ string) error {
|
|
return nil
|
|
}
|
|
|
|
func (m *testServiceManager) SetCertificateIssuedAt(_ context.Context, _, _ string) error {
|
|
return nil
|
|
}
|
|
|
|
func (m *testServiceManager) SetStatus(_ context.Context, _, _ string, _ service.Status) error {
|
|
return nil
|
|
}
|
|
|
|
func (m *testServiceManager) ReloadAllServicesForAccount(_ context.Context, _ string) error {
|
|
return nil
|
|
}
|
|
|
|
func (m *testServiceManager) ReloadService(_ context.Context, _, _ string) error {
|
|
return nil
|
|
}
|
|
|
|
func (m *testServiceManager) GetGlobalServices(ctx context.Context) ([]*service.Service, error) {
|
|
return m.store.GetServices(ctx, store.LockingStrengthNone)
|
|
}
|
|
|
|
func (m *testServiceManager) GetServiceByID(ctx context.Context, accountID, proxyID string) (*service.Service, error) {
|
|
return m.store.GetServiceByID(ctx, store.LockingStrengthNone, accountID, proxyID)
|
|
}
|
|
|
|
func (m *testServiceManager) GetAccountServices(ctx context.Context, accountID string) ([]*service.Service, error) {
|
|
return m.store.GetAccountServices(ctx, store.LockingStrengthNone, accountID)
|
|
}
|
|
|
|
func (m *testServiceManager) GetServiceIDByTargetID(_ context.Context, _, _ string) (string, error) {
|
|
return "", nil
|
|
}
|
|
|
|
func (m *testServiceManager) CreateServiceFromPeer(_ context.Context, _, _ string, _ *service.ExposeServiceRequest) (*service.ExposeServiceResponse, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *testServiceManager) RenewServiceFromPeer(_ context.Context, _, _, _ string) error {
|
|
return nil
|
|
}
|
|
|
|
func (m *testServiceManager) StopServiceFromPeer(_ context.Context, _, _, _ string) error {
|
|
return nil
|
|
}
|
|
|
|
func (m *testServiceManager) StartExposeReaper(_ context.Context) {}
|
|
|
|
func (m *testServiceManager) GetServiceByDomain(ctx context.Context, domain string) (*service.Service, error) {
|
|
return m.store.GetServiceByDomain(ctx, domain)
|
|
}
|
|
|
|
func (m *testServiceManager) GetClusters(_ context.Context, _, _ string) ([]nbproxy.Cluster, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func createTestState(t *testing.T, ps *nbgrpc.ProxyServiceServer, redirectURL string) string {
|
|
t.Helper()
|
|
|
|
resp, err := ps.GetOIDCURL(context.Background(), &proto.GetOIDCURLRequest{
|
|
RedirectUrl: redirectURL,
|
|
AccountId: "testAccountId",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
parsedURL, err := url.Parse(resp.Url)
|
|
require.NoError(t, err)
|
|
|
|
return parsedURL.Query().Get("state")
|
|
}
|
|
|
|
func TestAuthCallback_UserAllowedToLogin(t *testing.T) {
|
|
setup := setupAuthCallbackTest(t)
|
|
defer setup.cleanup()
|
|
|
|
setup.oidcServer.tokenSubject = "allowedUserId"
|
|
|
|
state := createTestState(t, setup.proxyService, "https://test-proxy.example.com/dashboard")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/reverse-proxy/callback?code=test-auth-code&state="+url.QueryEscape(state), nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
setup.router.ServeHTTP(rec, req)
|
|
|
|
require.Equal(t, http.StatusFound, rec.Code)
|
|
|
|
location := rec.Header().Get("Location")
|
|
require.NotEmpty(t, location)
|
|
|
|
parsedLocation, err := url.Parse(location)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, "test-proxy.example.com", parsedLocation.Host)
|
|
require.NotEmpty(t, parsedLocation.Query().Get("session_token"), "Should include session token")
|
|
require.Empty(t, parsedLocation.Query().Get("error"), "Should not have error parameter")
|
|
}
|
|
|
|
// TestAuthCallback_UserDeniedByAccountStatus asserts that a user whose account
|
|
// is pending approval or blocked never receives a session token from the OIDC
|
|
// callback, and that the redirect carries a description the proxy can render.
|
|
func TestAuthCallback_UserDeniedByAccountStatus(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
subject string
|
|
expectErrorDesc string
|
|
}{
|
|
{
|
|
name: "pending approval",
|
|
subject: "pendingUserId",
|
|
expectErrorDesc: "Your account is pending approval by an administrator",
|
|
},
|
|
{
|
|
name: "blocked",
|
|
subject: "blockedUserId",
|
|
expectErrorDesc: "Your account is blocked",
|
|
},
|
|
{
|
|
name: "unknown to management",
|
|
subject: "userMissingFromStoreId",
|
|
expectErrorDesc: "Service configuration error",
|
|
},
|
|
{
|
|
// The account topology stays out of the browser-visible message.
|
|
name: "belongs to another account",
|
|
subject: "otherAccountUserId",
|
|
expectErrorDesc: "Service configuration error",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
setup := setupAuthCallbackTest(t)
|
|
defer setup.cleanup()
|
|
|
|
setup.oidcServer.tokenSubject = tt.subject
|
|
|
|
state := createTestState(t, setup.proxyService, "https://test-proxy.example.com/dashboard")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/reverse-proxy/callback?code=test-auth-code&state="+url.QueryEscape(state), nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
setup.router.ServeHTTP(rec, req)
|
|
|
|
require.Equal(t, http.StatusFound, rec.Code)
|
|
|
|
parsedLocation, err := url.Parse(rec.Header().Get("Location"))
|
|
require.NoError(t, err)
|
|
|
|
require.Empty(t, parsedLocation.Query().Get("session_token"), "Denied user must not receive a session token")
|
|
require.Equal(t, "access_denied", parsedLocation.Query().Get("error"))
|
|
require.Equal(t, tt.expectErrorDesc, parsedLocation.Query().Get("error_description"))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAuthCallback_ProxyNotFound(t *testing.T) {
|
|
setup := setupAuthCallbackTest(t)
|
|
defer setup.cleanup()
|
|
|
|
setup.oidcServer.tokenSubject = "allowedUserId"
|
|
|
|
state := createTestState(t, setup.proxyService, "https://test-proxy.example.com/")
|
|
|
|
require.NoError(t, setup.store.DeleteService(context.Background(), "testAccountId", "testProxyId"))
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/reverse-proxy/callback?code=test-auth-code&state="+url.QueryEscape(state), nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
setup.router.ServeHTTP(rec, req)
|
|
|
|
require.Equal(t, http.StatusFound, rec.Code)
|
|
|
|
location := rec.Header().Get("Location")
|
|
parsedLocation, err := url.Parse(location)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, "access_denied", parsedLocation.Query().Get("error"))
|
|
}
|
|
|
|
func TestAuthCallback_InvalidToken(t *testing.T) {
|
|
setup := setupAuthCallbackTest(t)
|
|
defer setup.cleanup()
|
|
|
|
setup.oidcServer.failExchange = true
|
|
|
|
state := createTestState(t, setup.proxyService, "https://test-proxy.example.com/")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/reverse-proxy/callback?code=invalid-code&state="+url.QueryEscape(state), nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
setup.router.ServeHTTP(rec, req)
|
|
|
|
require.Equal(t, http.StatusInternalServerError, rec.Code)
|
|
require.Contains(t, rec.Body.String(), "Failed to exchange code")
|
|
}
|
|
|
|
func TestAuthCallback_ExpiredToken(t *testing.T) {
|
|
setup := setupAuthCallbackTest(t)
|
|
defer setup.cleanup()
|
|
|
|
setup.oidcServer.tokenSubject = "allowedUserId"
|
|
setup.oidcServer.tokenExpiry = -time.Hour
|
|
|
|
state := createTestState(t, setup.proxyService, "https://test-proxy.example.com/")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/reverse-proxy/callback?code=test-auth-code&state="+url.QueryEscape(state), nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
setup.router.ServeHTTP(rec, req)
|
|
|
|
require.Equal(t, http.StatusUnauthorized, rec.Code)
|
|
require.Contains(t, rec.Body.String(), "Failed to validate token")
|
|
}
|
|
|
|
func TestAuthCallback_InvalidState(t *testing.T) {
|
|
setup := setupAuthCallbackTest(t)
|
|
defer setup.cleanup()
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/reverse-proxy/callback?code=test-auth-code&state=invalid-state", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
setup.router.ServeHTTP(rec, req)
|
|
|
|
require.Equal(t, http.StatusBadRequest, rec.Code)
|
|
require.Contains(t, rec.Body.String(), "Invalid state")
|
|
}
|
|
|
|
func TestAuthCallback_MissingState(t *testing.T) {
|
|
setup := setupAuthCallbackTest(t)
|
|
defer setup.cleanup()
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/reverse-proxy/callback?code=test-auth-code", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
setup.router.ServeHTTP(rec, req)
|
|
|
|
require.Equal(t, http.StatusBadRequest, rec.Code)
|
|
}
|