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

pomerium / pomerium / 18690754649

21 Oct 2025 04:27PM UTC coverage: 53.953% (+0.02%) from 53.929%
18690754649

push

github

web-flow
endpoints: add paths (#5888)

## Summary
Add additional paths to the `endpoints` package.


## Checklist

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

60 of 76 new or added lines in 22 files covered. (78.95%)

8 existing lines in 5 files now uncovered.

27424 of 50829 relevant lines covered (53.95%)

86.61 hits per line

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

0.0
/integration/flows/flows.go
1
// Package flows has helper functions for working with pomerium end-user use-case flows.
2
package flows
3

4
import (
5
        "context"
6
        "fmt"
7
        "io"
8
        "net/http"
9
        "net/http/httptest"
10
        "net/url"
11
        "strconv"
12
        "strings"
13
        "time"
14

15
        "github.com/pomerium/pomerium/integration/forms"
16
        "github.com/pomerium/pomerium/internal/urlutil"
17
        "github.com/pomerium/pomerium/pkg/endpoints"
18
)
19

20
const (
21
        authenticateHostname = "authenticate.localhost.pomerium.io"
22
        idpHostname          = "mock-idp.localhost.pomerium.io"
23
)
24

25
type authenticateConfig struct {
26
        email           string
27
        groups          []string
28
        tokenExpiration time.Duration
29
        apiPath         string
30
        requestHeaders  http.Header
31
}
32

33
// An AuthenticateOption is an option for authentication.
34
type AuthenticateOption func(cfg *authenticateConfig)
35

36
func getAuthenticateConfig(options ...AuthenticateOption) *authenticateConfig {
×
37
        cfg := &authenticateConfig{
×
38
                tokenExpiration: time.Hour * 24,
×
39
                requestHeaders:  http.Header{},
×
40
        }
×
41
        for _, option := range options {
×
42
                if option != nil {
×
43
                        option(cfg)
×
44
                }
×
45
        }
46
        return cfg
×
47
}
48

49
// WithEmail sets the email to use.
50
func WithEmail(email string) AuthenticateOption {
×
51
        return func(cfg *authenticateConfig) {
×
52
                cfg.email = email
×
53
        }
×
54
}
55

56
// WithGroups sets the groups to use.
57
func WithGroups(groups ...string) AuthenticateOption {
×
58
        return func(cfg *authenticateConfig) {
×
59
                cfg.groups = groups
×
60
        }
×
61
}
62

63
// WithTokenExpiration sets the token expiration.
64
func WithTokenExpiration(tokenExpiration time.Duration) AuthenticateOption {
×
65
        return func(cfg *authenticateConfig) {
×
66
                cfg.tokenExpiration = tokenExpiration
×
67
        }
×
68
}
69

70
// WithAPI tells authentication to use API authentication flow.
71
func WithAPI() AuthenticateOption {
×
72
        return func(cfg *authenticateConfig) {
×
NEW
73
                cfg.apiPath = endpoints.PathPomeriumAPILogin
×
74
        }
×
75
}
76

77
func WithRequestHeader(name, value string) AuthenticateOption {
×
78
        return func(cfg *authenticateConfig) {
×
79
                cfg.requestHeaders.Set(name, value)
×
80
        }
×
81
}
82

83
// Authenticate submits a request to a URL, expects a redirect to authenticate and then openid and logs in.
84
// Finally it expects to redirect back to the original page.
85
func Authenticate(ctx context.Context, client *http.Client, url *url.URL, options ...AuthenticateOption) (*http.Response, error) {
×
86
        cfg := getAuthenticateConfig(options...)
×
87
        originalHostname := url.Hostname()
×
88
        var err error
×
89

×
90
        // Serve a local callback for programmatic redirect flow
×
91
        srv := httptest.NewUnstartedServer(http.RedirectHandler(url.String(), http.StatusFound))
×
92
        defer srv.Close()
×
93

×
94
        if cfg.apiPath != "" {
×
95
                srv.Start()
×
96
                apiLogin := url
×
97
                q := apiLogin.Query()
×
98
                q.Set(urlutil.QueryRedirectURI, srv.URL)
×
99
                apiLogin.RawQuery = q.Encode()
×
100

×
101
                apiLogin.Path = cfg.apiPath
×
102
                req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiLogin.String(), nil)
×
103
                if err != nil {
×
104
                        return nil, fmt.Errorf("via-api: invalid request: %w", err)
×
105
                }
×
106
                req.Header.Set("Accept", "application/json")
×
107

×
108
                res, err := client.Do(req)
×
109
                if err != nil {
×
110
                        return nil, fmt.Errorf("via-api: error making request: %w", err)
×
111
                }
×
112
                defer res.Body.Close()
×
113
                bodyBytes, err := io.ReadAll(res.Body)
×
114
                if err != nil {
×
115
                        return nil, fmt.Errorf("via-api: error reading response body: %w", err)
×
116
                }
×
117
                url, err = url.Parse(string(bodyBytes))
×
118
                if err != nil {
×
119
                        return nil, fmt.Errorf("via-api: error parsing response body: %w", err)
×
120
                }
×
121
        }
122

123
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil)
×
124
        if err != nil {
×
125
                return nil, err
×
126
        }
×
127

128
        var res *http.Response
×
129

