@@ -46,6 +46,8 @@ import (
46
46
"github.com/coder/coder/v2/testutil"
47
47
)
48
48
49
+ type HookRevokeTokenFn func () (httpStatus int ,err error )
50
+
49
51
type token struct {
50
52
issued time.Time
51
53
email string
@@ -196,9 +198,11 @@ type FakeIDP struct {
196
198
// hookValidRedirectURL can be used to reject a redirect url from the
197
199
// IDP -> Application. Almost all IDPs have the concept of
198
200
// "Authorized Redirect URLs". This can be used to emulate that.
199
- hookValidRedirectURL func (redirectURL string )error
200
- hookUserInfo func (email string ) (jwt.MapClaims ,error )
201
- hookAccessTokenJWT func (email string ,exp time.Time ) jwt.MapClaims
201
+ hookValidRedirectURL func (redirectURL string )error
202
+ hookUserInfo func (email string ) (jwt.MapClaims ,error )
203
+ hookRevokeToken HookRevokeTokenFn
204
+ revokeTokenGitHubFormat bool // GitHub doesn't follow token revocation RFC spec
205
+ hookAccessTokenJWT func (email string ,exp time.Time ) jwt.MapClaims
202
206
// defaultIDClaims is if a new client connects and we didn't preset
203
207
// some claims.
204
208
defaultIDClaims jwt.MapClaims
@@ -327,6 +331,19 @@ func WithStaticUserInfo(info jwt.MapClaims) func(*FakeIDP) {
327
331
}
328
332
}
329
333
334
+ func WithRevokeTokenRFC (revokeFunc HookRevokeTokenFn )func (* FakeIDP ) {
335
+ return func (f * FakeIDP ) {
336
+ f .hookRevokeToken = revokeFunc
337
+ }
338
+ }
339
+
340
+ func WithRevokeTokenGitHub (revokeFunc HookRevokeTokenFn )func (* FakeIDP ) {
341
+ return func (f * FakeIDP ) {
342
+ f .hookRevokeToken = revokeFunc
343
+ f .revokeTokenGitHubFormat = true
344
+ }
345
+ }
346
+
330
347
func WithDefaultIDClaims (claims jwt.MapClaims )func (* FakeIDP ) {
331
348
return func (f * FakeIDP ) {
332
349
f .defaultIDClaims = claims
@@ -358,6 +375,7 @@ type With429Arguments struct {
358
375
AuthorizePath bool
359
376
KeysPath bool
360
377
UserInfoPath bool
378
+ RevokePath bool
361
379
DeviceAuth bool
362
380
DeviceVerify bool
363
381
}
@@ -387,6 +405,10 @@ func With429(params With429Arguments) func(*FakeIDP) {
387
405
http .Error (rw ,"429, being manually blocked (userinfo)" ,http .StatusTooManyRequests )
388
406
return
389
407
}
408
+ if params .RevokePath && strings .Contains (r .URL .Path ,revokeTokenPath ) {
409
+ http .Error (rw ,"429, being manually blocked (revoke)" ,http .StatusTooManyRequests )
410
+ return
411
+ }
390
412
if params .DeviceAuth && strings .Contains (r .URL .Path ,deviceAuth ) {
391
413
http .Error (rw ,"429, being manually blocked (device-auth)" ,http .StatusTooManyRequests )
392
414
return
@@ -408,8 +430,10 @@ const (
408
430
authorizePath = "/oauth2/authorize"
409
431
keysPath = "/oauth2/keys"
410
432
userInfoPath = "/oauth2/userinfo"
411
- deviceAuth = "/login/device/code"
412
- deviceVerify = "/login/device"
433
+ // nolint:gosec // It also thinks this is a secret lol
434
+ revokeTokenPath = "/oauth2/revoke"
435
+ deviceAuth = "/login/device/code"
436
+ deviceVerify = "/login/device"
413
437
)
414
438
415
439
func NewFakeIDP (t testing.TB ,opts ... FakeIDPOpt )* FakeIDP {
@@ -486,6 +510,7 @@ func (f *FakeIDP) updateIssuerURL(t testing.TB, issuer string) {
486
510
TokenURL :u .ResolveReference (& url.URL {Path :tokenPath }).String (),
487
511
JWKSURL :u .ResolveReference (& url.URL {Path :keysPath }).String (),
488
512
UserInfoURL :u .ResolveReference (& url.URL {Path :userInfoPath }).String (),
513
+ RevokeURL :u .ResolveReference (& url.URL {Path :revokeTokenPath }).String (),
489
514
DeviceCodeURL :u .ResolveReference (& url.URL {Path :deviceAuth }).String (),
490
515
Algorithms : []string {
491
516
"RS256" ,
@@ -756,6 +781,7 @@ type ProviderJSON struct {
756
781
TokenURL string `json:"token_endpoint"`
757
782
JWKSURL string `json:"jwks_uri"`
758
783
UserInfoURL string `json:"userinfo_endpoint"`
784
+ RevokeURL string `json:"revocation_endpoint"`
759
785
DeviceCodeURL string `json:"device_authorization_endpoint"`
760
786
Algorithms []string `json:"id_token_signing_alg_values_supported"`
761
787
// This is custom
@@ -1146,6 +1172,29 @@ func (f *FakeIDP) httpHandler(t testing.TB) http.Handler {
1146
1172
_ = json .NewEncoder (rw ).Encode (claims )
1147
1173
}))
1148
1174
1175
+ mux .Handle (revokeTokenPath ,http .HandlerFunc (func (rw http.ResponseWriter ,r * http.Request ) {
1176
+ if f .revokeTokenGitHubFormat {
1177
+ u ,p ,ok := r .BasicAuth ()
1178
+ if ! ok || ! (u == f .clientID && p == f .clientSecret ) {
1179
+ httpError (rw ,http .StatusForbidden ,xerrors .Errorf ("basic auth failed" ))
1180
+ return
1181
+ }
1182
+ }else {
1183
+ _ ,ok := validateMW (rw ,r )
1184
+ if ! ok {
1185
+ httpError (rw ,http .StatusForbidden ,xerrors .Errorf ("token validation failed" ))
1186
+ return
1187
+ }
1188
+ }
1189
+
1190
+ code ,err := f .hookRevokeToken ()
1191
+ if err != nil {
1192
+ httpError (rw ,code ,xerrors .Errorf ("hook err: %w" ,err ))
1193
+ return
1194
+ }
1195
+ httpapi .Write (r .Context (),rw ,code ,"" )
1196
+ }))
1197
+
1149
1198
// There is almost no difference between this and /userinfo.
1150
1199
// The main tweak is that this route is "mounted" vs "handle" because "/userinfo"
1151
1200
// should be strict, and this one needs to handle sub routes.
@@ -1474,12 +1523,16 @@ func (f *FakeIDP) ExternalAuthConfig(t testing.TB, id string, custom *ExternalAu
1474
1523
DisplayName :id ,
1475
1524
InstrumentedOAuth2Config :oauthCfg ,
1476
1525
ID :id ,
1526
+ ClientID :f .clientID ,
1527
+ ClientSecret :f .clientSecret ,
1477
1528
// No defaults for these fields by omitting the type
1478
1529
Type :"" ,
1479
1530
DisplayIcon :f .WellknownConfig ().UserInfoURL ,
1480
1531
// Omit the /user for the validate so we can easily append to it when modifying
1481
1532
// the cfg for advanced tests.
1482
- ValidateURL :f .locked .IssuerURL ().ResolveReference (& url.URL {Path :"/external-auth-validate/" }).String (),
1533
+ ValidateURL :f .locked .IssuerURL ().ResolveReference (& url.URL {Path :"/external-auth-validate/" }).String (),
1534
+ RevokeURL :f .locked .IssuerURL ().ResolveReference (& url.URL {Path :revokeTokenPath }).String (),
1535
+ RevokeTimeout :1 * time .Second ,
1483
1536
DeviceAuth :& externalauth.DeviceAuth {
1484
1537
Config :oauthCfg ,
1485
1538
ClientID :f .clientID ,