detached f05fed4

fix(security): fail on invalid JWT instead of sending long-lived token as Bearer

commit
f05fed4d7e98c004cfb49f75742f58c2f7f74706
author
brice · · 2026-03-23 05:18 UTC
parent
2c89614
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +5 -7
M internal/adapters/in/cli/remote/client.go +5 -7
@@ -68,9 +68,9 @@ func NewClient(baseURL string, opts ...ClientOption) *Client {
6868 }
6969
7070 // WithToken sets the authentication token.
71-// If the token is a valid JWT, the subject claim is extracted for use
72-// in ephemeral token exchange. If parsing fails the subject is left
73-// empty and the token will be sent directly (backward compat).
71+// The token must be a valid JWT — the subject claim is extracted for
72+// ephemeral token exchange. If parsing fails, requests will return
73+// an error rather than sending the long-lived token directly.
7474 func WithToken(token string) ClientOption {
7575 return func(c *Client) {
7676 c.token = token
@@ -179,15 +179,13 @@ func (c *Client) exchangeToken(ctx context.Context) error {
179179 }
180180
181181 // bearerToken returns the token to use in Authorization headers.
182-// If the client has a subject (valid JWT), it exchanges for an ephemeral token.
183-// Otherwise it falls back to sending the long-lived token directly.
182+// It exchanges the long-lived token for an ephemeral one via /auth/token.
184183 func (c *Client) bearerToken(ctx context.Context) (string, error) {
185184 if c.token == "" {
186185 return "", nil
187186 }
188- // No subject means we couldn't parse the JWT; send token as-is (backward compat).
189187 if c.subject == "" {
190- return c.token, nil
188+ return "", fmt.Errorf("invalid token: could not extract subject from JWT; regenerate with: gordon auth token generate")
191189 }
192190 c.mu.Lock()
193191 defer c.mu.Unlock()
@@ -68,9 +68,9 @@ func NewClient(baseURL string, opts ...ClientOption) *Client {
68 }68 }
69 69
70 // WithToken sets the authentication token.70 // WithToken sets the authentication token.
71-// If the token is a valid JWT, the subject claim is extracted for use71+// The token must be a valid JWT — the subject claim is extracted for
72-// in ephemeral token exchange. If parsing fails the subject is left72+// ephemeral token exchange. If parsing fails, requests will return
73-// empty and the token will be sent directly (backward compat).73+// an error rather than sending the long-lived token directly.
74 func WithToken(token string) ClientOption {74 func WithToken(token string) ClientOption {
75 return func(c *Client) {75 return func(c *Client) {
76 c.token = token76 c.token = token
@@ -179,15 +179,13 @@ func (c *Client) exchangeToken(ctx context.Context) error {
179 }179 }
180 180
181 // bearerToken returns the token to use in Authorization headers.181 // bearerToken returns the token to use in Authorization headers.
182-// If the client has a subject (valid JWT), it exchanges for an ephemeral token.182+// It exchanges the long-lived token for an ephemeral one via /auth/token.
183-// Otherwise it falls back to sending the long-lived token directly.
184 func (c *Client) bearerToken(ctx context.Context) (string, error) {183 func (c *Client) bearerToken(ctx context.Context) (string, error) {
185 if c.token == "" {184 if c.token == "" {
186 return "", nil185 return "", nil
187 }186 }
188- // No subject means we couldn't parse the JWT; send token as-is (backward compat).
189 if c.subject == "" {187 if c.subject == "" {
190- return c.token, nil188+ return "", fmt.Errorf("invalid token: could not extract subject from JWT; regenerate with: gordon auth token generate")
191 }189 }
192 c.mu.Lock()190 c.mu.Lock()
193 defer c.mu.Unlock()191 defer c.mu.Unlock()

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close