Skip to content

Commit d5bd4b9

Browse files
ajhollandEduardo Gomes
authored andcommitted
Add graceful rotation support for client secrets
Co-authored-by: Alex Holland <aholland@cloudflare.com>
1 parent 57714bf commit d5bd4b9

File tree

3 files changed

+139
-98
lines changed

3 files changed

+139
-98
lines changed

.changelog/4189.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:enhancement
2+
access_service_tokens: Added graceful rotation support for client secrets
3+
```

access_service_tokens.go

Lines changed: 60 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -16,68 +16,79 @@ var (
1616

1717
// AccessServiceToken represents an Access Service Token.
1818
type AccessServiceToken struct {
19-
ClientID string `json:"client_id"`
20-
CreatedAt *time.Time `json:"created_at"`
21-
ExpiresAt *time.Time `json:"expires_at"`
22-
ID string `json:"id"`
23-
Name string `json:"name"`
24-
UpdatedAt *time.Time `json:"updated_at"`
25-
Duration string `json:"duration,omitempty"`
26-
LastSeenAt *time.Time `json:"last_seen_at,omitempty"`
19+
ClientID string `json:"client_id"`
20+
CreatedAt *time.Time `json:"created_at"`
21+
ExpiresAt *time.Time `json:"expires_at"`
22+
ID string `json:"id"`
23+
Name string `json:"name"`
24+
UpdatedAt *time.Time `json:"updated_at"`
25+
Duration string `json:"duration,omitempty"`
26+
LastSeenAt *time.Time `json:"last_seen_at,omitempty"`
27+
ClientSecretVersion int64 `json:"client_secret_version"`
28+
PreviousClientSecretExpiresAt *time.Time `json:"previous_client_secret_expires_at,omitempty"`
2729
}
2830

2931
// AccessServiceTokenUpdateResponse represents the response from the API
3032
// when a new Service Token is updated. This base struct is also used in the
3133
// Create as they are very similar responses.
3234
type AccessServiceTokenUpdateResponse struct {
33-
CreatedAt *time.Time `json:"created_at"`
34-
UpdatedAt *time.Time `json:"updated_at"`
35-
ExpiresAt *time.Time `json:"expires_at"`
36-
ID string `json:"id"`
37-
Name string `json:"name"`
38-
ClientID string `json:"client_id"`
39-
Duration string `json:"duration,omitempty"`
40-
LastSeenAt *time.Time `json:"last_seen_at,omitempty"`
35+
CreatedAt *time.Time `json:"created_at"`
36+
UpdatedAt *time.Time `json:"updated_at"`
37+
ExpiresAt *time.Time `json:"expires_at"`
38+
ID string `json:"id"`
39+
Name string `json:"name"`
40+
ClientID string `json:"client_id"`
41+
ClientSecret string `json:"client_secret,omitempty"`
42+
Duration string `json:"duration,omitempty"`
43+
LastSeenAt *time.Time `json:"last_seen_at,omitempty"`
44+
ClientSecretVersion int64 `json:"client_secret_version"`
45+
PreviousClientSecretExpiresAt *time.Time `json:"previous_client_secret_expires_at,omitempty"`
4146
}
4247

4348
// AccessServiceTokenRefreshResponse represents the response from the API
4449
// when an existing service token is refreshed to last longer.
4550
type AccessServiceTokenRefreshResponse struct {
46-
CreatedAt *time.Time `json:"created_at"`
47-
UpdatedAt *time.Time `json:"updated_at"`
48-
ExpiresAt *time.Time `json:"expires_at"`
49-
ID string `json:"id"`
50-
Name string `json:"name"`
51-
ClientID string `json:"client_id"`
52-
Duration string `json:"duration,omitempty"`
53-
LastSeenAt *time.Time `json:"last_seen_at,omitempty"`
51+
CreatedAt *time.Time `json:"created_at"`
52+
UpdatedAt *time.Time `json:"updated_at"`
53+
ExpiresAt *time.Time `json:"expires_at"`
54+
ID string `json:"id"`
55+
Name string `json:"name"`
56+
ClientID string `json:"client_id"`
57+
Duration string `json:"duration,omitempty"`
58+
LastSeenAt *time.Time `json:"last_seen_at,omitempty"`
59+
ClientSecretVersion int64 `json:"client_secret_version"`
60+
PreviousClientSecretExpiresAt *time.Time `json:"previous_client_secret_expires_at,omitempty"`
5461
}
5562

5663
// AccessServiceTokenCreateResponse is the same API response as the Update
5764
// operation with the exception that the `ClientSecret` is present in a
5865
// Create operation.
5966
type AccessServiceTokenCreateResponse struct {
60-
CreatedAt *time.Time `json:"created_at"`
61-
UpdatedAt *time.Time `json:"updated_at"`
62-
ExpiresAt *time.Time `json:"expires_at"`
63-
ID string `json:"id"`
64-
Name string `json:"name"`
65-
ClientID string `json:"client_id"`
66-
ClientSecret string `json:"client_secret"`
67-
Duration string `json:"duration,omitempty"`
67+
CreatedAt *time.Time `json:"created_at"`
68+
UpdatedAt *time.Time `json:"updated_at"`
69+
ExpiresAt *time.Time `json:"expires_at"`
70+
ID string `json:"id"`
71+
Name string `json:"name"`
72+
ClientID string `json:"client_id"`
73+
ClientSecret string `json:"client_secret"`
74+
Duration string `json:"duration,omitempty"`
75+
ClientSecretVersion int64 `json:"client_secret_version"`
76+
PreviousClientSecretExpiresAt *time.Time `json:"previous_client_secret_expires_at,omitempty"`
6877
}
6978

7079
// AccessServiceTokenRotateResponse is the same API response as the Create
7180
// operation.
7281
type AccessServiceTokenRotateResponse struct {
73-
CreatedAt *time.Time `json:"created_at"`
74-
UpdatedAt *time.Time `json:"updated_at"`
75-
ExpiresAt *time.Time `json:"expires_at"`
76-
ID string `json:"id"`
77-
Name string `json:"name"`
78-
ClientID string `json:"client_id"`
79-
ClientSecret string `json:"client_secret"`
80-
Duration string `json:"duration,omitempty"`
82+
CreatedAt *time.Time `json:"created_at"`
83+
UpdatedAt *time.Time `json:"updated_at"`
84+
ExpiresAt *time.Time `json:"expires_at"`
85+
ID string `json:"id"`
86+
Name string `json:"name"`
87+
ClientID string `json:"client_id"`
88+
ClientSecret string `json:"client_secret"`
89+
Duration string `json:"duration,omitempty"`
90+
ClientSecretVersion int64 `json:"client_secret_version"`
91+
PreviousClientSecretExpiresAt *time.Time `json:"previous_client_secret_expires_at,omitempty"`
8192
}
8293

8394
// AccessServiceTokensListResponse represents the response from the list
@@ -136,14 +147,18 @@ type AccessServiceTokensRotateSecretDetailResponse struct {
136147
type ListAccessServiceTokensParams struct{}
137148

138149
type CreateAccessServiceTokenParams struct {
139-
Name string `json:"name"`
140-
Duration string `json:"duration,omitempty"`
150+
Name string `json:"name"`
151+
Duration string `json:"duration,omitempty"`
152+
ClientSecretVersion int64 `json:"client_secret_version"`
153+
PreviousClientSecretExpiresAt *time.Time `json:"previous_client_secret_expires_at,omitempty"`
141154
}
142155

143156
type UpdateAccessServiceTokenParams struct {
144-
Name string `json:"name"`
145-
UUID string `json:"-"`
146-
Duration string `json:"duration,omitempty"`
157+
Name string `json:"name"`
158+
UUID string `json:"-"`
159+
Duration string `json:"duration,omitempty"`
160+
ClientSecretVersion int64 `json:"client_secret_version"`
161+
PreviousClientSecretExpiresAt *time.Time `json:"previous_client_secret_expires_at,omitempty"`
147162
}
148163

149164
func (api *API) ListAccessServiceTokens(ctx context.Context, rc *ResourceContainer, params ListAccessServiceTokensParams) ([]AccessServiceToken, ResultInfo, error) {

access_service_tokens_test.go

Lines changed: 76 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ func TestAccessServiceTokens(t *testing.T) {
3030
"name": "CI/CD token",
3131
"client_id": "88bf3b6d86161464f6509f7219099e57.access.example.com",
3232
"duration": "8760h",
33-
"last_seen_at": "2014-02-03T06:07:00.12345Z"
33+
"last_seen_at": "2014-02-03T06:07:00.12345Z",
34+
"client_secret_version": 1,
35+
"previous_client_secret_expires_at": "2014-12-01T05:20:00.12345Z"
3436
}
3537
]
3638
}
@@ -41,17 +43,20 @@ func TestAccessServiceTokens(t *testing.T) {
4143
updatedAt, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z")
4244
expiresAt, _ := time.Parse(time.RFC3339, "2015-01-01T05:20:00.12345Z")
4345
lastSeenAt, _ := time.Parse(time.RFC3339, "2014-02-03T06:07:00.12345Z")
46+
previousClientSecretExpiresAt, _ := time.Parse(time.RFC3339, "2014-12-01T05:20:00.12345Z")
4447

4548
want := []AccessServiceToken{
4649
{
47-
CreatedAt: &createdAt,
48-
UpdatedAt: &updatedAt,
49-
ExpiresAt: &expiresAt,
50-
ID: "f174e90a-fafe-4643-bbbc-4a0ed4fc8415",
51-
Name: "CI/CD token",
52-
ClientID: "88bf3b6d86161464f6509f7219099e57.access.example.com",
53-
Duration: "8760h",
54-
LastSeenAt: &lastSeenAt,
50+
CreatedAt: &createdAt,
51+
UpdatedAt: &updatedAt,
52+
ExpiresAt: &expiresAt,
53+
ID: "f174e90a-fafe-4643-bbbc-4a0ed4fc8415",
54+
Name: "CI/CD token",
55+
ClientID: "88bf3b6d86161464f6509f7219099e57.access.example.com",
56+
Duration: "8760h",
57+
LastSeenAt: &lastSeenAt,
58+
ClientSecretVersion: 1,
59+
PreviousClientSecretExpiresAt: &previousClientSecretExpiresAt,
5560
},
5661
}
5762

@@ -91,21 +96,23 @@ func TestCreateAccessServiceToken(t *testing.T) {
9196
"name": "CI/CD token",
9297
"client_id": "88bf3b6d86161464f6509f7219099e57.access.example.com",
9398
"client_secret": "bdd31cbc4dec990953e39163fbbb194c93313ca9f0a6e420346af9d326b1d2a5",
94-
"duration": "8760h"
99+
"duration": "8760h",
100+
"client_secret_version": 1
95101
}
96102
}
97103
`)
98104
}
99105