×
130
        // (1) redirect to authenticate
×
131
        for req.URL.Hostname() == originalHostname {
×
132
                res, err = client.Do(req)
×
133
                if err != nil {
×
134
                        return nil, err
×
135
                }
×
136
                defer res.Body.Close()
×
137

×
138
                req, err = requestFromRedirectResponse(ctx, res, req)
×
139
                if err != nil {
×
140
                        return nil, fmt.Errorf("expected redirect 1 to %s: %w", authenticateHostname, err)
×
141
                }
×
142
        }
143

144
        // (2) redirect to idp
145
        for req.URL.Hostname() == authenticateHostname {
×
146
                res, err = client.Do(req)
×
147
                if err != nil {
×
148
                        return nil, err
×
149
                }
×
150
                defer res.Body.Close()
×
151

×
152
                req, err = requestFromRedirectResponse(ctx, res, req)
×
153
                if err != nil {
×
154
                        return nil, fmt.Errorf("expected redirect 2 to %s: %w", idpHostname, err)
×
155
                }
×
156
        }
157

158
        // (3) submit the form
159
        for req.URL.Hostname() == idpHostname {
×
160
                res, err = client.Do(req)
×
161
                if err != nil {
×
162
                        return nil, err
×
163
                }
×
164
                defer res.Body.Close()
×
165

×
166
                fs := forms.Parse(res.Body)
×
167
                if len(fs) > 0 {
×
168
                        f := fs[0]
×
169
                        f.Inputs["email"] = cfg.email
×
170
                        if len(cfg.groups) > 0 {
×
171
                                f.Inputs["groups"] = strings.Join(cfg.groups, ",")
×
172
                        }
×
173
                        f.Inputs["token_expiration"] = strconv.Itoa(int(cfg.tokenExpiration.Seconds()))
×
174
                        req, err = f.NewRequestWithContext(ctx, req.URL)
×
175
                        if err != nil {
×
176
                                return nil, err
×
177
                        }
×
178
                        addRequestHeaders(req, cfg.requestHeaders)
×
179
                } else {
×
180
                        req, err = requestFromRedirectResponse(ctx, res, req)
×
181
                        if err != nil {
×
182
                                return nil, fmt.Errorf("expected redirect 3 to %s: %w", idpHostname, err)
×
183
                        }
×
184
                }
185
        }
186

187
        // (4) back to authenticate
188
        for req.URL.Hostname() == authenticateHostname {
×
189
                res, err = client.Do(req)
×
190
                if err != nil {
×
191
                        return nil, err
×
192
                }
×
193
                defer res.Body.Close()
×
194

×
195
                req, err = requestFromRedirectResponse(ctx, res, req)
×
196
                if err != nil {
×
197
                        return nil, fmt.Errorf("expected redirect 4 to %s: %w", originalHostname, err)
×
198
                }
×
199
        }
200

201
        // (5) finally to callback
NEW
202
        if req.URL.Path != endpoints.PathPomeriumCallback+"/" {
×
NEW
203
                return nil, fmt.Errorf("expected to redirect 5 back to %s, but got %s", endpoints.PathPomeriumCallback+"/", req.URL.String())
×
UNCOV
204
        }
×
205

206
        res, err = client.Do(req)
×
207
        if err != nil {
×
208
                return nil, err
×
209
        }
×
210
        defer res.Body.Close()
×
211

×
212
        req, err = requestFromRedirectResponse(ctx, res, req)
×
213
        if err != nil {
×
214
                return nil, fmt.Errorf("expected redirect to %s: %w", originalHostname, err)
×
215
        }
×
216

217
        // Programmatic flow: Follow redirect from local callback
218
        if cfg.apiPath != "" {
×
219
                req, err = requestFromRedirectResponse(ctx, res, req)
×
220
                if err != nil {
×
221
                        return nil, fmt.Errorf("expected redirect to %s: %w", srv.URL, err)
×
222
                }
×
223
                res, err = client.Do(req)
×
224
                if err != nil {
×
225
                        return nil, err
×
226
                }
×
227
                req, err = requestFromRedirectResponse(ctx, res, req)
×
228
                if err != nil {
×
229
                        return nil, fmt.Errorf("expected redirect to %s: %w", originalHostname, err)
×
230
                }
×
231
        }
232

233
        return client.Do(req)
×
234
}
235

236
func requestFromRedirectResponse(ctx context.Context, res *http.Response, req *http.Request) (*http.Request, error) {
×
237
        if res.Header.Get("Location") == "" {
×
238
                return nil, fmt.Errorf("no location header found in response headers")
×
239
        }
×
240
        location, err := url.Parse(res.Header.Get("Location"))
×
241
        if err != nil {
×
242
                return nil, fmt.Errorf("error parsing location: %w", err)
×
243
        }
×
244
        location = req.URL.ResolveReference(location)
×
245
        newreq, err := http.NewRequestWithContext(ctx, http.MethodGet, location.String(), nil)
×
246
        if err != nil {
×
247
                return nil, err
×
248
        }
×
249
        addRequestHeaders(newreq, req.Header)
×
250
        return newreq, nil
×
251
}
252

253
func addRequestHeaders(req *http.Request, headers http.Header) {
×
254
        for h := range headers {
×
255
                req.Header[h] = headers[h]
×
256
        }
×
257
}
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