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

pomerium / pomerium / 19449879791

18 Nov 2025 12:48AM UTC coverage: 54.843% (-0.02%) from 54.863%
19449879791

push

github

web-flow
fix: standardize resolving sessions (#5933)

## Summary

<!--  For example...
The existing implementation has poor numerical properties for
large arguments, so use the McGillicutty algorithm to improve
accuracy above 1e10.

The algorithm is described at
https://wikipedia.org/wiki/McGillicutty_Algorithm
-->

## Related issues


[ENG-3172](https://linear.app/pomerium/issue/ENG-3172/session-binding-expiry-not-handled-correctly-in-all-cases)

## User Explanation

<!-- How would you explain this change to the user? If this
change doesn't create any user-facing changes, you can leave
this blank. If filled out, add the `docs` label -->

## Checklist

- [X] reference any related issues
- [X] updated unit tests
- [X] add appropriate label (`enhancement`, `bug`, `breaking`,
`dependencies`, `ci`)
- [X] ready for review

7 of 18 new or added lines in 2 files covered. (38.89%)

23 existing lines in 8 files now uncovered.

28712 of 52353 relevant lines covered (54.84%)

94.08 hits per line

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

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

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

15
        "github.com/cenkalti/backoff/v4"
16
        oteltrace "go.opentelemetry.io/otel/trace"
17
        "google.golang.org/grpc/codes"
18
        "google.golang.org/grpc/status"
19
        "google.golang.org/protobuf/types/known/timestamppb"
20

21
        extensions_ssh "github.com/pomerium/envoy-custom/api/extensions/filters/network/ssh"
22
        "github.com/pomerium/pomerium/authorize/evaluator"
23
        "github.com/pomerium/pomerium/config"
24
        "github.com/pomerium/pomerium/internal/log"
25
        "github.com/pomerium/pomerium/pkg/grpc/databroker"
26
        identitypb "github.com/pomerium/pomerium/pkg/grpc/identity"
27
        "github.com/pomerium/pomerium/pkg/grpc/session"
28
        "github.com/pomerium/pomerium/pkg/identity"
29
        "github.com/pomerium/pomerium/pkg/policy/criteria"
30
        "github.com/pomerium/pomerium/pkg/ssh/code"
31
        "github.com/pomerium/pomerium/pkg/ssh/portforward"
32
)
33

34
type Evaluator interface {
35
        EvaluateSSH(ctx context.Context, streamID uint64, req *Request) (*evaluator.Result, error)
36
        GetDataBrokerServiceClient() databroker.DataBrokerServiceClient
37
        InvalidateCacheForRecords(context.Context, ...*databroker.Record)
38
}
39

40
type Request struct {
41
        Username         string
42
        Hostname         string
43
        PublicKey        []byte
44
        SessionID        string
45
        SourceAddress    string
46
        SessionBindingID string
47

48
        LogOnlyIfDenied         bool
49
        UseUpstreamTunnelPolicy bool
50
}
51

52
type Auth struct {
53
        evaluator      Evaluator
54
        currentConfig  *atomic.Pointer[config.Config]
55
        tracerProvider oteltrace.TracerProvider
56
        codeIssuer     code.Issuer
57
}
58

59
func NewAuth(
60
        evaluator Evaluator,
61
        currentConfig *atomic.Pointer[config.Config],
62
        tracerProvider oteltrace.TracerProvider,
63
        codeIssuer code.Issuer,
64
) *Auth {
54✔
65
        return &Auth{
54✔
66
                evaluator,
54✔
67
                currentConfig,
54✔
68
                tracerProvider,
54✔
69
                codeIssuer,
54✔
70
        }
54✔
71
}
54✔
72

73
// GetDataBrokerServiceClient implements AuthInterface.
74
func (a *Auth) GetDataBrokerServiceClient() databroker.DataBrokerServiceClient {
173✔
75
        return a.evaluator.GetDataBrokerServiceClient()
173✔
76
}
173✔
77

78
func (a *Auth) HandlePublicKeyMethodRequest(
79
        ctx context.Context,
80
        info StreamAuthInfo,
81
        req *extensions_ssh.PublicKeyMethodRequest,
82
) (PublicKeyAuthMethodResponse, error) {
46✔
83
        resp, err := a.handlePublicKeyMethodRequest(ctx, info, req)
46✔
84
        if err != nil {
47✔
85
                log.Ctx(ctx).Error().Err(err).Msg("ssh publickey auth request error")
1✔
86
                return resp, status.Error(codes.Internal, "internal error")
1✔
87
        }
1✔
88
        return resp, err
45✔
89
}
90

91
func (a *Auth) handlePublicKeyMethodRequest(
92
        ctx context.Context,
93
        info StreamAuthInfo,
94
        req *extensions_ssh.PublicKeyMethodRequest,
95
) (PublicKeyAuthMethodResponse, error) {
48✔
96
        sessionID, err := a.resolveSessionIDFromFingerprint(ctx, req.PublicKeyFingerprintSha256)
48✔
97
        if err != nil {
90✔
98
                if st, ok := status.FromError(err); ok && st.Code() == codes.NotFound {
82✔
99
                        return PublicKeyAuthMethodResponse{
40✔
100
                                Allow:                    publicKeyAllowResponse(req.PublicKey),
40✔
101
                                RequireAdditionalMethods: []string{MethodKeyboardInteractive},
40✔
102
                        }, nil
40✔
103
                }
40✔
104
                return PublicKeyAuthMethodResponse{}, err
2✔
105
        }
106
        bindingID, _ := sessionIDFromFingerprint(req.PublicKeyFingerprintSha256)
6✔
107
        sshreq := &Request{
6✔
108
                Username:         *info.Username,
6✔
109
                Hostname:         *info.Hostname,
6✔
110
                PublicKey:        req.PublicKey,
6✔
111
                SessionID:        sessionID,
6✔
112
                SessionBindingID: bindingID,
6✔
113
                SourceAddress:    info.SourceAddress,
6✔
114
        }
6✔
115
        log.Ctx(ctx).Debug().
6✔
116
                Str("username", *info.Username).
6✔
117
                Str("hostname", *info.Hostname).
6✔
118
                Str("session-id", sessionID).
6✔
119
                Msg("ssh publickey auth request")
6✔
120

6✔
121
        // Special case: internal command (e.g. routes portal).
6✔
122
        if *info.Hostname == "" {
7✔
123
                _, err := session.Get(ctx, a.evaluator.GetDataBrokerServiceClient(), sessionID)
1✔
124
                if status.Code(err) == codes.NotFound {
1✔
UNCOV
125
                        // Require IdP login.
×
UNCOV
126
                        return PublicKeyAuthMethodResponse{
×
UNCOV
127
                                Allow:                    publicKeyAllowResponse(req.PublicKey),
×
UNCOV
128
                                RequireAdditionalMethods: []string{MethodKeyboardInteractive},
×
UNCOV
129
                        }, nil
×
130
                } else if err != nil {
1✔
131
                        return PublicKeyAuthMethodResponse{}, err
×
132
                }
×
133
        }
134

135
        res, err := a.evaluator.EvaluateSSH(ctx, info.StreamID, sshreq)
6✔
136
        if err != nil {
7✔
137
                return PublicKeyAuthMethodResponse{}, err
1✔
138
        }
1✔
139

140
        // Interpret the results of policy evaluation.
141
        if res.HasReason(criteria.ReasonSSHPublickeyUnauthorized) {
6✔
142
                // This public key is not allowed, but the client is free to try a different key.
1✔
143
                return PublicKeyAuthMethodResponse{
1✔
144
                        RequireAdditionalMethods: []string{MethodPublicKey},
1✔
145
                }, nil
1✔
146
        } else if res.HasReason(criteria.ReasonUserUnauthenticated) {
6✔
147
                // Mark public key as allowed, to initiate IdP login flow.
1✔
148
                return PublicKeyAuthMethodResponse{
1✔
149
                        Allow:                    publicKeyAllowResponse(req.PublicKey),
1✔
150
                        RequireAdditionalMethods: []string{MethodKeyboardInteractive},
1✔
151
                }, nil
1✔
152
        } else if res.Allow.Value && !res.Deny.Value {
6✔
153
                // Allowed, no login needed.
2✔
154
                return PublicKeyAuthMethodResponse{
2✔
155
                        Allow: publicKeyAllowResponse(req.PublicKey),
2✔
156
                }, nil
2✔
157
        }
2✔
158
        // Denied, no login needed.
159
        return PublicKeyAuthMethodResponse{}, nil
1✔
160
}
161

162
func publicKeyAllowResponse(publicKey []byte) *extensions_ssh.PublicKeyAllowResponse {
43✔
163
        return &extensions_ssh.PublicKeyAllowResponse{
43✔
164
                PublicKey: publicKey,
43✔
165
                Permissions: &extensions_ssh.Permissions{
43✔
166
                        PermitPortForwarding:  true,
43✔
167
                        PermitAgentForwarding: true,
43✔
168
                        PermitX11Forwarding:   true,
43✔
169
                        PermitPty:             true,
43✔
170
                        PermitUserRc:          true,
43✔
171
                        ValidStartTime:        timestamppb.New(time.Now().Add(-1 * time.Minute)),
43✔
172
                        ValidEndTime:          timestamppb.New(time.Now().Add(1 * time.Hour)),
43✔
173
                },
43✔
174
        }
43✔
175
}
43✔
176

177
func (a *Auth) HandleKeyboardInteractiveMethodRequest(
178
        ctx context.Context,
179
        info StreamAuthInfo,
180
        _ *extensions_ssh.KeyboardInteractiveMethodRequest,
181
        querier KeyboardInteractiveQuerier,
182
) (KeyboardInteractiveAuthMethodResponse, error) {
40✔
183
        resp, err := a.handleKeyboardInteractiveMethodRequest(ctx, info, querier)
40✔
184
        if err != nil {
41✔
185
                log.Ctx(ctx).Error().Err(err).Msg("ssh keyboard-interactive auth request error")
1✔
186
                if _, ok := status.FromError(err); !ok {
1✔
187
                        return resp, status.Error(codes.Internal, err.Error())
×
188
                }
×
189
                return resp, err
1✔
190
        }
191
        return resp, err
39✔
192
}
193

194
func (a *Auth) handleKeyboardInteractiveMethodRequest(
195
        ctx context.Context,
196
        info StreamAuthInfo,
197
        querier KeyboardInteractiveQuerier,
198
) (KeyboardInteractiveAuthMethodResponse, error) {
42✔
199
        if info.PublicKeyAllow.Value == nil {
43✔
200
                // Sanity check: this method is only valid if we already accepted a public key.
1✔
201
                return KeyboardInteractiveAuthMethodResponse{}, errPublicKeyAllowNil
1✔
202
        }
1✔
203

204
        log.Ctx(ctx).Debug().
41✔
205
                Str("username", *info.Username).
41✔
206
                Str("hostname", *info.Hostname).
41✔
207
                Str("publickey-fingerprint", base64.StdEncoding.EncodeToString(info.PublicKeyFingerprintSha256)).
41✔
208
                Msg("ssh keyboard-interactive auth request")
41✔
209

41✔
210
        // Initiate the IdP login flow.
41✔
211
        err := a.handleLogin(ctx, *info.Hostname, info.SourceAddress, info.PublicKeyFingerprintSha256, querier)
41✔
212
        if err != nil {
43✔
213
                return KeyboardInteractiveAuthMethodResponse{}, err
2✔
214
        }
2✔
215

216
        if err := a.EvaluateDelayed(ctx, info); err != nil {
41✔
217
                // Denied.
2✔
218
                return KeyboardInteractiveAuthMethodResponse{}, nil
2✔
219
        }
2✔
220
        // Allowed.
221
        return KeyboardInteractiveAuthMethodResponse{
37✔
222
                Allow: &extensions_ssh.KeyboardInteractiveAllowResponse{},
37✔
223
        }, nil
37✔
224
}
225

226
func (a *Auth) handleLogin(
227
        ctx context.Context,
228
        hostname string,
229
        sourceAddr string,
230
        publicKeyFingerprint []byte,
231
        querier KeyboardInteractiveQuerier,
232
) error {
41✔
233
        bindingKey, err := sessionIDFromFingerprint(publicKeyFingerprint)
41✔
234
        if err != nil {
42✔
235
                return err
1✔
236
        }
1✔
237
        idp, authenticator, err := a.getAuthenticator(ctx, hostname)
40✔
238
        if err != nil {
40✔
239
                return err
×
240
        }
×
241
        cfg := a.currentConfig.Load()
40✔
242
        authURL, _ := cfg.Options.GetInternalAuthenticateURL()
40✔
243
        generatedCode := a.codeIssuer.IssueCode()
40✔
244
        now := timestamppb.Now()
40✔
245

40✔
246
        req := &session.SessionBindingRequest{
40✔
247
                IdpId:     idp.GetId(),
40✔
248
                Key:       bindingKey,
40✔
249
                Protocol:  session.ProtocolSSH,
40✔
250
                State:     session.SessionBindingRequestState_InFlight,
40✔
251
                CreatedAt: now,
40✔
252
                ExpiresAt: timestamppb.New(now.AsTime().Add(code.DefaultCodeTTL)),
40✔
253
                Details: map[string]string{
40✔
254
                        session.DetailSourceAddr: sourceAddr,
40✔
255
                },
40✔
256
        }
40✔
257

40✔
258
        ctxT, ca := context.WithDeadline(ctx, req.ExpiresAt.AsTime())
40✔
259
        defer ca()
40✔
260
        associatedCode, err := a.codeIssuer.AssociateCode(ctxT, generatedCode, req)
40✔
261
        if err != nil {
40✔
262
                return status.Error(codes.Aborted, "failed to associate a code to this session")
×
263
        }
×
264
        var prompt string
40✔
265

40✔
266
        query := &url.Values{}
40✔
267
        query.Add("user_code", string(associatedCode))
40✔
268
        promptURI := authURL.ResolveReference(&url.URL{
40✔
269
                Path:     "/.pomerium/sign_in",
40✔
270
                RawQuery: query.Encode(),
40✔
271
        })
40✔
272
        prompt = promptURI.String()
40✔
273
        _, _ = querier.Prompt(ctxT, &extensions_ssh.KeyboardInteractiveInfoPrompts{
40✔
274
                Name:        "Please sign in with " + authenticator.Name() + " to continue",
40✔
275
                Instruction: prompt,
40✔
276
                Prompts:     nil,
40✔
277
        })
40✔
278

40✔
279
        statusC := a.codeIssuer.OnCodeDecision(ctxT, associatedCode)
40✔
280
        select {
40✔
281
        case <-a.codeIssuer.Done():
×
282
                return status.Error(codes.Internal, "code issuer can no longer process this request")
×
283
        case <-ctxT.Done():
×
284
                return status.Error(codes.Canceled, "authentication request timeout")
×
285
        case st, ok := <-statusC:
40✔
286
                if !ok {
40✔
287
                        return status.Error(codes.DeadlineExceeded, "authentication request cancelled by user or timeout exceeded")
×
288
                }
×
289
                if st.State == session.SessionBindingRequestState_Revoked {
41✔
290
                        return status.Error(codes.PermissionDenied, "user has denied this code")
1✔
291
                }
1✔
292
                if st.BindingKey != bindingKey {
39✔
293
                        return status.Error(codes.Internal, "mismatched binding keys")
×
294
                }
×
295
                ctxca, ca := context.WithTimeout(context.Background(), 30*time.Second)
39✔
296
                defer ca()
39✔
297
                b := backoff.WithContext(backoff.NewExponentialBackOff(), ctxca)
39✔
298
                client := a.evaluator.GetDataBrokerServiceClient()
39✔
299
                err := backoff.Retry(func() error {
78✔
300
                        rec, err := client.Get(ctxca, &databroker.GetRequest{
39✔
301
                                Type: "type.googleapis.com/session.SessionBinding",
39✔
302
                                Id:   bindingKey,
39✔
303
                        })
39✔
304
                        if rec.Record.DeletedAt != nil {
39✔
305
                                return fmt.Errorf("stale record")
×
306
                        }
×
307
                        if err != nil {
39✔
308
                                return err
×
309
                        }
×
310
                        a.evaluator.InvalidateCacheForRecords(ctxca, rec.GetRecord())
39✔
311
                        return nil
39✔
312
                }, b)
313
                if err != nil {
39✔
314
                        return status.Error(codes.Internal, fmt.Sprintf("failed to get matching session binding : %s", err.Error()))
×
315
                }
×
316
                return nil
39✔
317
        }
318
}
319

320
var errAccessDenied = status.Error(codes.PermissionDenied, "access denied")
321

322
func (a *Auth) EvaluateDelayed(ctx context.Context, info StreamAuthInfo) error {
51✔
323
        req, err := a.sshRequestFromStreamAuthInfo(ctx, info)
51✔
324
        if err != nil {
52✔
325
                return err
1✔
326
        }
1✔
327
        res, err := a.evaluator.EvaluateSSH(ctx, info.StreamID, req)
50✔
328
        if err != nil {
50✔
329
                return err
×
330
        }
×
331

332
        if res.Allow.Value && !res.Deny.Value {
93✔
333
                return nil
43✔
334
        }
43✔
335
        return errAccessDenied
7✔
336
}
337

338
// EvaluatePortForward implements AuthInterface.
339
func (a *Auth) EvaluatePortForward(ctx context.Context, info StreamAuthInfo, portForwardInfo portforward.RouteInfo) error {
×
340
        // XXX: temporary stub
×
341
        _ = portForwardInfo
×
342
        req, err := a.sshRequestFromStreamAuthInfo(ctx, info)
×
343
        if err != nil {
×
344
                return err
×
345
        }
×
346
        req.UseUpstreamTunnelPolicy = true
×
347
        res, err := a.evaluator.EvaluateSSH(ctx, info.StreamID, req)
×
348
        if err != nil {
×
349
                return err
×
350
        }
×
351

352
        if res.Allow.Value && !res.Deny.Value {
×
353
                return nil
×
354
        }
×
355
        return errAccessDenied
×
356
}
357

358
func (a *Auth) FormatSession(ctx context.Context, info StreamAuthInfo) ([]byte, error) {
8✔
359
        sessionID, err := a.resolveSessionIDFromFingerprint(ctx, info.PublicKeyFingerprintSha256)
8✔
360
        if err != nil {
9✔
361
                return nil, err
1✔
362
        }
1✔
363
        session, err := session.Get(ctx, a.evaluator.GetDataBrokerServiceClient(), sessionID)
7✔
364
        if err != nil {
7✔
365
                return nil, err
×
366
        }
×
367
        var b bytes.Buffer
7✔
368
        fmt.Fprintf(&b, "User ID:    %s\n", session.UserId)
7✔
369
        fmt.Fprintf(&b, "Session ID: %s\n", sessionID)
7✔
370
        fmt.Fprintf(&b, "Expires at: %s (in %s)\n",
7✔
371
                session.ExpiresAt.AsTime().String(),
7✔
372
                time.Until(session.ExpiresAt.AsTime()).Round(time.Second))
7✔
373
        fmt.Fprintf(&b, "Claims:\n")
7✔
374
        keys := make([]string, 0, len(session.Claims))
7✔
375
        for key := range session.Claims {
63✔
376
                keys = append(keys, key)
56✔
377
        }
56✔
378
        slices.Sort(keys)
7✔
379
        for _, key := range keys {
63✔
380
                fmt.Fprintf(&b, "  %s: ", key)
56✔
381
                vs := session.Claims[key].AsSlice()
56✔
382
                if len(vs) != 1 {
57✔
383
                        b.WriteRune('[')
1✔
384
                }
1✔
385
                if len(vs) == 1 {
111✔
386
                        switch key {
55✔
387
                        case "iat":
6✔
388
                                d, _ := vs[0].(float64)
6✔
389
                                t := time.Unix(int64(d), 0)
6✔
390
                                fmt.Fprintf(&b, "%s (%s ago)", t, time.Since(t).Round(time.Second))
6✔
391
                        case "exp":
6✔
392
                                d, _ := vs[0].(float64)
6✔
393
                                t := time.Unix(int64(d), 0)
6✔
394
                                fmt.Fprintf(&b, "%s (in %s)", t, time.Until(t).Round(time.Second))
6✔
395
                        default:
43✔
396
                                fmt.Fprintf(&b, "%#v", vs[0])
43✔
397
                        }
398
                } else if len(vs) > 1 {
2✔
399
                        for i, v := range vs {
3✔
400
                                fmt.Fprintf(&b, "%#v", v)
2✔
401
                                if i < len(vs)-1 {
3✔
402
                                        b.WriteString(", ")
1✔
403
                                }
1✔
404
                        }
405
                }
406
                if len(vs) != 1 {
57✔
407
                        b.WriteRune(']')
1✔
408
                }
1✔
409
                b.WriteRune('\n')
56✔
410
        }
411
        return b.Bytes(), nil
7✔
412
}
413

414
func (a *Auth) DeleteSession(ctx context.Context, info StreamAuthInfo) error {
8✔
415
        sessionID, err := a.resolveSessionIDFromFingerprint(ctx, info.PublicKeyFingerprintSha256)
8✔
416
        if err != nil {
11✔
417
                return err
3✔
418
        }
3✔
419
        toInvalidate := []*databroker.Record{}
5✔
420
        sessionErr := session.Delete(ctx, a.evaluator.GetDataBrokerServiceClient(), sessionID)
5✔
421
        a.evaluator.InvalidateCacheForRecords(ctx,
5✔
422
                &databroker.Record{
5✔
423
                        Type: "type.googleapis.com/session.Session",
5✔
424
                        Id:   sessionID,
5✔
425
                },
5✔
426
        )
5✔
427
        toInvalidate = append(toInvalidate, &databroker.Record{
5✔
428
                Type: "type.googleapis.com/session.Session",
5✔
429
                Id:   sessionID,
5✔
430
        })
5✔
431

5✔
432
        bindingRecs, bindingErr := a.codeIssuer.RevokeSessionBindingBySession(ctx, sessionID)
5✔
433
        if bindingErr == nil && len(bindingRecs) > 0 {
5✔
434
                toInvalidate = append(toInvalidate, bindingRecs...)
×
435
        }
×
436
        a.evaluator.InvalidateCacheForRecords(ctx, toInvalidate...)
5✔
437
        return errors.Join(sessionErr, bindingErr)
5✔
438
}
439

440
func (a *Auth) getAuthenticator(ctx context.Context, hostname string) (*identitypb.Provider, identity.Authenticator, error) {
40✔
441
        opts := a.currentConfig.Load().Options
40✔
442

40✔
443
        redirectURL, err := opts.GetAuthenticateRedirectURL()
40✔
444
        if err != nil {
40✔
445
                return nil, nil, err
×
446
        }
×
447

448
        idp, err := opts.GetIdentityProviderForPolicy(opts.GetRouteForSSHHostname(hostname))
40✔
449
        if err != nil {
40✔
450
                return nil, nil, err
×
451
        }
×
452

453
        authenticator, err := identity.GetIdentityProvider(ctx, a.tracerProvider, idp, redirectURL,
40✔
454
                opts.RuntimeFlags[config.RuntimeFlagRefreshSessionAtIDTokenExpiration])
40✔
455
        if err != nil {
40✔
456
                return nil, nil, err
×
457
        }
×
458

459
        return idp, authenticator, nil
40✔
460
}
461

462
var _ AuthInterface = (*Auth)(nil)
463

464
var errInvalidFingerprint = errors.New("invalid public key fingerprint")
465

466
func (a *Auth) resolveSessionID(ctx context.Context, sessionBindingID string) (string, error) {
112✔
467
        resp, err := a.evaluator.GetDataBrokerServiceClient().Get(ctx, &databroker.GetRequest{
112✔
468
                Type: "type.googleapis.com/session.SessionBinding",
112✔
469
                Id:   sessionBindingID,
112✔
470
        })
112✔
471
        if err != nil {
151✔
472
                return "", err
39✔
473
        }
39✔
474
        if resp.Record.DeletedAt != nil {
73✔
NEW
475
                return "", status.Error(codes.NotFound, "session binding deleted")
×
476
        }
×
477

478
        var binding session.SessionBinding
73✔
479
        if err := resp.Record.Data.UnmarshalTo(&binding); err != nil {
73✔
NEW
480
                return "", status.Error(codes.Internal, err.Error())
×
481
        }
×
482
        now := time.Now()
73✔
483
        if binding.ExpiresAt.AsTime().Before(now) {
74✔
484
                return "", status.Error(codes.NotFound, "session binding no longer valid")
1✔
485
        }
1✔
486
        if binding.Protocol != session.ProtocolSSH {
72✔
NEW
487
                return "", status.Error(codes.Internal, "invalid protocol")
×
488
        }
×
489
        sessionResp, err := a.evaluator.GetDataBrokerServiceClient().Get(ctx, &databroker.GetRequest{
72✔
490
                Type: "type.googleapis.com/session.Session",
72✔
491
                Id:   binding.SessionId,
72✔
492
        })
72✔
493
        if err != nil {
76✔
494
                return "", err
4✔
495
        }
4✔
496
        if sessionResp.GetRecord().DeletedAt != nil {
68✔
NEW
497
                return "", status.Error(codes.NotFound, "session deleted")
×
498
        }
×
499

500
        return binding.SessionId, nil
68✔
501
}
502

503
func sessionIDFromFingerprint(sha256fingerprint []byte) (string, error) {
212✔
504
        if len(sha256fingerprint) != sha256.Size {
216✔
505
                return "", errInvalidFingerprint
4✔
506
        }
4✔
507
        return "sshkey-SHA256:" + base64.RawStdEncoding.EncodeToString(sha256fingerprint), nil
208✔
508
}
509

510
func (a *Auth) resolveSessionIDFromFingerprint(ctx context.Context, sha256fingerprint []byte) (string, error) {
115✔
511
        id, err := sessionIDFromFingerprint(sha256fingerprint)
115✔
512
        if err != nil {
118✔
513
                return "", err
3✔
514
        }
3✔
515
        return a.resolveSessionID(ctx, id)
112✔
516
}
517

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

520
// Converts from StreamAuthInfo to an SSHRequest, assuming the PublicKeyAllow field is not nil.
521
func (a *Auth) sshRequestFromStreamAuthInfo(ctx context.Context, info StreamAuthInfo) (*Request, error) {
51✔
522
        if info.PublicKeyAllow.Value == nil {
51✔
523
                return nil, errPublicKeyAllowNil
×
524
        }
×
525
        sessionID, err := a.resolveSessionIDFromFingerprint(ctx, info.PublicKeyFingerprintSha256)
51✔
526
        if err != nil {
52✔
527
                return nil, err
1✔
528
        }
1✔
529

530
        bindingID, _ := sessionIDFromFingerprint(info.PublicKeyFingerprintSha256)
50✔
531
        return &Request{
50✔
532
                Username:         *info.Username,
50✔
533
                Hostname:         *info.Hostname,
50✔
534
                PublicKey:        info.PublicKeyAllow.Value.PublicKey,
50✔
535
                SessionID:        sessionID,
50✔
536
                SourceAddress:    info.SourceAddress,
50✔
537
                SessionBindingID: bindingID,
50✔
538

50✔
539
                LogOnlyIfDenied: info.InitialAuthComplete,
50✔
540
        }, nil
50✔
541
}
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