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