@@ -59,6 +59,166 @@ func TestOAuth2ProviderApps(t *testing.T) {
59
59
})
60
60
}
61
61
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
+
62
222
func TestOAuth2ProviderAppSecrets (t * testing.T ) {
63
223
t .Parallel ()
64
224