Skip to content

Commit ace2b06

Browse files
authored
Merge pull request #1341 from entireio/auth-context-consolidation
auth: simplify auth/login UX — drop PATs, context-aware status & logout
2 parents dc9a2c7 + f9c7d97 commit ace2b06

19 files changed

Lines changed: 1251 additions & 1051 deletions
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package api
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"net/url"
8+
)
9+
10+
// AuthSession is a single active login session — an OAuth refresh-token family —
11+
// returned by entire-core's session endpoint. One is created per
12+
// `entire login`, across all of a user's devices. Plaintext token values are
13+
// never returned by the server, only metadata. (The list envelope's wire key
14+
// is "tokens"; the rows are sessions.)
15+
type AuthSession struct {
16+
ID string `json:"id"`
17+
UserID string `json:"user_id"`
18+
Name string `json:"name"`
19+
Scope string `json:"scope"`
20+
ExpiresAt string `json:"expires_at"`
21+
LastUsedAt *string `json:"last_used_at"`
22+
CreatedAt string `json:"created_at"`
23+
}
24+
25+
// AuthSessionsResponse is the envelope returned by the list endpoint.
26+
type AuthSessionsResponse struct {
27+
Sessions []AuthSession `json:"tokens"`
28+
}
29+
30+
// errAuthSessionsPathUnset surfaces when a session method is called on a Client
31+
// that wasn't given a base path. Construct via
32+
// NewClientWithBaseURL(...).WithAuthSessionsPath(...).
33+
var errAuthSessionsPathUnset = errors.New("api: auth sessions path is unset (call (*Client).WithAuthSessionsPath before list/revoke)")
34+
35+
func (c *Client) authSessionsBasePath() (string, error) {
36+
if c.authSessionsPath == "" {
37+
return "", errAuthSessionsPathUnset
38+
}
39+
return c.authSessionsPath, nil
40+
}
41+
42+
// ListAuthSessions returns the authenticated user's active login sessions.
43+
func (c *Client) ListAuthSessions(ctx context.Context) ([]AuthSession, error) {
44+
base, err := c.authSessionsBasePath()
45+
if err != nil {
46+
return nil, fmt.Errorf("list sessions: %w", err)
47+
}
48+
resp, err := c.Get(ctx, base)
49+
if err != nil {
50+
return nil, fmt.Errorf("list sessions: %w", err)
51+
}
52+
defer resp.Body.Close()
53+
54+
if err := CheckResponse(resp); err != nil {
55+
return nil, fmt.Errorf("list sessions: %w", err)
56+
}
57+
58+
var out AuthSessionsResponse
59+
if err := DecodeJSON(resp, &out); err != nil {
60+
return nil, fmt.Errorf("list sessions: %w", err)
61+
}
62+
return out.Sessions, nil
63+
}
64+
65+
// RevokeCurrentAuthSession revokes the login session this client is authenticating
66+
// with (the family the current bearer belongs to).
67+
func (c *Client) RevokeCurrentAuthSession(ctx context.Context) error {
68+
base, err := c.authSessionsBasePath()
69+
if err != nil {
70+
return fmt.Errorf("revoke current session: %w", err)
71+
}
72+
resp, err := c.Delete(ctx, base+"/current")
73+
if err != nil {
74+
return fmt.Errorf("revoke current session: %w", err)
75+
}
76+
defer resp.Body.Close()
77+
78+
if err := CheckResponse(resp); err != nil {
79+
return fmt.Errorf("revoke current session: %w", err)
80+
}
81+
return nil
82+
}
83+
84+
// RevokeAuthSession revokes the login session with the given id.
85+
func (c *Client) RevokeAuthSession(ctx context.Context, id string) error {
86+
base, err := c.authSessionsBasePath()
87+
if err != nil {
88+
return fmt.Errorf("revoke session %s: %w", id, err)
89+
}
90+
resp, err := c.Delete(ctx, base+"/"+url.PathEscape(id))
91+
if err != nil {
92+
return fmt.Errorf("revoke session %s: %w", id, err)
93+
}
94+
defer resp.Body.Close()
95+
96+
if err := CheckResponse(resp); err != nil {
97+
return fmt.Errorf("revoke session %s: %w", id, err)
98+
}
99+
return nil
100+
}
Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
"testing"
1010
)
1111

