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