• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pomerium / pomerium / 23496157803

24 Mar 2026 02:51PM UTC coverage: 45.485% (+0.01%) from 45.473%
23496157803

push

github

web-flow
ci: update dependencies (#6210)

This PR updates dependencies not managed by dependabot.

Co-authored-by: GitHub Actions <apparitor@users.noreply.github.com>

34818 of 76548 relevant lines covered (45.49%)

115.02 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.95
/pkg/ssh/auth.go
1
package ssh
2

3
import (
4
        "context"
5
        "crypto/sha256"
6
        "encoding/base64"
7
        "errors"
8
        "fmt"
9
        "net/url"
10
        "sync/atomic"
11
        "time"
12

13
        "github.com/cenkalti/backoff/v4"
14
        "github.com/rs/zerolog"
15
        otelcode "go.opentelemetry.io/otel/codes"
16
        "go.opentelemetry.io/otel/metric"
17
        noopmetric "go.opentelemetry.io/otel/metric/noop"
18
        oteltrace "go.opentelemetry.io/otel/trace"
19
        nooptrace "go.opentelemetry.io/otel/trace/noop"
20
        "google.golang.org/grpc/codes"
21
        "google.golang.org/grpc/status"
22
        "google.golang.org/protobuf/types/known/timestamppb"
23

24
        extensions_ssh "github.com/pomerium/envoy-custom/api/extensions/filters/network/ssh"
25
        "github.com/pomerium/pomerium/authorize/evaluator"
26
        "github.com/pomerium/pomerium/config"
27
        "github.com/pomerium/pomerium/internal/log"
28
        "github.com/pomerium/pomerium/pkg/grpc/databroker"
29
        identitypb "github.com/pomerium/pomerium/pkg/grpc/identity"
30
        "github.com/pomerium/pomerium/pkg/grpc/session"
31
        "github.com/pomerium/pomerium/pkg/identity"
32
        "github.com/pomerium/pomerium/pkg/identity/oidc"
33
        "github.com/pomerium/pomerium/pkg/identity/oidc/hosted"
34
        "github.com/pomerium/pomerium/pkg/policy/criteria"
35
        "github.com/pomerium/pomerium/pkg/ssh/api"
36
        "github.com/pomerium/pomerium/pkg/ssh/code"
37
)
38

39
const (
40
        telemetryFingerprintAttribute = "publickey-fingerprint"
41
)
42

43
//go:generate go tool -modfile ../../internal/tools/go.mod go.uber.org/mock/mockgen -typed -destination ./mock/mock_evaluator.go . SSHEvaluator
44

45
//nolint:revive
46
type SSHEvaluator interface {
47
        EvaluateSSH(ctx context.Context, streamID uint64, req AuthRequest, initialAuthComplete bool) (*evaluator.Result, error)
48
        EvaluateUpstreamTunnel(ctx context.Context, req AuthRequest, route *config.Policy) (*evaluator.Result, error)
49
}
50

51
type Evaluator interface {
52
        SSHEvaluator
53
        databroker.ClientGetter
54
        InvalidateCacheForRecords(context.Context, ...*databroker.Record)
55
}
56

57
type AuthRequest struct {
58
        Username         string
59
        Hostname         string
60
        PublicKey        string // No encoding
61
        SessionID        string
62
        SourceAddress    string
63
        SessionBindingID string
64
        LogOnlyIfDenied  bool
65
}
66

67
type Auth struct {
68
        evaluator      Evaluator
69
        currentConfig  *atomic.Pointer[config.Config]
70
        tracerProvider oteltrace.TracerProvider
71
        tracer         oteltrace.Tracer
72
        codeIssuer     code.Issuer
73
        codeMetrics    *code.Metrics
74
}
75

76
type Options struct {
77
        meter  metric.Meter
78
        tracer oteltrace.Tracer
79
}
80

81
func (o *Options) Apply(opts ...Option) {
61✔
82
        for _, opt := range opts {
141✔
83
                opt(o)
80✔
84
        }
80✔
85
}
86

87
type Option func(o *Options)
88

89
func WithMetricMeter(m metric.Meter) Option {
40✔
90
        return func(o *Options) {
80✔
91
                o.meter = m
40✔
92
        }
40✔
93
}
94

95
func WithTracer(t oteltrace.Tracer) Option {
40✔
96
        return func(o *Options) {
80✔
97
                o.tracer = t
40✔
98
        }
40✔
99
}
100

101
func NewAuth(
102
        evaluator Evaluator,
103
        currentConfig *atomic.Pointer[config.Config],
104
        tracerProvider oteltrace.TracerProvider,
105
        codeIssuer code.Issuer,
106
        opts ...Option,
107
) *Auth {
61✔
108
        options := Options{
61✔
109
                meter:  noopmetric.Meter{},
61✔
110
                tracer: nooptrace.Tracer{},
61✔
111
        }
61✔
112
        options.Apply(opts...)
61✔
113
        metrics, err := code.NewMetrics(options.meter)
61✔
114
        if err != nil {
61✔
115
                log.Fatal().Msg("error initializing ssh auth code metrics")
×
116
        }
×
117

118
        return &Auth{
61✔
119
                evaluator,
61✔
120
                currentConfig,
61✔
121
                tracerProvider,
61✔
122
                options.tracer,
61✔
123
                codeIssuer,
61✔
124
                metrics,
61✔
125
        }
61✔
126
}
127

128
// GetDataBrokerServiceClient implements AuthInterface.
129
func (a *Auth) GetDataBrokerServiceClient() databroker.DataBrokerServiceClient {
211✔
130
        return a.evaluator.GetDataBrokerServiceClient()
211✔
131
}
211✔
132

133
func (a *Auth) HandlePublicKeyMethodRequest(
134
        ctx context.Context,
135
        info StreamAuthInfo,
136
        user api.UserRequest,
137
        req *extensions_ssh.PublicKeyMethodRequest,
138
) (PublicKeyAuthMethodResponse, error) {
50✔
139
        ctx, span := a.tracer.Start(ctx, "authorize.ssh.HandlePublicKeyMethodRequest")
50✔
140
        defer span.End()
50✔
141
        resp, err := a.handlePublicKeyMethodRequest(ctx, info, user, req)
50✔
142
        if err != nil {
51✔
143
                log.Ctx(ctx).Error().Err(err).Msg("ssh publickey auth request error")
1✔
144
                span.SetStatus(otelcode.Error, "internal error")
1✔
145
                return resp, status.Error(codes.Internal, "internal error")
1✔
146
        }
1✔
147
        return resp, err
49✔
148
}
149

150
func fingerprintAsStrAttribute(publicKeyFingerprintSha256 []byte) string {
92✔
151
        if publicKeyFingerprintSha256 == nil {
95✔
152
                return ""
3✔
153
        }
3✔
154
        return base64.RawStdEncoding.EncodeToString(publicKeyFingerprintSha256)
89✔
155
}
156

157
func (a *Auth) handlePublicKeyMethodRequest(
158
        ctx context.Context,
159
        info StreamAuthInfo,
160
        user api.UserRequest,
161
        req *extensions_ssh.PublicKeyMethodRequest,
162
) (PublicKeyAuthMethodResponse, error) {
52✔
163
        ctx, span := a.tracer.Start(ctx, "authorize.ssh.handlePublicKeyMethodRequest")
52✔
164
        defer span.End()
52✔
165
        sessionBindingID, err := sessionIDFromFingerprint(req.PublicKeyFingerprintSha256)
52✔
166
        if err != nil {
53✔
167
                return PublicKeyAuthMethodResponse{}, err
1✔
168
        }
1✔
169
        sessionBinding, err := a.resolveSession(ctx, sessionBindingID)
51✔
170
        if err != nil {
96✔
171
                if st, ok := status.FromError(err); ok && st.Code() == codes.NotFound {
89✔
172
                        span.SetStatus(otelcode.Ok, "must reauthenticated")
44✔
173
                        return PublicKeyAuthMethodResponse{
44✔
174
                                Allow:                    publicKeyAllowResponse(req.PublicKey),
44✔
175
                                RequireAdditionalMethods: []string{MethodKeyboardInteractive},
44✔
176
                        }, nil
44✔
177
                }
44✔
178
                return PublicKeyAuthMethodResponse{}, err
1✔
179
        }
180
        sshreq := AuthRequest{
6✔
181
                Username:         user.Username(),
6✔
182
                Hostname:         user.Hostname(),
6✔
183
                PublicKey:        string(req.PublicKey),
6✔
184
                SessionID:        sessionBinding.SessionId,
6✔
185
                SessionBindingID: sessionBindingID,
6✔
186
                SourceAddress:    info.SourceAddress,
6✔
187
        }
6✔
188
        log.Ctx(ctx).Debug().
6✔
189
                Str("username", user.Username()).
6✔
190
                Str("hostname", user.Hostname()).
6✔
191
                Str("session-id", sessionBinding.SessionId).
6✔
192
                Msg("ssh publickey auth request")
6✔
193

6✔
194
        // Special case: internal command (e.g. routes portal).
6✔
195
        if user.Hostname() == "" {
7✔
196
                _, err := session.Get(ctx, a.evaluator.GetDataBrokerServiceClient(), sessionBinding.SessionId)
1✔
197
                if status.Code(err) == codes.NotFound {
1✔
198
                        // Require IdP login.
×
199
                        return PublicKeyAuthMethodResponse{
×
200
                                Allow:                    publicKeyAllowResponse(req.PublicKey),
×
201
                                RequireAdditionalMethods: []string{MethodKeyboardInteractive},
×
202
                        }, nil
×
203
                } else if err != nil {
1✔
204
                        return PublicKeyAuthMethodResponse{}, err
×
205
                }
×
206
        }
207

208
        res, err := a.evaluator.EvaluateSSH(ctx, info.StreamID, sshreq, info.InitialAuthComplete)
6✔
209
        if err != nil {
7✔
210
                span.SetStatus(otelcode.Error, "internal error : evaluate")
1✔
211
                return PublicKeyAuthMethodResponse{}, err
1✔
212
        }
1✔
213

214
        // Interpret the results of policy evaluation.
215
        if res.HasReason(criteria.ReasonSSHPublickeyUnauthorized) {
6✔
216
                // This public key is not allowed, but the client is free to try a different key.
1✔
217
                span.SetStatus(otelcode.Ok, "public key not allowed")
1✔
218
                return PublicKeyAuthMethodResponse{
1✔
219
                        RequireAdditionalMethods: []string{MethodPublicKey},
1✔
220
                }, nil
1✔
221
        } else if res.HasReason(criteria.ReasonUserUnauthenticated) {
6✔
222
                // Mark public key as allowed, to initiate IdP login flow.
1✔
223
                span.SetStatus(otelcode.Ok, "un-authenticated")
1✔
224
                return PublicKeyAuthMethodResponse{
1✔
225
                        Allow:                    publicKeyAllowResponse(req.PublicKey),
1✔
226
                        RequireAdditionalMethods: []string{MethodKeyboardInteractive},
1✔
227
                }, nil
1✔
228
        } else if res.Allow.Value && !res.Deny.Value {
6✔
229
                // Allowed, no login needed.
2✔
230
                span.SetStatus(otelcode.Ok, "allowed")
2✔
231
                return PublicKeyAuthMethodResponse{
2✔
232
                        Allow: publicKeyAllowResponse(req.PublicKey),
2✔
233
                }, nil
2✔
234
        }
2✔
235
        // Denied, no login needed.
236
        span.SetStatus(otelcode.Ok, "denied")
1✔
237
        return PublicKeyAuthMethodResponse{}, nil
1✔
238
}
239

240
func publicKeyAllowResponse(publicKey []byte) *extensions_ssh.PublicKeyAllowResponse {
47✔
241
        return &extensions_ssh.PublicKeyAllowResponse{
47✔
242
                PublicKey: publicKey,
47✔
243
                Permissions: &extensions_ssh.Permissions{
47✔
244
                        PermitPortForwarding:  true,
47✔
245
                        PermitAgentForwarding: true,
47✔
246
                        PermitX11Forwarding:   true,
47✔
247
                        PermitPty:             true,
47✔
248
                        PermitUserRc:          true,
47✔
249
                        ValidStartTime:        timestamppb.New(time.Now().Add(-1 * time.Minute)),
47✔
250
                        ValidEndTime:          timestamppb.New(time.Now().Add(1 * time.Hour)),
47✔
251
                },
47✔
252
        }
47✔
253
}
47✔
254

255
func (a *Auth) HandleKeyboardInteractiveMethodRequest(
256
        ctx context.Context,
257
        info StreamAuthInfo,
258
        user api.UserRequest,
259
        _ *extensions_ssh.KeyboardInteractiveMethodRequest,
260
        querier KeyboardInteractiveQuerier,
261
) (KeyboardInteractiveAuthMethodResponse, error) {
45✔
262
        resp, err := a.handleKeyboardInteractiveMethodRequest(ctx, info, user, querier)
45✔
263
        if err != nil {
47✔
264
                log.Ctx(ctx).Error().Err(err).Msg("ssh keyboard-interactive auth request error")
2✔
265
                if _, ok := status.FromError(err); !ok {
2✔
266
                        return resp, status.Error(codes.Internal, err.Error())
×
267
                }
×
268
                return resp, err
2✔
269
        }
270
        return resp, err
43✔
271
}
272

273
func (a *Auth) handleKeyboardInteractiveMethodRequest(
274
        ctx context.Context,
275
        info StreamAuthInfo,
276
        user api.UserRequest,
277
        querier KeyboardInteractiveQuerier,
278
) (KeyboardInteractiveAuthMethodResponse, error) {
47✔
279
        fingerprintAttrVal := fingerprintAsStrAttribute(info.PublicKeyFingerprintSha256)
47✔
280
        ctx, span := a.tracer.Start(ctx, "authorize.ssh.handleKeyboardInteractiveMethodRequest")
47✔
281
        defer span.End()
47✔
282
        if info.PublicKeyAllow.Value == nil {
48✔
283
                // Sanity check: this method is only valid if we already accepted a public key.
1✔
284
                return KeyboardInteractiveAuthMethodResponse{}, errPublicKeyAllowNil
1✔
285
        }
1✔
286

287
        log.Ctx(ctx).Debug().
46✔
288
                Str("username", user.Username()).
46✔
289
                Str("hostname", user.Hostname()).
46✔
290
                Str(telemetryFingerprintAttribute, fingerprintAttrVal).
46✔
291
                Msg("ssh keyboard-interactive auth request")
46✔
292

46✔
293
        // Initiate the IdP login flow.
46✔
294
        err := a.handleLogin(ctx, user.Hostname(), info.SourceAddress, info.PublicKeyFingerprintSha256, querier)
46✔
295
        if err != nil {
49✔
296
                span.SetStatus(otelcode.Error, "login failed")
3✔
297
                return KeyboardInteractiveAuthMethodResponse{}, err
3✔
298
        }
3✔
299

300
        if err := a.EvaluateDelayed(ctx, info, user); err != nil {
45✔
301
                // Denied.
2✔
302
                span.SetStatus(otelcode.Ok, "denied")
2✔
303
                return KeyboardInteractiveAuthMethodResponse{}, nil
2✔
304
        }
2✔
305
        // Allowed.
306
        span.SetStatus(otelcode.Ok, "allowed")
41✔
307
        return KeyboardInteractiveAuthMethodResponse{
41✔
308
                Allow: &extensions_ssh.KeyboardInteractiveAllowResponse{},
41✔
309
        }, nil
41✔
310
}
311

312
func (a *Auth) handleLogin(
313
        ctx context.Context,
314
        hostname string,
315
        sourceAddr string,
316
        publicKeyFingerprint []byte,
317
        querier KeyboardInteractiveQuerier,
318
) (err error) {
46✔
319
        ctx, span := a.tracer.Start(ctx, "authorize.ssh.handleLogin")
46✔
320
        defer span.End()
46✔
321

46✔
322
        cfg := a.currentConfig.Load()
46✔
323
        if cfg.Options.UseStatelessAuthenticateFlow() {
47✔
324
                return status.Error(codes.FailedPrecondition, "ssh login is not currently enabled")
1✔
325
        }
1✔
326

327
        l := log.Ctx(ctx).With().
45✔
328
                Str("protocol", "ssh").
45✔
329
                Str("source-addr", sourceAddr).
45✔
330
                Str(telemetryFingerprintAttribute, fingerprintAsStrAttribute(publicKeyFingerprint)).
45✔
331
                Logger()
45✔
332

45✔
333
        a.codeMetrics.SSHAuthCodeRequestsTotal.Add(ctx, 1)
45✔
334
        a.codeMetrics.PendingSessionInc(ctx)
45✔
335
        defer a.codeMetrics.PendingSessionDec(ctx)
45✔
336
        l.Info().Msg("client requesting authentication")
45✔
337

45✔
338
        bindingKey, err := sessionIDFromFingerprint(publicKeyFingerprint)
45✔
339
        if err != nil {
46✔
340
                return a.reportLoginCodeFailure(ctx, l, span, codes.Internal, err.Error())
1✔
341
        }
1✔
342
        idp, authenticator, err := a.getAuthenticator(ctx, hostname)
44✔
343
        if err != nil {
44✔
344
                return a.reportLoginCodeFailure(ctx, l, span, codes.Internal, err.Error())
×
345
        }
×
346
        authURL, _ := cfg.Options.GetInternalAuthenticateURL()
44✔
347
        generatedCode := a.codeIssuer.IssueCode()
44✔
348
        now := timestamppb.Now()
44✔
349

44✔
350
        req := &session.SessionBindingRequest{
44✔
351
                IdpId:     idp.GetId(),
44✔
352
                Key:       bindingKey,
44✔
353
                Protocol:  session.ProtocolSSH,
44✔
354
                State:     session.SessionBindingRequestState_InFlight,
44✔
355
                CreatedAt: now,
44✔
356
                ExpiresAt: timestamppb.New(now.AsTime().Add(code.DefaultCodeTTL)),
44✔
357
                Details: map[string]string{
44✔
358
                        session.DetailSourceAddr: sourceAddr,
44✔
359
                },
44✔
360
        }
44✔
361

44✔
362
        ctxT, ca := context.WithDeadline(ctx, req.ExpiresAt.AsTime())
44✔
363
        defer ca()
44✔
364
        startCodeTime := time.Now()
44✔
365
        associatedCode, err := a.codeIssuer.AssociateCode(ctxT, generatedCode, req)
44✔
366
        endCodeTime := time.Now()
44✔
367
        a.codeMetrics.SSHIssueCodeDuration.Record(ctx, endCodeTime.Sub(startCodeTime).Seconds())
44✔
368
        if err != nil {
44✔
369
                return a.reportLoginCodeFailure(ctx, l, span, codes.Aborted, "failed to associate a code to this session")
×
370
        }
×
371
        var prompt string
44✔
372

44✔
373
        query := &url.Values{}
44✔
374
        query.Add("user_code", string(associatedCode))
44✔
375
        promptURI := authURL.ResolveReference(&url.URL{
44✔
376
                Path:     "/.pomerium/sign_in",
44✔
377
                RawQuery: query.Encode(),
44✔
378
        })
44✔
379
        prompt = promptURI.String()
44✔
380
        _, _ = querier.Prompt(ctxT, &extensions_ssh.KeyboardInteractiveInfoPrompts{
44✔
381
                Name:        SignInPrompt(authenticator),
44✔
382
                Instruction: prompt,
44✔
383
                Prompts:     nil,
44✔
384
        })
44✔
385
        startDecisionTime := time.Now()
44✔
386

44✔
387
        defer func() {
88✔
388
                endDecisionTime := time.Now()
44✔
389
                a.codeMetrics.SSHUserCodeDecisionDuration.Record(ctx, endDecisionTime.Sub(startDecisionTime).Seconds())
44✔
390
        }()
44✔
391

392
        statusC := a.codeIssuer.OnCodeDecision(ctxT, associatedCode)
44✔
393
        select {
44✔
394
        case <-a.codeIssuer.Done():
×
395
                return a.reportLoginCodeFailure(ctx, l, span, codes.Internal, "code issuer can no longer process this request")
×
396
        case <-ctxT.Done():
×
397
                return a.reportLoginCodeFailure(ctx, l, span, codes.Canceled, "authentication request timeout")
×
398
        case st, ok := <-statusC:
44✔
399
                if !ok {
44✔
400
                        return a.reportLoginCodeFailure(ctx, l, span, codes.DeadlineExceeded, "authentication request cancelled by user or timeout exceeded")
×
401
                }
×
402
                if st.State == session.SessionBindingRequestState_Revoked {
45✔
403
                        return a.reportLoginCodeFailure(ctx, l, span, codes.PermissionDenied, "user has denied this code")
1✔
404
                }
1✔
405
                if st.BindingKey != bindingKey {
43✔
406
                        return a.reportLoginCodeFailure(ctx, l, span, codes.Internal, "mismatched binding keys")
×
407
                }
×
408
                ctxca, ca := context.WithTimeout(context.Background(), 30*time.Second)
43✔
409
                defer ca()
43✔
410
                b := backoff.WithContext(backoff.NewExponentialBackOff(), ctxca)
43✔
411
                client := a.evaluator.GetDataBrokerServiceClient()
43✔
412
                err := backoff.Retry(func() error {
86✔
413
                        rec, err := client.Get(ctxca, &databroker.GetRequest{
43✔
414
                                Type: "type.googleapis.com/session.SessionBinding",
43✔
415
                                Id:   bindingKey,
43✔
416
                        })
43✔
417
                        if rec.Record.DeletedAt != nil {
43✔
418
                                return fmt.Errorf("stale record")
×
419
                        }
×
420
                        if err != nil {
43✔
421
                                return err
×
422
                        }
×
423
                        a.evaluator.InvalidateCacheForRecords(ctxca, rec.GetRecord())
43✔
424
                        return nil
43✔
425
                }, b)
426
                if err != nil {
43✔
427
                        return a.reportLoginCodeFailure(ctx, l, span, codes.Internal, fmt.Sprintf("failed to get matching session binding : %s", err.Error()))
×
428
                }
×
429
                l.Info().Msg("successfully authenticated")
43✔
430
                span.SetStatus(otelcode.Ok, "successfully authenticated")
43✔
431
                return nil
43✔
432
        }
433
}
434

435
func SignInPrompt(authenticator identity.Authenticator) string {
47✔
436
        switch authenticator.Name() {
47✔
437
        case hosted.Name, oidc.Name:
46✔
438
                return "Please sign in to continue"
46✔
439
        default:
1✔
440
                return "Please sign in with " + authenticator.Name() + " to continue"
1✔
441
        }
442
}
443

444
func (a *Auth) reportLoginCodeFailure(
445
        ctx context.Context,
446
        l zerolog.Logger,
447
        span oteltrace.Span,
448
        grpcCode codes.Code,
449
        errorStr string,
450
) error {
2✔
451
        switch grpcCode {
2✔
452
        case codes.PermissionDenied:
1✔
453
                a.codeMetrics.SSHAuthCodeRequestFailuresTotal.Add(ctx, 1, metric.WithAttributes(
1✔
454
                        code.FailureReason(code.FailureRevoked),
1✔
455
                ))
1✔
456
                l.Error().Str(zerolog.ErrorFieldName, errorStr).Msg("code denied")
1✔
457
                span.SetStatus(otelcode.Error, "code denied")
1✔
458
        case codes.Canceled, codes.DeadlineExceeded:
×
459
                a.codeMetrics.SSHAuthCodeRequestFailuresTotal.Add(ctx, 1, metric.WithAttributes(
×
460
                        code.FailureReason(code.FailureTimeout),
×
461
                ))
×
462
                l.Error().Str(zerolog.ErrorFieldName, errorStr).Msg("cancelled")
×
463
                span.SetStatus(otelcode.Error, "cancelled")
×
464

465
        default:
1✔
466
                a.codeMetrics.SSHAuthCodeRequestFailuresTotal.Add(ctx, 1, metric.WithAttributes(
1✔
467
                        code.FailureReason(code.FailureInternal),
1✔
468
                ))
1✔
469
                l.Error().Str(zerolog.ErrorFieldName, errorStr).Msg("internal failure")
1✔
470
                span.SetStatus(otelcode.Error, "internal failure")
1✔
471
        }
472
        return status.Error(grpcCode, errorStr)
2✔
473
}
474

475
var errAccessDenied = status.Error(codes.PermissionDenied, "access denied")
476

477
func (a *Auth) EvaluateDelayed(ctx context.Context, info StreamAuthInfo, user api.UserRequest) error {
61✔
478
        req, err := a.sshRequestFromStreamAuthInfo(ctx, info, user)
61✔
479
        if err != nil {
62✔
480
                return err
1✔
481
        }
1✔
482
        res, err := a.evaluator.EvaluateSSH(ctx, info.StreamID, req, info.InitialAuthComplete)
60✔
483
        if err != nil {
60✔
484
                return err
×
485
        }
×
486

487
        if res.Allow.Value && !res.Deny.Value {
110✔
488
                return nil
50✔
489
        }
50✔
490
        return errAccessDenied
10✔
491
}
492

493
// EvaluatePortForward implements AuthInterface.
494
func (a *Auth) EvaluatePortForward(ctx context.Context, info StreamAuthInfo, user api.UserRequest, route *config.Policy) error {
×
495
        req, err := a.sshRequestFromStreamAuthInfo(ctx, info, user)
×
496
        if err != nil {
×
497
                return err
×
498
        }
×
499
        res, err := a.evaluator.EvaluateUpstreamTunnel(ctx, req, route)
×
500
        if err != nil {
×
501
                return err
×
502
        }
×
503

504
        if res.Allow.Value && !res.Deny.Value {
×
505
                return nil
×
506
        }
×
507
        return errAccessDenied
×
508
}
509

510
func (a *Auth) GetSession(ctx context.Context, info StreamAuthInfo) (*session.Session, error) {
8✔
511
        sessionBinding, err := a.resolveSessionFromFingerprint(ctx, info.PublicKeyFingerprintSha256)
8✔
512
        if err != nil {
9✔
513
                return nil, err
1✔
514
        }
1✔
515
        sessionResp, err := a.evaluator.GetDataBrokerServiceClient().Get(ctx, &databroker.GetRequest{
7✔
516
                Type: "type.googleapis.com/session.Session",
7✔
517
                Id:   sessionBinding.SessionId,
7✔
518
        })
7✔
519
        if err != nil {
7✔
520
                return nil, err
×
521
        }
×
522
        if sessionResp.GetRecord().DeletedAt != nil {
7✔
523
                return nil, status.Error(codes.NotFound, "session deleted")
×
524
        }
×
525
        var s session.Session
7✔
526
        if err := sessionResp.Record.Data.UnmarshalTo(&s); err != nil {
7✔
527
                return nil, status.Error(codes.Internal, err.Error())
×
528
        }
×
529
        return &s, nil
7✔
530
}
531

532
func (a *Auth) DeleteSession(ctx context.Context, info StreamAuthInfo) error {
8✔
533
        binding, err := a.resolveSessionFromFingerprint(ctx, info.PublicKeyFingerprintSha256)
8✔
534
        if err != nil {
10✔
535
                return err
2✔
536
        }
2✔
537
        toInvalidate := []*databroker.Record{}
6✔
538
        sessionErr := session.Delete(ctx, a.evaluator.GetDataBrokerServiceClient(), binding.SessionId)
6✔
539
        a.evaluator.InvalidateCacheForRecords(ctx,
6✔
540
                &databroker.Record{
6✔
541
                        Type: "type.googleapis.com/session.Session",
6✔
542
                        Id:   binding.SessionId,
6✔
543
                },
6✔
544
        )
6✔
545
        toInvalidate = append(toInvalidate, &databroker.Record{
6✔
546
                Type: "type.googleapis.com/session.Session",
6✔
547
                Id:   binding.SessionId,
6✔
548
        })
6✔
549

6✔
550
        bindingRecs, bindingErr := a.codeIssuer.RevokeSessionBindingBySession(ctx, binding.SessionId)
6✔
551
        if bindingErr == nil && len(bindingRecs) > 0 {
7✔
552
                toInvalidate = append(toInvalidate, bindingRecs...)
1✔
553
        }
1✔
554
        a.evaluator.InvalidateCacheForRecords(ctx, toInvalidate...)
6✔
555
        return errors.Join(sessionErr, bindingErr)
6✔
556
}
557

558
func (a *Auth) getAuthenticator(ctx context.Context, hostname string) (*identitypb.Provider, identity.Authenticator, error) {
44✔
559
        opts := a.currentConfig.Load().Options
44✔
560

44✔
561
        redirectURL, err := opts.GetAuthenticateRedirectURL()
44✔
562
        if err != nil {
44✔
563
                return nil, nil, err
×
564
        }
×
565

566
        idp, err := opts.GetIdentityProviderForPolicy(opts.GetRouteForSSHHostname(hostname))
44✔
567
        if err != nil {
44✔
568
                return nil, nil, err
×
569
        }
×
570

571
        authenticator, err := identity.GetIdentityProvider(ctx, a.tracerProvider, idp, redirectURL,
44✔
572
                opts.RuntimeFlags[config.RuntimeFlagRefreshSessionAtIDTokenExpiration])
44✔
573
        if err != nil {
44✔
574
                return nil, nil, err
×
575
        }
×
576

577
        return idp, authenticator, nil
44✔
578
}
579

580
var _ AuthInterface = (*Auth)(nil)
581

582
var errInvalidFingerprint = errors.New("invalid public key fingerprint")
583

584
func (a *Auth) resolveSession(ctx context.Context, sessionBindingID string) (*session.SessionBinding, error) {
126✔
585
        resp, err := a.evaluator.GetDataBrokerServiceClient().Get(ctx, &databroker.GetRequest{
126✔
586
                Type: "type.googleapis.com/session.SessionBinding",
126✔
587
                Id:   sessionBindingID,
126✔
588
        })
126✔
589
        if err != nil {
170✔
590
                return nil, err
44✔
591
        }
44✔
592
        if resp.Record.DeletedAt != nil {
82✔
593
                return nil, status.Error(codes.NotFound, "session binding deleted")
×
594
        }
×
595

596
        var binding session.SessionBinding
82✔
597
        if err := resp.Record.Data.UnmarshalTo(&binding); err != nil {
82✔
598
                return nil, status.Error(codes.Internal, err.Error())
×
599
        }
×
600
        now := time.Now()
82✔
601
        if binding.ExpiresAt.AsTime().Before(now) {
83✔
602
                return nil, status.Error(codes.NotFound, "session binding no longer valid")
1✔
603
        }
1✔
604
        if binding.Protocol != session.ProtocolSSH {
81✔
605
                return nil, status.Error(codes.Internal, "invalid protocol")
×
606
        }
×
607
        sessionResp, err := a.evaluator.GetDataBrokerServiceClient().Get(ctx, &databroker.GetRequest{
81✔
608
                Type: "type.googleapis.com/session.Session",
81✔
609
                Id:   binding.SessionId,
81✔
610
        })
81✔
611
        if err != nil {
83✔
612
                return nil, err
2✔
613
        }
2✔
614
        if sessionResp.GetRecord().DeletedAt != nil {
79✔
615
                return nil, status.Error(codes.NotFound, "session deleted")
×
616
        }
×
617

618
        return &binding, nil
79✔
619
}
620

621
func sessionIDFromFingerprint(sha256fingerprint []byte) (string, error) {
174✔
622
        if len(sha256fingerprint) != sha256.Size {
178✔
623
                return "", errInvalidFingerprint
4✔
624
        }
4✔
625
        return "sshkey-SHA256:" + base64.RawStdEncoding.EncodeToString(sha256fingerprint), nil
170✔
626
}
627

628
func (a *Auth) resolveSessionFromFingerprint(ctx context.Context, sha256fingerprint []byte) (*session.SessionBinding, error) {
16✔
629
        id, err := sessionIDFromFingerprint(sha256fingerprint)
16✔
630
        if err != nil {
18✔
631
                return nil, err
2✔
632
        }
2✔
633
        return a.resolveSession(ctx, id)
14✔
634
}
635

636
var errPublicKeyAllowNil = errors.New("expected PublicKeyAllow message not to be nil")
637

638
// Converts from StreamAuthInfo to an SSHRequest, assuming the PublicKeyAllow field is not nil.
639
func (a *Auth) sshRequestFromStreamAuthInfo(ctx context.Context, info StreamAuthInfo, user api.UserRequest) (AuthRequest, error) {
61✔
640
        if info.PublicKeyAllow.Value == nil {
61✔
641
                return AuthRequest{}, errPublicKeyAllowNil
×
642
        }
×
643
        sessionBindingID, err := sessionIDFromFingerprint(info.PublicKeyFingerprintSha256)
61✔
644
        if err != nil {
61✔
645
                return AuthRequest{}, err
×
646
        }
×
647
        sessionBinding, err := a.resolveSession(ctx, sessionBindingID)
61✔
648
        if err != nil {
62✔
649
                return AuthRequest{}, err
1✔
650
        }
1✔
651

652
        return AuthRequest{
60✔
653
                Username:         user.Username(),
60✔
654
                Hostname:         user.Hostname(),
60✔
655
                PublicKey:        string(info.PublicKeyAllow.Value.PublicKey),
60✔
656
                SessionID:        sessionBinding.SessionId,
60✔
657
                SourceAddress:    info.SourceAddress,
60✔
658
                SessionBindingID: sessionBindingID,
60✔
659

60✔
660
                LogOnlyIfDenied: info.InitialAuthComplete,
60✔
661
        }, nil
60✔
662
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc