feat(auth): вход через VK ID и Яндекс ID

Владельцу нужны VK и Яндекс (Google/Discord/GitHub остаются в каталоге и
включаются своими ключами).

VK ID (id.vk.ru, OAuth 2.1):
- обязательный PKCE: code_challenge в запросе авторизации, code_verifier при
  обмене; верификатор выводится из подписанного state (HMAC), хранить нечего;
- device_id из callback уходит в обмен кода;
- для конфиденциальных приложений секрет передаётся как service_token, а не
  client_secret (и обмен идёт параметрами в теле, не Basic);
- профиль: POST /oauth2/user_info с client_id и access_token; права email и
  vkid.personal_info.

Яндекс ID:
- права login:email и login:info; секрет — в теле запроса обмена;
- профиль: GET login.yandex.ru/info?format=json с заголовком
   (не Bearer).

Общее:
- в профиль добавлено DisplayName: VK отдаёт имя и фамилию, Яндекс — real_name,
  раньше они терялись;
- почта считается подтверждённой, если провайдер её отдал (отдельного флага нет,
  адрес приходит только по соответствующему праву) — D-083;
- новые переменные OAUTH_VK_* и OAUTH_YANDEX_* в установщике и .env.example;
- тесты: полные флоу обоих провайдеров на мок-серверах, каталог и ручки.
This commit is contained in:
2026-09-27 17:57:48 +03:00
parent a835903938
commit 2a5d409d19
7 changed files with 524 additions and 26 deletions
+12 -1
View File
@@ -21,7 +21,18 @@ func TestOAuthProviderNotConfigured(t *testing.T) {
t.Fatalf("error code = %q, want oauth.provider_not_configured", code)
}
rec = doJSON(t, srv, http.MethodGet, "/api/v1/auth/oauth/vk/start", "")
// VK и Яндекс есть в каталоге, но без ключей тоже отвечают «не настроен».
for _, provider := range []string{"vk", "yandex"} {
rec = doJSON(t, srv, http.MethodGet, "/api/v1/auth/oauth/"+provider+"/start", "")
if rec.Code != http.StatusNotFound {
t.Fatalf("%s start without config = %d, want 404", provider, rec.Code)
}
if code := errorCodeOf(t, rec); code != "oauth.provider_not_configured" {
t.Fatalf("%s error code = %q, want oauth.provider_not_configured", provider, code)
}
}
rec = doJSON(t, srv, http.MethodGet, "/api/v1/auth/oauth/ok/start", "")
if rec.Code != http.StatusNotFound {
t.Fatalf("unknown provider = %d, want 404", rec.Code)
}