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
+59
View File
@@ -664,3 +664,62 @@ func TestOAuthYandexFlow(t *testing.T) {
t.Fatalf("Authorization = %q, want `OAuth …`", infoAuth)
}
}
// TestOAuthVKPublicAppWithoutSecret — публичному приложению VK ID сервисный
// ключ не нужен: вход включается по одному client_id, а запрос обмена не несёт
// ни client_secret, ни service_token (защиту даёт PKCE).
func TestOAuthVKPublicAppWithoutSecret(t *testing.T) {
originalProviders := oauthProviderSet
originalInfoURL := vkUserInfoURL
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
if r.PostForm.Get("service_token") != "" || r.PostForm.Get("client_secret") != "" {
t.Errorf("публичное приложение не должно отправлять секрет: %v", r.PostForm)
}
w.Header().Set("Content-Type", "application/json")
if strings.HasSuffix(r.URL.Path, "/user_info") {
_ = json.NewEncoder(w).Encode(map[string]any{
"user": map[string]any{"user_id": 7, "first_name": "Пётр", "email": "petr@example.com"},
})
return
}
_ = json.NewEncoder(w).Encode(map[string]any{"access_token": "vk-token", "token_type": "Bearer"})
}))
t.Cleanup(server.Close)
providers := defaultOAuthProviders()
vk := providers["vk"]
vk.Endpoint.TokenURL = server.URL + "/token"
providers["vk"] = vk
oauthProviderSet = providers
vkUserInfoURL = server.URL + "/user_info"
t.Cleanup(func() {
oauthProviderSet = originalProviders
vkUserInfoURL = originalInfoURL
})
cfg := oauthTestConfig()
cfg.OAuthVKClientID = "vk-public"
cfg.OAuthVKClientSecret = ""
service, _ := oauthTestService(t, cfg)
if !service.OAuthEnabled() {
t.Fatal("вход через VK должен работать без сервисного ключа")
}
state, err := service.signOAuthState("vk", "/app")
if err != nil {
t.Fatalf("sign state: %v", err)
}
ctx := context.Background()
user, _, _, _, err := service.OAuthCallback(ctx, "vk", "code", state,
url.Values{"device_id": {"device-1"}}, "127.0.0.1", "agent")
if err != nil {
t.Fatalf("OAuthCallback: %v", err)
}
email, err := service.Email(ctx, user.ID)
if err != nil || email != "petr@example.com" {
t.Fatalf("email = %q, err = %v", email, err)
}
}