Read-only mirrors. Development, issues and pull requests are on GitHub.

summaryrefslogtreecommitdiffhomepage
path: root/docs/ACCOUNT-API.md
blob: 022c958fff33958867896723fd998e42f37e4d4c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
<!-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> -->
<!-- SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1 -->

# Account API

The account service is the ASP.NET Core application in `apps/api`. Browsers reach it through the same origin as the
page, under `/api/`; the Next.js server forwards those requests to the service on loopback port 5085.

All bodies are JSON with camel-case property names. Errors use the problem-details format (`application/problem+json`).
Every response carries `Cache-Control: no-store`.

## Summary

| Method and path | Purpose | Rate limited |
| --- | --- | --- |
| `POST /api/v1/accounts/register` | create a password account and sign it in | yes |
| `POST /api/v1/accounts/login` | sign in with email address and password | yes |
| `POST /api/v1/accounts/logout` | end the current session | no |
| `GET /api/v1/accounts/me` | return the signed-in account | no |
| `GET /api/v1/accounts/oauth/google/start` | begin sign-in with Google | yes |
| `GET /api/v1/accounts/oauth/google/complete` | finish sign-in with Google | yes |
| `GET /api/v1/status` | report that the service is online | no |
| `GET /health` | liveness probe for the service manager | no |

## Field rules

| Field | Rule |
| --- | --- |
| `accountName` | 8 to 128 ASCII letters or digits, beginning with an upper-case letter. Unique without regard to case; the spelling the owner chose is kept. `Progmasoft` and `Leitwolf` are reserved in every case. |
| `email` | a single mailbox without a display name, at most 254 characters. Unique without regard to case. Ownership is not verified. |
| `password` | 12 to 256 characters, without a null character. No composition rule; long passphrases are welcome. |

Surrounding white space of an account name or email address is ignored.

## Register

`POST /api/v1/accounts/register`

```json
{ "accountName": "ExampleUser1", "email": "user@example.com", "password": "a long passphrase" }
```

**201 Created.** The session cookie is set, `Location` names the account, and the body is the account:

```json
{ "accountName": "ExampleUser1", "email": "user@example.com", "createdAt": "2026-10-03T12:00:00+00:00" }
```

**400 Bad Request.** A validation problem whose `errors` object names each refused field (`accountName`, `email`,
`password`) with a message. A name or address that is already taken is reported the same way, on its field.

## Sign in

`POST /api/v1/accounts/login`

```json
{ "email": "user@example.com", "password": "a long passphrase" }
```

**200 OK.** The session cookie is set and the body is the account, as above.

**401 Unauthorized.** One message for every failure. The response does not say whether the address or the password
was wrong, and an unknown address costs the same password verification as a wrong password, so timing does not reveal
which addresses are registered.

An account that was created through Google has no password and cannot sign in here.

## Sign out

`POST /api/v1/accounts/logout`

**204 No Content.** The session is revoked and the cookie deleted. The response is the same when no session existed.

## Current account

`GET /api/v1/accounts/me`

**200 OK** with the account, or **401 Unauthorized** when the request has no valid session. An expired session is
revoked when it is seen.

## Sign-in with Google

The flow is a server-side authorization-code flow. The browser is redirected; it never receives Google tokens.

1. `GET /api/v1/accounts/oauth/google/start` sends the browser to Google. When a new account should be created, the
   request carries `?accountName=<name>`. An invalid name redirects back to
   `/register?google=invalid-account-name` without contacting Google.
2. Google returns to `/api/v1/accounts/oauth/google/callback`, which the authentication middleware handles. The
   identity is kept for at most ten minutes in a separate cookie, `__Host-ProgmasoftExternal`.
3. `GET /api/v1/accounts/oauth/google/complete` decides the outcome and removes that cookie.

| Situation | Result |
| --- | --- |
| the Google identity is already linked to an account | signed in; redirect to `/<Account>/dashboard` |
| the identity is new and a valid, free account name was supplied | account created and signed in |
| the identity is new and no account name was supplied | redirect to `/register?google=account-name-required` |
| the supplied account name is invalid | redirect to `/register?google=invalid-account-name` |
| the supplied account name is taken | redirect to `/register?google=account-name-unavailable` |
| the email address of the identity belongs to another account | redirect to `/login?google=failed` |
| Google reported no usable identity, or the callback failed | redirect to `/login?google=failed` |

A Google identity is never linked to an existing account by email address. Password registrations do not verify the
address, so linking by address could give one person's account to another.

The service needs a Google client identifier and secret to run at all; see [Known limitations](LIMITATIONS.md).

## Status and health

`GET /api/v1/status` returns `{ "service": "progmaweb-account", "status": "online", "version": "1.0.0" }`.
`GET /health` is the ASP.NET Core health check for the service manager and is not a public contract.

## Sessions and cookies

| Cookie | Purpose | Attributes |
| --- | --- | --- |
| `__Host-ProgmasoftSession` | the account session | `HttpOnly`, `Secure`, `SameSite=Lax`, `Path=/`, expires with the session |
| `__Host-ProgmasoftExternal` | carries the Google identity between callback and completion | `HttpOnly`, `Secure`, `SameSite=Lax`, ten minutes |

A session token is 256 random bits. The service stores only its SHA-256 digest, so a copy of the store does not
contain a usable token. A session lasts 12 hours by default; `AccountSessions:LifetimeHours` changes that, within 1 to
168 hours.

Passwords are hashed with the ASP.NET Core Identity password hasher and are never stored or logged in plain text.

## Limits

- **Rate limit.** The endpoints marked above share one limit per client address: 12 requests in a sliding window of
  three minutes. A request over the limit gets **429 Too Many Requests**.
- **Body size.** A request body larger than 16 KiB gets **413 Content Too Large**.
- **Hosts.** The service answers only the host names listed in `AllowedHosts`; any other host gets **400**.

The client address comes from `X-Forwarded-For`, which the service accepts from loopback proxies only.

## Configuration

| Setting | Meaning | Default |
| --- | --- | --- |
| `AccountSessions:LifetimeHours` | hours a session stays valid, clamped to 1–168 | `12` |
| `AccountSessions:CookieName` | name of the session cookie | `__Host-ProgmasoftSession` |
| `Authentication:Google:ClientId` | Google OAuth client identifier | none; required |
| `Authentication:Google:ClientSecret` | Google OAuth client secret | none; required |
| `AllowedHosts` | host names the service answers | see `appsettings.json` |
| `ASPNETCORE_URLS` | listener address | `http://127.0.0.1:5085` in production |

Environment variables use a double underscore for the colon, for example `Authentication__Google__ClientId`. Secrets
belong in the process environment or a secret manager and are never committed.

## What the API does not do

It does not verify email addresses, send mail, reset passwords, list accounts or delete accounts. See
[Known limitations](LIMITATIONS.md).