12-
func TestClient_RevokeCurrentToken_SendsDeleteWithBearer(t *testing.T) {
12+
func TestClient_RevokeCurrentAuthSession_SendsDeleteWithBearer(t *testing.T) {
1313
t.Parallel()
1414

1515
var gotMethod, gotPath, gotAuth string
@@ -23,25 +23,25 @@ func TestClient_RevokeCurrentToken_SendsDeleteWithBearer(t *testing.T) {
2323
}))
2424
defer server.Close()
2525

26-
c := NewClient("tok").WithAuthTokensPath("/api/v1/auth/tokens")
26+
c := NewClient("tok").WithAuthSessionsPath("/api/auth/tokens")
2727
c.baseURL = server.URL
2828

29-
if err := c.RevokeCurrentToken(context.Background()); err != nil {
30-
t.Fatalf("RevokeCurrentToken() error = %v", err)
29+
if err := c.RevokeCurrentAuthSession(context.Background()); err != nil {
30+
t.Fatalf("RevokeCurrentAuthSession() error = %v", err)
3131
}
3232

3333
if gotMethod != http.MethodDelete {
3434
t.Errorf("method = %q, want DELETE", gotMethod)
3535
}
36-
if gotPath != "/api/v1/auth/tokens/current" {
37-
t.Errorf("path = %q, want /api/v1/auth/tokens/current", gotPath)
36+
if gotPath != "/api/auth/tokens/current" {
37+
t.Errorf("path = %q, want /api/auth/tokens/current", gotPath)
3838
}
3939
if gotAuth != testBearerHeader {
4040
t.Errorf("Authorization = %q, want %q", gotAuth, testBearerHeader)
4141
}
4242
}
4343

44-
func TestClient_RevokeCurrentToken_ReturnsHTTPErrorOn401(t *testing.T) {
44+
func TestClient_RevokeCurrentAuthSession_ReturnsHTTPErrorOn401(t *testing.T) {
4545
t.Parallel()
4646

4747
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
@@ -51,10 +51,10 @@ func TestClient_RevokeCurrentToken_ReturnsHTTPErrorOn401(t *testing.T) {
5151
}))
5252
defer server.Close()
5353

54-
c := NewClient("tok").WithAuthTokensPath("/api/v1/auth/tokens")
54+
c := NewClient("tok").WithAuthSessionsPath("/api/auth/tokens")
5555
c.baseURL = server.URL
5656

57-
err := c.RevokeCurrentToken(context.Background())
57+
err := c.RevokeCurrentAuthSession(context.Background())
5858
if err == nil {
5959
t.Fatal("expected error for 401 response")
6060
}
@@ -70,7 +70,7 @@ func TestClient_RevokeCurrentToken_ReturnsHTTPErrorOn401(t *testing.T) {
7070
}
7171
}
7272

73-
func TestClient_ListTokens_DecodesResponse(t *testing.T) {
73+
func TestClient_ListAuthSessions_DecodesResponse(t *testing.T) {
7474
t.Parallel()
7575

7676
var gotMethod, gotPath, gotAuth string
@@ -87,19 +87,19 @@ func TestClient_ListTokens_DecodesResponse(t *testing.T) {
8787
}))
8888
defer server.Close()
8989

90-
c := NewClient("tok").WithAuthTokensPath("/api/v1/auth/tokens")
90+
c := NewClient("tok").WithAuthSessionsPath("/api/auth/tokens")
9191
c.baseURL = server.URL
9292

93-
tokens, err := c.ListTokens(context.Background())
93+
tokens, err := c.ListAuthSessions(context.Background())
9494
if err != nil {
95-
t.Fatalf("ListTokens() error = %v", err)
95+
t.Fatalf("ListAuthSessions() error = %v", err)
9696
}
9797

