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

pomerium / pomerium / 19183140028

07 Nov 2025 10:38PM UTC coverage: 55.476% (-0.6%) from 56.094%
19183140028

push

github

web-flow
feat: ssh authorization code flow (#5873)

## Summary

Implements the oauth authorization code flow for native SSH connections
in pomerium.

## Related issues


[ENG-3056](https://linear.app/pomerium/issue/ENG-3056/ssh-authorization-code-flow-initial-implementation)

The issue has a comment explaining some known limitations.

## User Explanation

Allows user to use native SSH without requiring IDPs to support the
device code flow. Native ssh now uses the Authorization code flow, which
most if not all IDPs support - notably, any existing IDP configuration
for non-native ssh Pomerium should now work with native ssh.

## Checklist

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

---------

Co-authored-by: Joe Kralicky <joekralicky@gmail.com>

284 of 1065 new or added lines in 19 files covered. (26.67%)

25 existing lines in 5 files now uncovered.

28737 of 51801 relevant lines covered (55.48%)

94.77 hits per line

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

0.0
/pkg/ssh/code/reader.go
1
package code
2

3
import (
4
        "context"
5

6
        "github.com/cenkalti/backoff/v4"
7
        "github.com/rs/zerolog/log"
8
        "google.golang.org/grpc/codes"
9
        "google.golang.org/grpc/status"
10

11
        "github.com/pomerium/pomerium/pkg/grpc/databroker"
12
        "github.com/pomerium/pomerium/pkg/grpc/session"
13
)
14

15
type reader struct {
16
        client databroker.DataBrokerServiceClient
17
}
18

19
var _ Reader = (*reader)(nil)
20

NEW
21
func NewReader(client databroker.DataBrokerServiceClient) Reader {
×
NEW
22
        return &reader{
×
NEW
23
                client: client,
×
NEW
24
        }
×
NEW
25
}
×
26

NEW
27
func (r *reader) GetBindingRequest(ctx context.Context, id CodeID) (*session.SessionBindingRequest, bool) {
×
NEW
28
        b := backoff.WithContext(backoff.NewExponentialBackOff(), ctx)
×
NEW
29
        resp, err := backoff.RetryWithData(func() (*databroker.GetResponse, error) {
×
NEW
30
                resp, err := r.client.Get(ctx, &databroker.GetRequest{
×
NEW
31
                        Type: "type.googleapis.com/session.SessionBindingRequest",
×
NEW
32
                        Id:   string(id),
×
NEW
33
                })
×
NEW
34
                if st, ok := status.FromError(err); ok && st.Code() == codes.NotFound {
×
NEW
35
                        return nil, backoff.Permanent(err)
×
NEW
36
                }
×
NEW
37
                if err != nil {
×
NEW
38
                        return nil, err
×
NEW
39
                }
×
NEW
40
                return resp, nil
×
41
        }, b)
NEW
42
        if err != nil {
×
NEW
43
                return nil, false
×
NEW
44
        }
×
NEW
45
        s := &session.SessionBindingRequest{}
×
NEW
46
        if err := resp.GetRecord().GetData().UnmarshalTo(s); err != nil {
×
NEW
47
                log.Err(err).Ctx(ctx).Msg("GetBindingRequest: failed to unmarshal session binding request")
×
NEW
48
                return nil, false
×
NEW
49
        }
×
NEW
50
        return s, true
×
51
}
52

NEW
53
func (r *reader) GetSessionByUserID(ctx context.Context, userID string) (map[string]*IdentitySessionPair, error) {
×
NEW
54
        ret := map[string]*IdentitySessionPair{}
×
NEW
55
        filterByUser := indexedFieldFilter("user_id", userID)
×
NEW
56

×
NEW
57
        sessBindingRecs, err := r.client.Query(ctx, &databroker.QueryRequest{
×
NEW
58
                Type:   "type.googleapis.com/session.SessionBinding",
×
NEW
59
                Filter: filterByUser,
×
NEW
60
                Limit:  queryLimit,
×
NEW
61
        })
×
NEW
62

×
NEW
63
        if st, ok := status.FromError(err); ok && st.Code() == codes.NotFound {
×
NEW
64
                return ret, nil
×
NEW
65
        }
×
NEW
66
        if err != nil {
×
NEW
67
                return nil, err
×
NEW
68
        }
×
69

NEW
70
        identityBindingRecs, err := r.client.Query(ctx, &databroker.QueryRequest{
×
NEW
71
                Type:   "type.googleapis.com/session.IdentityBinding",
×
NEW
72
                Filter: filterByUser,
×
NEW
73
                Limit:  queryLimit,
×
NEW
74
        })
×
NEW
75
        if st, ok := status.FromError(err); ok && st.Code() == codes.NotFound {
×
NEW
76
                identityBindingRecs = &databroker.QueryResponse{
×
NEW
77
                        Records: []*databroker.Record{},
×
NEW
78
                }
×
NEW
79
        } else if err != nil {
×
NEW
80
                return nil, err
×
NEW
81
        }
×
82

NEW
83
        for _, sb := range sessBindingRecs.GetRecords() {
×
NEW
84
                var sess session.SessionBinding
×
NEW
85
                if err := sb.GetData().UnmarshalTo(&sess); err != nil {
×
NEW
86
                        log.Err(err).Ctx(ctx).Msg("GetSessionByUserID: failed to unmarshal session binding")
×
NEW
87
                        continue
×
88
                }
NEW
89
                sessionID := sb.GetId()
×
NEW
90
                ret[sessionID] = &IdentitySessionPair{
×
NEW
91
                        SB: &sess,
×
NEW
92
                }
×
93
        }
94

NEW
95
        for _, ib := range identityBindingRecs.GetRecords() {
×
NEW
96
                var ident session.IdentityBinding
×
NEW
97
                if err := ib.GetData().UnmarshalTo(&ident); err != nil {
×
NEW
98
                        log.Err(err).Ctx(ctx).Msg("GetSessionByUserID: failed to unmarshal identity binding")
×
NEW
99
                        continue
×
100
                }
NEW
101
                sessionID := ib.GetId()
×
NEW
102
                val, ok := ret[sessionID]
×
NEW
103
                if !ok {
×
NEW
104
                        val = &IdentitySessionPair{}
×
NEW
105
                }
×
NEW
106
                val.IB = &ident
×
107
        }
NEW
108
        return ret, nil
×
109
}
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