7
7
8
8
"github.com/catalystgo/catalyst-forge/lib/foundry/httpkit"
9
9
"github.com/gin-gonic/gin"
10
- "github.com/input-output-hk/catalyst-forge/services/api/internal/authkit/authkit "
10
+ "github.com/input-output-hk/catalyst-forge/services/api/internal/authkit"
11
11
"github.com/input-output-hk/catalyst-forge/services/api/internal/authkit/rate"
12
12
"github.com/input-output-hk/catalyst-forge/services/api/internal/authkit/service"
13
13
"github.com/input-output-hk/catalyst-forge/services/api/internal/authkit/store"
@@ -23,7 +23,7 @@ type DeviceLinkDeps struct {
23
23
// RegisterDeviceLink registers all device-link endpoints.
24
24
func RegisterDeviceLink (r * gin.Engine , deps DeviceLinkDeps ) {
25
25
api := r .Group ("/api/v1/auth/device-link" )
26
-
26
+
27
27
// @Summary Begin device link flow
28
28
// @Description Initiates a device authorization flow for CLI/device authentication
29
29
// @Tags auth
@@ -35,7 +35,7 @@ func RegisterDeviceLink(r *gin.Engine, deps DeviceLinkDeps) {
35
35
// @Failure 500 {object} httpkit.ErrorResponse "Internal server error"
36
36
// @Router /api/v1/auth/device-link/begin [post]
37
37
api .POST ("/begin" , beginDeviceLinkHandler (deps .DeviceLinkService , deps .RateLimiter ))
38
-
38
+
39
39
// @Summary Authorize device link
40
40
// @Description Authorizes a pending device link request (requires authentication and step-up)
41
41
// @Tags auth
@@ -49,7 +49,7 @@ func RegisterDeviceLink(r *gin.Engine, deps DeviceLinkDeps) {
49
49
// @Failure 428 {object} httpkit.ErrorResponse "Step-up authentication required"
50
50
// @Router /api/v1/auth/device-link/authorize [post]
51
51
api .POST ("/authorize" , authorizeDeviceLinkHandler (deps .DeviceLinkService ))
52
-
52
+
53
53
// @Summary Exchange device code for tokens
54
54
// @Description Exchanges a device code for access and refresh tokens (polling endpoint)
55
55
// @Tags auth
@@ -90,7 +90,7 @@ func beginDeviceLinkHandler(linkService service.DeviceLinkService, limiter rate.
90
90
return func (c * gin.Context ) {
91
91
// Get client IP for rate limiting
92
92
clientIP := getClientIP (c )
93
-
93
+
94
94
// Check rate limit
95
95
if limiter != nil {
96
96
allowed , err := limiter .AllowBegin (c .Request .Context (), clientIP )
@@ -99,33 +99,33 @@ func beginDeviceLinkHandler(linkService service.DeviceLinkService, limiter rate.
99
99
return
100
100
}
101
101
}
102
-
102
+
103
103
var req struct {
104
104
DeviceName string `json:"device_name"`
105
105
Purpose string `json:"purpose"` // "login" or "step_up"
106
106
}
107
-
107
+
108
108
if err := httpkit .ParseJSON (c .Writer , c .Request , & req ); err != nil {
109
109
return
110
110
}
111
-
111
+
112
112
// Default device name if not provided
113
113
if req .DeviceName == "" {
114
114
req .DeviceName = "CLI Device"
115
115
}
116
-
116
+
117
117
// Default purpose to login
118
118
if req .Purpose == "" {
119
119
req .Purpose = "login"
120
120
}
121
-
121
+
122
122
// Begin the device link flow
123
123
resp , err := linkService .BeginDeviceLink (c .Request .Context (), req .DeviceName , req .Purpose )
124
124
if err != nil {
125
125
_ = httpkit .NewInternalError ().Write (c .Writer )
126
126
return
127
127
}
128
-
128
+
129
129
_ = httpkit .WriteJSON (c .Writer , http .StatusOK , resp )
130
130
}
131
131
}
@@ -139,33 +139,33 @@ func authorizeDeviceLinkHandler(linkService service.DeviceLinkService) gin.Handl
139
139
_ = httpkit .NewUnauthorizedError ("authentication required" ).Write (c .Writer )
140
140
return
141
141
}
142
-
142
+
143
143
// Check for fresh step-up
144
144
if ctx .RequiresStepUp (time .Now ().UTC ()) {
145
145
httpkit .ErrorResponse (c .Writer , http .StatusPreconditionRequired , "step_up_required" , "Fresh authentication required" )
146
146
return
147
147
}
148
-
148
+
149
149
var req struct {
150
150
UserCode string `json:"user_code"`
151
151
}
152
-
152
+
153
153
if err := httpkit .ParseJSON (c .Writer , c .Request , & req ); err != nil {
154
154
return
155
155
}
156
-
156
+
157
157
if req .UserCode == "" {
158
158
_ = httpkit .NewBadRequestError ("user_code is required" ).Write (c .Writer )
159
159
return
160
160
}
161
-
161
+
162
162
// Authorize the device link
163
163
err := linkService .AuthorizeDeviceLink (c .Request .Context (), req .UserCode , ctx .UserID )
164
164
if err != nil {
165
165
_ = httpkit .NewBadRequestError ("invalid or expired code" ).Write (c .Writer )
166
166
return
167
167
}
168
-
168
+
169
169
c .Status (http .StatusNoContent )
170
170
}
171
171
}
@@ -176,24 +176,24 @@ func exchangeDeviceCodeHandler(linkService service.DeviceLinkService, limiter ra
176
176
var req struct {
177
177
DeviceCode string `json:"device_code"`
178
178
}
179
-
179
+
180
180
if err := httpkit .ParseJSON (c .Writer , c .Request , & req ); err != nil {
181
181
return
182
182
}
183
-
183
+
184
184
if req .DeviceCode == "" {
185
185
_ = httpkit .NewBadRequestError ("device_code is required" ).Write (c .Writer )
186
186
return
187
187
}
188
-
188
+
189
189
// Check rate limit
190
190
if limiter != nil {
191
191
allowed , err := limiter .AllowExchange (c .Request .Context (), req .DeviceCode )
192
192
if err != nil {
193
193
// Check if it's a slow_down error
194
194
if err .Error () == "slow_down" {
195
195
_ = httpkit .WriteJSON (c .Writer , http .StatusBadRequest , map [string ]string {
196
- "error" : "slow_down" ,
196
+ "error" : "slow_down" ,
197
197
"error_description" : "You are polling too frequently" ,
198
198
})
199
199
return
@@ -202,45 +202,45 @@ func exchangeDeviceCodeHandler(linkService service.DeviceLinkService, limiter ra
202
202
if ! allowed {
203
203
_ = limiter .RecordSlowDown (c .Request .Context (), req .DeviceCode )
204
204
_ = httpkit .WriteJSON (c .Writer , http .StatusBadRequest , map [string ]string {
205
- "error" : "slow_down" ,
205
+ "error" : "slow_down" ,
206
206
"error_description" : "You are polling too frequently" ,
207
207
})
208
208
return
209
209
}
210
210
}
211
-
211
+
212
212
// Exchange the device code
213
213
resp , err := linkService .ExchangeDeviceCode (c .Request .Context (), req .DeviceCode )
214
214
if err != nil {
215
215
_ = httpkit .NewInternalError ().Write (c .Writer )
216
216
return
217
217
}
218
-
218
+
219
219
// Check status for pending/expired responses
220
220
if resp .Status != "" {
221
221
// Return status codes for polling clients
222
222
switch resp .Status {
223
223
case "authorization_pending" :
224
224
_ = httpkit .WriteJSON (c .Writer , http .StatusBadRequest , map [string ]string {
225
- "error" : "authorization_pending" ,
225
+ "error" : "authorization_pending" ,
226
226
"error_description" : "The authorization request is still pending" ,
227
227
})
228
228
case "slow_down" :
229
229
_ = httpkit .WriteJSON (c .Writer , http .StatusBadRequest , map [string ]string {
230
- "error" : "slow_down" ,
230
+ "error" : "slow_down" ,
231
231
"error_description" : "You are polling too frequently" ,
232
232
})
233
233
case "expired_token" :
234
234
_ = httpkit .WriteJSON (c .Writer , http .StatusBadRequest , map [string ]string {
235
- "error" : "expired_token" ,
235
+ "error" : "expired_token" ,
236
236
"error_description" : "The device code has expired" ,
237
237
})
238
238
default :
239
239
_ = httpkit .NewInternalError ().Write (c .Writer )
240
240
}
241
241
return
242
242
}
243
-
243
+
244
244
// Success - reset rate limit and return tokens
245
245
if limiter != nil {
246
246
_ = limiter .ResetExchange (c .Request .Context (), req .DeviceCode )
@@ -267,13 +267,13 @@ func RegisterDeviceLinkVerify(r *gin.Engine, linkStore store.DeviceLinkStore) {
267
267
_ = httpkit .NewBadRequestError ("code parameter is required" ).Write (c .Writer )
268
268
return
269
269
}
270
-
270
+
271
271
link , err := service .VerifyDeviceCode (c .Request .Context (), linkStore , userCode )
272
272
if err != nil {
273
273
_ = httpkit .NewBadRequestError ("invalid or expired code" ).Write (c .Writer )
274
274
return
275
275
}
276
-
276
+
277
277
_ = httpkit .WriteJSON (c .Writer , http .StatusOK , map [string ]interface {}{
278
278
"device_name" : link .DeviceName ,
279
279
"purpose" : link .Purpose ,
@@ -301,12 +301,12 @@ func getClientIP(c *gin.Context) string {
301
301
}
302
302
return strings .TrimSpace (xff )
303
303
}
304
-
304
+
305
305
// Try X-Real-IP header
306
306
if xri := c .GetHeader ("X-Real-IP" ); xri != "" {
307
307
return xri
308
308
}
309
-
309
+
310
310
// Fall back to remote address
311
311
return c .ClientIP ()
312
- }
312
+ }
0 commit comments