feat(auth): VK ID работает и как публичное приложение

Публичному приложению VK ID сервисный ключ не нужен: обмен кода защищает PKCE,
и VK выдаёт токен по одному client_id. Раньше провайдер включался только при
заполненных id и секрете, то есть публичный вариант был недоступен.

- у провайдера появился флаг SecretOptional (у VK ID — true);
- включение провайдера и каталог учитывают флаг;
- тест TestOAuthVKPublicAppWithoutSecret: вход работает без секрета, в запросе
  обмена нет ни client_secret, ни service_token.
This commit is contained in:
2026-09-27 17:59:49 +03:00
parent 00d86bb70c
commit c88f774749
2 changed files with 75 additions and 3 deletions
+16 -3
View File
@@ -60,6 +60,9 @@ type oauthProvider struct {
// DeviceIDParam — имя query-параметра, который провайдер возвращает вместе с
// кодом и требует при обмене кода на токен (у VK ID это device_id).
DeviceIDParam string
// SecretOptional — провайдер работает и без секрета (публичное приложение
// VK ID: обмен защищён PKCE, сервисный ключ нужен только конфиденциальным).
SecretOptional bool
// TokenSecretParam — имя POST-параметра для секрета при обмене кода.
// Пусто — секрет уходит обычной Basic-авторизацией (как у GitHub/Google).
// У VK ID это service_token: он нужен конфиденциальным приложениям, а
@@ -114,6 +117,7 @@ func defaultOAuthProviders() map[string]oauthProvider {
// VK ID без PKCE не работает: code_challenge обязателен в запросе
// авторизации, device_id возвращается в callback и нужен при обмене.
PKCE: true,
SecretOptional: true,
DeviceIDParam: "device_id",
TokenSecretParam: "service_token",
FetchProfile: fetchVKProfile,
@@ -171,7 +175,10 @@ func (s *Service) oauthConfigs() map[string]oauthProvider {
enabled := make(map[string]oauthProvider, len(providers))
for id, provider := range providers {
credentials, ok := s.oauth[id]
if !ok || credentials.clientID == "" || credentials.clientSecret == "" {
if !ok || credentials.clientID == "" {
continue
}
if credentials.clientSecret == "" && !provider.SecretOptional {
continue
}
provider.Scopes = append([]string{}, provider.Scopes...)
@@ -847,9 +854,15 @@ func oauthGetJSON(ctx context.Context, client *http.Client, token *oauth2.Token,
func oauthFromConfig(cfg config.Config, baseURL string) (map[string]oauthClientCredentials, string) {
credentials := map[string]oauthClientCredentials{}
add := func(id, clientID, clientSecret string) {
if clientID != "" && clientSecret != "" {
credentials[id] = oauthClientCredentials{clientID: clientID, clientSecret: clientSecret}
provider, known := oauthProviders()[id]
if clientID == "" {
return
}
// Публичному приложению VK ID секрет не нужен: обмен защищает PKCE.
if clientSecret == "" && (!known || !provider.SecretOptional) {
return
}
credentials[id] = oauthClientCredentials{clientID: clientID, clientSecret: clientSecret}
}
add("vk", cfg.OAuthVKClientID, cfg.OAuthVKClientSecret)
add("yandex", cfg.OAuthYandexClientID, cfg.OAuthYandexClientSecret)