feat(api): add return_session login for cross-origin clients

Return the session token in the login response instead of a cookie, and accept it as a Bearer token without CSRF.
This commit is contained in:
justme committed 2026-10-06 16:59:16 +08:00
1 parent 193fc3a95c
commit 91c63d0cf2
5 files changed
+90 -20

No files matched your search

@@ -58,6 +58,39 @@ public sealed class ApiBasicsTests
Assert.Equal("no-store", ok.Headers.CacheControl!.ToString());
}
[Fact]
public async Task Login_WithReturnSession_GivesABearerSession_WithoutACookie_AndNoCsrfNeeded()
{
using HttpClient client = _backend.NewClient();
using HttpResponseMessage login = await client.PostAsync("/api/admin/auth/login", BackendFixture.Json(new JsonObject { ["username"] = BackendFixture.AdminUser, ["password"] = BackendFixture.AdminPassword, ["return_session"] = true }));
Assert.Equal(HttpStatusCode.OK, login.StatusCode);
Assert.False(login.Headers.Contains("Set-Cookie"));
string session = JsonNode.Parse(await login.Content.ReadAsStringAsync())!["session_token"]!.GetValue<string>();
Assert.False(string.IsNullOrEmpty(session));
// 之后当 API 令牌那样用 Bearer;不是 Cookie,所以不需要 CSRF,也能做非 GET 操作
using var me = new HttpRequestMessage(HttpMethod.Get, "/api/admin/auth/me");
me.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", session);
using HttpResponseMessage meResponse = await client.SendAsync(me);
Assert.Equal(HttpStatusCode.OK, meResponse.StatusCode);
Assert.Equal(BackendFixture.AdminUser, JsonNode.Parse(await meResponse.Content.ReadAsStringAsync())!["username"]!.GetValue<string>());
using var create = new HttpRequestMessage(HttpMethod.Post, "/api/admin/join-tokens") { Content = BackendFixture.Json(new JsonObject()) };
create.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", session);
using HttpResponseMessage created = await client.SendAsync(create);
Assert.Equal(HttpStatusCode.Created, created.StatusCode);
// 退出之后这个会话令牌就失效
using var logout = new HttpRequestMessage(HttpMethod.Post, "/api/admin/auth/logout");
logout.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", session);
using HttpResponseMessage loggedOut = await client.SendAsync(logout);
Assert.Equal(HttpStatusCode.NoContent, loggedOut.StatusCode);
using var after = new HttpRequestMessage(HttpMethod.Get, "/api/admin/auth/me");
after.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", session);
using HttpResponseMessage afterResponse = await client.SendAsync(after);
Assert.Equal(HttpStatusCode.Unauthorized, afterResponse.StatusCode);
}
[Fact]
public async Task SessionCookie_RequiresCsrfHeaderOnUnsafeMethods()
{