9898
if gotMethod != http.MethodGet {
9999
t.Errorf("method = %q, want GET", gotMethod)
100100
}
101-
if gotPath != "/api/v1/auth/tokens" {
102-
t.Errorf("path = %q, want /api/v1/auth/tokens", gotPath)
101+
if gotPath != "/api/auth/tokens" {
102+
t.Errorf("path = %q, want /api/auth/tokens", gotPath)
103103
}
104104
if gotAuth != testBearerHeader {
105105
t.Errorf("Authorization = %q, want %q", gotAuth, testBearerHeader)
@@ -119,7 +119,7 @@ func TestClient_ListTokens_DecodesResponse(t *testing.T) {
119119
}
120120
}
121121

122-
func TestClient_ListTokens_ReturnsHTTPErrorOn401(t *testing.T) {
122+
func TestClient_ListAuthSessions_ReturnsHTTPErrorOn401(t *testing.T) {
123123
t.Parallel()
124124

125125
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
@@ -129,10 +129,10 @@ func TestClient_ListTokens_ReturnsHTTPErrorOn401(t *testing.T) {
129129
}))
130130
defer server.Close()
131131

132-
c := NewClient("tok").WithAuthTokensPath("/api/v1/auth/tokens")
132+
c := NewClient("tok").WithAuthSessionsPath("/api/auth/tokens")
133133
c.baseURL = server.URL
134134

135-
_, err := c.ListTokens(context.Background())
135+
_, err := c.ListAuthSessions(context.Background())
136136
if err == nil {
137137
t.Fatal("expected error for 401")
138138
}
@@ -141,7 +141,7 @@ func TestClient_ListTokens_ReturnsHTTPErrorOn401(t *testing.T) {
141141
}
142142
}
143143

144-
func TestClient_RevokeToken_SendsDeleteWithEscapedID(t *testing.T) {
144+
func TestClient_RevokeAuthSession_SendsDeleteWithEscapedID(t *testing.T) {
145145
t.Parallel()
146146

147147
var gotMethod, gotEscapedPath, gotDecodedPath string
@@ -155,26 +155,26 @@ func TestClient_RevokeToken_SendsDeleteWithEscapedID(t *testing.T) {
155155
}))
156156
defer server.Close()
157157

158-
c := NewClient("tok").WithAuthTokensPath("/api/v1/auth/tokens")
158+
c := NewClient("tok").WithAuthSessionsPath("/api/auth/tokens")
159159
c.baseURL = server.URL
160160

161161
// Use an id that needs URL escaping to verify we don't blindly concat.
162-
if err := c.RevokeToken(context.Background(), "abc/def 1"); err != nil {
163-
t.Fatalf("RevokeToken() error = %v", err)
162+
if err := c.RevokeAuthSession(context.Background(), "abc/def 1"); err != nil {
163+
t.Fatalf("RevokeAuthSession() error = %v", err)
164164
}
165165

166166
if gotMethod != http.MethodDelete {
167167
t.Errorf("method = %q, want DELETE", gotMethod)
168168
}
169-
if want := "/api/v1/auth/tokens/abc%2Fdef%201"; gotEscapedPath != want {
169+
if want := "/api/auth/tokens/abc%2Fdef%201"; gotEscapedPath != want {
170170
t.Errorf("escaped path = %q, want %q", gotEscapedPath, want)
171171
}
172-
if want := "/api/v1/auth/tokens/abc/def 1"; gotDecodedPath != want {
172+
if want := "/api/auth/tokens/abc/def 1"; gotDecodedPath != want {
173173
t.Errorf("decoded path = %q, want %q", gotDecodedPath, want)
174174
}
175175
}
176176

177-
func TestClient_RevokeToken_ReturnsErrorBody(t *testing.T) {
177+
func TestClient_RevokeAuthSession_ReturnsErrorBody(t *testing.T) {
178178
t.Parallel()
179179

180180
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
@@ -184,10 +184,10 @@ func TestClient_RevokeToken_ReturnsErrorBody(t *testing.T) {
184184
}))
185185
defer server.Close()
186186

187-
c := NewClient("tok").WithAuthTokensPath("/api/v1/auth/tokens")
187+
c := NewClient("tok").WithAuthSessionsPath("/api/auth/tokens")
188188
c.baseURL = server.URL
189189

190-
err := c.RevokeToken(context.Background(), "missing")
190+
err := c.RevokeAuthSession(context.Background(), "missing")
191191
if err == nil {
192192
t.Fatal("expected error for 404")
193193
}

cmd/entire/cli/api/auth_tokens.go

Lines changed: 0 additions & 98 deletions
This file was deleted.

0 commit comments

Comments
 (0)