@@ -61,6 +61,166 @@ func TestOAuth2ProviderApps(t *testing.T) {
61
61
})
62
62
}
63
63
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
+
64
224
func TestOAuth2ProviderAppSecrets (t * testing.T ) {
65
225
t .Parallel ()
66
226