100106
expected := AccessServiceTokenCreateResponse{
101-
CreatedAt: &createdAt,
102-
UpdatedAt: &updatedAt,
103-
ExpiresAt: &expiresAt,
104-
ID: "f174e90a-fafe-4643-bbbc-4a0ed4fc8415",
105-
Name: "CI/CD token",
106-
ClientID: "88bf3b6d86161464f6509f7219099e57.access.example.com",
107-
ClientSecret: "bdd31cbc4dec990953e39163fbbb194c93313ca9f0a6e420346af9d326b1d2a5",
108-
Duration: "8760h",
107+
CreatedAt: &createdAt,
108+
UpdatedAt: &updatedAt,
109+
ExpiresAt: &expiresAt,
110+
ID: "f174e90a-fafe-4643-bbbc-4a0ed4fc8415",
111+
Name: "CI/CD token",
112+
ClientID: "88bf3b6d86161464f6509f7219099e57.access.example.com",
113+
ClientSecret: "bdd31cbc4dec990953e39163fbbb194c93313ca9f0a6e420346af9d326b1d2a5",
114+
Duration: "8760h",
115+
ClientSecretVersion: 1,
109116
}
110117

111118
mux.HandleFunc("/accounts/"+testAccountID+"/access/service_tokens", handler)
@@ -144,7 +151,8 @@ func TestUpdateAccessServiceToken(t *testing.T) {
144151
"name": "CI/CD token",
145152
"client_id": "88bf3b6d86161464f6509f7219099e57.access.example.com",
146153
"duration": "8760h",
147-
"last_seen_at": "2014-02-03T06:07:00.12345Z"
154+
"last_seen_at": "2014-02-03T06:07:00.12345Z",
155+
"client_secret_version": 1
148156
}
149157
}
150158
`)
@@ -156,14 +164,15 @@ func TestUpdateAccessServiceToken(t *testing.T) {
156164
lastSeenAt, _ := time.Parse(time.RFC3339, "2014-02-03T06:07:00.12345Z")
157165

158166
expected := AccessServiceTokenUpdateResponse{
159-
CreatedAt: &createdAt,
160-
UpdatedAt: &updatedAt,
161-
ExpiresAt: &expiresAt,
162-
ID: "f174e90a-fafe-4643-bbbc-4a0ed4fc8415",
163-
Name: "CI/CD token",
164-
ClientID: "88bf3b6d86161464f6509f7219099e57.access.example.com",
165-
Duration: "8760h",
166-
LastSeenAt: &lastSeenAt,
167+
CreatedAt: &createdAt,
168+
UpdatedAt: &updatedAt,
169+
ExpiresAt: &expiresAt,
170+
ID: "f174e90a-fafe-4643-bbbc-4a0ed4fc8415",
171+
Name: "CI/CD token",
172+
ClientID: "88bf3b6d86161464f6509f7219099e57.access.example.com",
173+
Duration: "8760h",
174+
LastSeenAt: &lastSeenAt,
175+
ClientSecretVersion: 1,
167176
}
168177

169178
mux.HandleFunc("/accounts/"+testAccountID+"/access/service_tokens/f174e90a-fafe-4643-bbbc-4a0ed4fc8415", handler)
@@ -202,7 +211,9 @@ func TestRefreshAccessServiceToken(t *testing.T) {
202211
"name": "CI/CD token",
203212
"client_id": "88bf3b6d86161464f6509f7219099e57.access.example.com",
204213
"duration": "8760h",
205-
"last_seen_at": "2014-02-03T06:07:00.12345Z"
214+
"last_seen_at": "2014-02-03T06:07:00.12345Z",
215+
"client_secret_version": 2,
216+
"previous_client_secret_expires_at": "2014-12-01T05:20:00.12345Z"
206217
}
207218
}
208219
`)
@@ -213,15 +224,19 @@ func TestRefreshAccessServiceToken(t *testing.T) {
213224
expiresAt, _ := time.Parse(time.RFC3339, "2015-01-01T05:20:00.12345Z")
214225
lastSeenAt, _ := time.Parse(time.RFC3339, "2014-02-03T06:07:00.12345Z")
215226

227+
previousClientSecretExpiresAt, _ := time.Parse(time.RFC3339, "2014-12-01T05:20:00.12345Z")
228+
216229
expected := AccessServiceTokenRefreshResponse{
217-
CreatedAt: &createdAt,
218-
UpdatedAt: &updatedAt,
219-
ExpiresAt: &expiresAt,
220-
ID: "f174e90a-fafe-4643-bbbc-4a0ed4fc8415",
221-
Name: "CI/CD token",
222-
ClientID: "88bf3b6d86161464f6509f7219099e57.access.example.com",
223-
Duration: "8760h",
224-
LastSeenAt: &lastSeenAt,
230+
CreatedAt: &createdAt,
231+
UpdatedAt: &updatedAt,
232+
ExpiresAt: &expiresAt,
233+
ID: "f174e90a-fafe-4643-bbbc-4a0ed4fc8415",
234+
Name: "CI/CD token",
235+
ClientID: "88bf3b6d86161464f6509f7219099e57.access.example.com",
236+
Duration: "8760h",
237+
LastSeenAt: &lastSeenAt,
238+
ClientSecretVersion: 2,
239+
PreviousClientSecretExpiresAt: &previousClientSecretExpiresAt,
225240
}
226241

227242
mux.HandleFunc("/accounts/"+testAccountID+"/access/service_tokens/f174e90a-fafe-4643-bbbc-4a0ed4fc8415/refresh", handler)
@@ -252,21 +267,27 @@ func TestRotateAccessServiceToken(t *testing.T) {
252267
"name": "CI/CD token",
253268
"client_id": "88bf3b6d86161464f6509f7219099e57.access.example.com",
254269
"client_secret": "bdd31cbc4dec990953e39163fbbb194c93313ca9f0a6e420346af9d326b1d2a5",
255-
"duration": "8760h"
270+
"duration": "8760h",
271+
"client_secret_version": 2,
272+
"previous_client_secret_expires_at": "2014-12-01T05:20:00.12345Z"
256273
}
257274
}
258275
`)
259276
}
260277

278+
previousClientSecretExpiresAt, _ := time.Parse(time.RFC3339, "2014-12-01T05:20:00.12345Z")
279+
261280
expected := AccessServiceTokenRotateResponse{
262-
CreatedAt: &createdAt,
263-
UpdatedAt: &updatedAt,
264-
ExpiresAt: &expiresAt,
265-
ID: "f174e90a-fafe-4643-bbbc-4a0ed4fc8415",
266-
Name: "CI/CD token",
267-
ClientID: "88bf3b6d86161464f6509f7219099e57.access.example.com",
268-
ClientSecret: "bdd31cbc4dec990953e39163fbbb194c93313ca9f0a6e420346af9d326b1d2a5",
269-
Duration: "8760h",
281+
CreatedAt: &createdAt,
282+
UpdatedAt: &updatedAt,
283+
ExpiresAt: &expiresAt,
284+
ID: "f174e90a-fafe-4643-bbbc-4a0ed4fc8415",
285+
Name: "CI/CD token",
286+
ClientID: "88bf3b6d86161464f6509f7219099e57.access.example.com",
287+
ClientSecret: "bdd31cbc4dec990953e39163fbbb194c93313ca9f0a6e420346af9d326b1d2a5",
288+
Duration: "8760h",
289+
ClientSecretVersion: 2,
290+
PreviousClientSecretExpiresAt: &previousClientSecretExpiresAt,
270291
}
271292

272293
mux.HandleFunc("/accounts/"+testAccountID+"/access/service_tokens/f174e90a-fafe-4643-bbbc-4a0ed4fc8415/rotate", handler)
@@ -296,20 +317,22 @@ func TestDeleteAccessServiceToken(t *testing.T) {
296317
"id": "f174e90a-fafe-4643-bbbc-4a0ed4fc8415",
297318
"name": "CI/CD token",
298319
"client_id": "88bf3b6d86161464f6509f7219099e57.access.example.com",
299-
"duration": "8760h"
320+
"duration": "8760h",
321+
"client_secret_version": 1
300322
}
301323
}
302324
`)
303325
}
304326

305327
expected := AccessServiceTokenUpdateResponse{
306-
CreatedAt: &createdAt,
307-
UpdatedAt: &updatedAt,
308-
ExpiresAt: &expiresAt,
309-
ID: "f174e90a-fafe-4643-bbbc-4a0ed4fc8415",
310-
Name: "CI/CD token",
311-
ClientID: "88bf3b6d86161464f6509f7219099e57.access.example.com",
312-
Duration: "8760h",
328+
CreatedAt: &createdAt,
329+
UpdatedAt: &updatedAt,
330+
ExpiresAt: &expiresAt,
331+
ID: "f174e90a-fafe-4643-bbbc-4a0ed4fc8415",
332+
Name: "CI/CD token",
333+
ClientID: "88bf3b6d86161464f6509f7219099e57.access.example.com",
334+
Duration: "8760h",
335+
ClientSecretVersion: 1,
313336
}
314337

315338
mux.HandleFunc("/accounts/"+testAccountID+"/access/service_tokens/f174e90a-fafe-4643-bbbc-4a0ed4fc8415", handler)

0 commit comments

Comments
 (0)