Skip to content

Commit 0bd1c28

Browse files
authored
feat(oauth2): add admin endpoint to regenerate OAuth client secrets (#2170)
## Summary - Add `POST /admin/oauth/clients/{client_id}/regenerate_secret` endpoint - Only allow secret regeneration for confidential clients - Return updated client with new plaintext secret in response
1 parent e6afe45 commit 0bd1c28

File tree

4 files changed

+55
-3
lines changed

4 files changed

+55
-3
lines changed

internal/api/api.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ func NewAPIWithVersion(globalConfig *conf.GlobalConfiguration, db *storage.Conne
348348
r.Use(api.oauthServer.LoadOAuthServerClient)
349349
r.Get("/", api.oauthServer.OAuthServerClientGet)
350350
r.Delete("/", api.oauthServer.OAuthServerClientDelete)
351+
r.Post("/regenerate_secret", api.oauthServer.OAuthServerClientRegenerateSecret)
351352
})
352353
})
353354
})

internal/api/api_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ func TestOAuthServerDisabledByDefault(t *testing.T) {
6262

6363
// OAuth server should be disabled by default
6464
require.False(t, api.config.OAuthServer.Enabled)
65-
65+
6666
// OAuth server instance should not be initialized when disabled
6767
require.Nil(t, api.oauthServer)
6868
}
@@ -78,7 +78,7 @@ func TestOAuthServerCanBeEnabled(t *testing.T) {
7878

7979
// OAuth server should be enabled
8080
require.True(t, api.config.OAuthServer.Enabled)
81-
81+
8282
// OAuth server instance should be initialized when enabled
8383
require.NotNil(t, api.oauthServer)
8484
}

internal/api/oauthserver/handlers.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,27 @@ func (s *Server) OAuthServerClientDelete(w http.ResponseWriter, r *http.Request)
190190
return nil
191191
}
192192

193+
// OAuthServerClientRegenerateSecret handles POST /admin/oauth/clients/{client_id}/regenerate_secret
194+
func (s *Server) OAuthServerClientRegenerateSecret(w http.ResponseWriter, r *http.Request) error {
195+
ctx := r.Context()
196+
client := shared.GetOAuthServerClient(ctx)
197+
198+
// Only confidential clients can have their secrets regenerated
199+
if !client.IsConfidential() {
200+
return apierrors.NewBadRequestError(apierrors.ErrorCodeValidationFailed, "Cannot regenerate secret for public client")
201+
}
202+
203+
updatedClient, plaintextSecret, err := s.regenerateOAuthServerClientSecret(ctx, client.ID)
204+
if err != nil {
205+
return apierrors.NewInternalServerError("Error regenerating OAuth client secret").WithInternalError(err)
206+
}
207+
208+
response := oauthServerClientToResponse(updatedClient)
209+
response.ClientSecret = plaintextSecret
210+
211+
return shared.SendJSON(w, http.StatusOK, response)
212+
}
213+
193214
// OAuthServerClientList handles GET /admin/oauth/clients
194215
func (s *Server) OAuthServerClientList(w http.ResponseWriter, r *http.Request) error {
195216
ctx := r.Context()

internal/api/oauthserver/service.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,6 @@ func validateRedirectURI(uri string) error {
141141
return nil
142142
}
143143

144-
145144
// generateClientSecret generates a secure random client secret
146145
func generateClientSecret() string {
147146
b := make([]byte, 32)
@@ -248,3 +247,34 @@ func (s *Server) deleteOAuthServerClient(ctx context.Context, clientID uuid.UUID
248247

249248
return nil
250249
}
250+
251+
// regenerateOAuthServerClientSecret regenerates a client secret for confidential clients
252+
func (s *Server) regenerateOAuthServerClientSecret(ctx context.Context, clientID uuid.UUID) (*models.OAuthServerClient, string, error) {
253+
db := s.db.WithContext(ctx)
254+
255+
client, err := models.FindOAuthServerClientByID(db, clientID)
256+
if err != nil {
257+
return nil, "", err
258+
}
259+
260+
// Only confidential clients can have their secrets regenerated
261+
if !client.IsConfidential() {
262+
return nil, "", errors.New("cannot regenerate secret for public client")
263+
}
264+
265+
// Generate new client secret
266+
plaintextSecret := generateClientSecret()
267+
hash, err := hashClientSecret(plaintextSecret)
268+
if err != nil {
269+
return nil, "", errors.Wrap(err, "failed to hash client secret")
270+
}
271+
272+
// Update client with new secret hash
273+
client.ClientSecretHash = hash
274+
275+
if err := models.UpdateOAuthServerClient(db, client); err != nil {
276+
return nil, "", errors.Wrap(err, "failed to update OAuth client with new secret")
277+
}
278+
279+
return client, plaintextSecret, nil
280+
}

0 commit comments

Comments
 (0)