@@ -59,6 +59,166 @@ func TestOAuth2ProviderApps(t *testing.T) {
5959})
6060}
6161
62+ func TestOAuth2ProviderAppBulkRevoke (t * testing.T ) {
63+ t .Parallel ()
64+
65+ t .Run ("ClientCredentialsAppRevocation" ,func (t * testing.T ) {
66+ t .Parallel ()
67+ client := coderdtest .New (t ,nil )
68+ _ = coderdtest .CreateFirstUser (t ,client )
69+ ctx := t .Context ()
70+
71+ // Create an OAuth2 app with client credentials grant type
72+ app ,err := client .PostOAuth2ProviderApp (ctx , codersdk.PostOAuth2ProviderAppRequest {
73+ Name :fmt .Sprintf ("test-revoke-app-%d" ,time .Now ().UnixNano ()),
74+ RedirectURIs : []string {"http://localhost:3000" },
75+ GrantTypes : []codersdk.OAuth2ProviderGrantType {codersdk .OAuth2ProviderGrantTypeClientCredentials },
76+ })
77+ require .NoError (t ,err )
78+
79+ // Create a client secret for the app
80+ secret ,err := client .PostOAuth2ProviderAppSecret (ctx ,app .ID )
81+ require .NoError (t ,err )
82+
83+ // Request a token using client credentials flow with plain HTTP client
84+ httpClient := & http.Client {}
85+ tokenReq ,err := http .NewRequestWithContext (ctx ,http .MethodPost ,client .URL .String ()+ "/oauth2/token" ,strings .NewReader (url.Values {
86+ "grant_type" : []string {"client_credentials" },
87+ "client_id" : []string {app .ID .String ()},
88+ "client_secret" : []string {secret .ClientSecretFull },
89+ }.Encode ()))
90+ require .NoError (t ,err )
91+ tokenReq .Header .Set ("Content-Type" ,"application/x-www-form-urlencoded" )
92+ tokenResp ,err := httpClient .Do (tokenReq )
93+ require .NoError (t ,err )
94+ defer tokenResp .Body .Close ()
95+ require .Equal (t ,http .StatusOK ,tokenResp .StatusCode )
96+
97+ var tokenData struct {
98+ AccessToken string `json:"access_token"`
99+ TokenType string `json:"token_type"`
100+ }
101+ err = json .NewDecoder (tokenResp .Body ).Decode (& tokenData )
102+ require .NoError (t ,err )
103+ require .NotEmpty (t ,tokenData .AccessToken )
104+ require .Equal (t ,"Bearer" ,tokenData .TokenType )
105+
106+ // Verify the token works by making an authenticated request
107+ authReq ,err := http .NewRequestWithContext (ctx ,http .MethodGet ,client .URL .String ()+ "/api/v2/users/me" ,nil )
108+ require .NoError (t ,err )
109+ authReq .Header .Set ("Authorization" ,"Bearer " + tokenData .AccessToken )
110+ authResp ,err := httpClient .Do (authReq )
111+ require .NoError (t ,err )
112+ defer authResp .Body .Close ()
113+ require .Equal (t ,http .StatusOK ,authResp .StatusCode )// Token should work
114+
115+ // Now revoke all tokens for this app using the new bulk revoke endpoint
116+ revokeResp ,err := client .Request (ctx ,http .MethodPost ,fmt .Sprintf ("/api/v2/oauth2-provider/apps/%s/revoke" ,app .ID ),nil )
117+ require .NoError (t ,err )
118+ defer revokeResp .Body .Close ()
119+ require .Equal (t ,http .StatusNoContent ,revokeResp .StatusCode )
120+
121+ // Verify the token no longer works
122+ authReq2 ,err := http .NewRequestWithContext (ctx ,http .MethodGet ,client .URL .String ()+ "/api/v2/users/me" ,nil )
123+ require .NoError (t ,err )
124+ authReq2 .Header .Set ("Authorization" ,"Bearer " + tokenData .AccessToken )
125+
126+ authResp2 ,err := httpClient .Do (authReq2 )
127+ require .NoError (t ,err )
128+ defer authResp2 .Body .Close ()
129+ require .Equal (t ,http .StatusUnauthorized ,authResp2 .StatusCode )// Token should be revoked
130+ })
131+
132+ t .Run ("MultipleTokensRevocation" ,func (t * testing.T ) {
133+ t .Parallel ()
134+ client := coderdtest .New (t ,nil )
135+ _ = coderdtest .CreateFirstUser (t ,client )
136+ ctx := t .Context ()
137+
138+ // Create an OAuth2 app
139+ app ,err := client .PostOAuth2ProviderApp (ctx , codersdk.PostOAuth2ProviderAppRequest {
140+ Name :fmt .Sprintf ("test-multi-revoke-app-%d" ,time .Now ().UnixNano ()),
141+ RedirectURIs : []string {"http://localhost:3000" },
142+ GrantTypes : []codersdk.OAuth2ProviderGrantType {codersdk .OAuth2ProviderGrantTypeClientCredentials },
143+ })
144+ require .NoError (t ,err )
145+
146+ // Create multiple secrets for the app
147+ secret1 ,err := client .PostOAuth2ProviderAppSecret (ctx ,app .ID )
148+ require .NoError (t ,err )
149+ secret2 ,err := client .PostOAuth2ProviderAppSecret (ctx ,app .ID )
150+ require .NoError (t ,err )
151+
152+ // Request multiple tokens using different secrets with plain HTTP client
153+ httpClient := & http.Client {}
154+ var tokens []string
155+ for _ ,secret := range []codersdk.OAuth2ProviderAppSecretFull {secret1 ,secret2 } {
156+ tokenReq ,err := http .NewRequestWithContext (ctx ,http .MethodPost ,client .URL .String ()+ "/oauth2/token" ,strings .NewReader (url.Values {
157+ "grant_type" : []string {"client_credentials" },
158+ "client_id" : []string {app .ID .String ()},
159+ "client_secret" : []string {secret .ClientSecretFull },
160+ }.Encode ()))
161+ require .NoError (t ,err )
162+ tokenReq .Header .Set ("Content-Type" ,"application/x-www-form-urlencoded" )
163+ tokenResp ,err := httpClient .Do (tokenReq )
164+ require .NoError (t ,err )
165+ defer tokenResp .Body .Close ()
166+ require .Equal (t ,http .StatusOK ,tokenResp .StatusCode )
167+
168+ var tokenData struct {
169+ AccessToken string `json:"access_token"`
170+ }
171+ err = json .NewDecoder (tokenResp .Body ).Decode (& tokenData )
172+ require .NoError (t ,err )
173+ tokens = append (tokens ,tokenData .AccessToken )
174+ }
175+
176+ // Verify all tokens work
177+ for _ ,token := range tokens {
178+ authReq ,err := http .NewRequestWithContext (ctx ,http .MethodGet ,client .URL .String ()+ "/api/v2/users/me" ,nil )
179+ require .NoError (t ,err )
180+ authReq .Header .Set ("Authorization" ,"Bearer " + token )
181+
182+ authResp ,err := httpClient .Do (authReq )
183+ require .NoError (t ,err )
184+ defer authResp .Body .Close ()
185+ require .Equal (t ,http .StatusOK ,authResp .StatusCode )
186+ }
187+
188+ // Revoke all tokens for this app using bulk revoke
189+ revokeResp ,err := client .Request (ctx ,http .MethodPost ,fmt .Sprintf ("/api/v2/oauth2-provider/apps/%s/revoke" ,app .ID ),nil )
190+ require .NoError (t ,err )
191+ defer revokeResp .Body .Close ()
192+ require .Equal (t ,http .StatusNoContent ,revokeResp .StatusCode )
193+
194+ // Verify all tokens are now revoked
195+ for _ ,token := range tokens {
196+ authReq ,err := http .NewRequestWithContext (ctx ,http .MethodGet ,client .URL .String ()+ "/api/v2/users/me" ,nil )
197+ require .NoError (t ,err )
198+ authReq .Header .Set ("Authorization" ,"Bearer " + token )
199+
200+ authResp ,err := httpClient .Do (authReq )
201+ require .NoError (t ,err )
202+ defer authResp .Body .Close ()
203+ require .Equal (t ,http .StatusUnauthorized ,authResp .StatusCode )
204+ }
205+ })
206+
207+ t .Run ("AppNotFound" ,func (t * testing.T ) {
208+ t .Parallel ()
209+ client := coderdtest .New (t ,nil )
210+ coderdtest .CreateFirstUser (t ,client )
211+ ctx := t .Context ()
212+
213+ // Try to revoke tokens for non-existent app
214+ fakeAppID := uuid .New ()
215+ revokeResp ,err := client .Request (ctx ,http .MethodPost ,fmt .Sprintf ("/api/v2/oauth2-provider/apps/%s/revoke" ,fakeAppID ),nil )
216+ require .NoError (t ,err )
217+ defer revokeResp .Body .Close ()
218+ require .Equal (t ,http .StatusNotFound ,revokeResp .StatusCode )
219+ })
220+ }
221+
62222func TestOAuth2ProviderAppSecrets (t * testing.T ) {
63223t .Parallel ()
64224