# Sign-in API

The contract a program is written against. If you are working on
[the MCCP app](https://github.com/DasMaffin/ModdableCaptureCardProvider) - the Moddable Console
Capture Platform - this is the document you need; you do not need to read the site's source. (The
repository still carries the old name in its address, which GitHub redirects; the name in these
documents is the current one.)

**Accounts belong to the site, not to your program.** The same account signs in at
`dasmaffin.com/auth/login` in a browser and here from an application, and a person can bind more than
one way in to it. Your program never sees a password after the moment somebody types one.

Base address: `https://dasmaffin.com`. Everything below hangs off `/api/auth`.

---

## 1. Make an account

```
POST /api/auth/register
Content-Type: application/json

{ "email": "someone@example.com", "password": "a long enough one",
  "displayName": "Someone", "client": "MCCP 1.4 on DESKTOP-1234" }
```

`displayName` is optional. `client` is optional and is worth sending: it is stored against the
refresh token, so a person can later recognise which machine is signed in.

`201` with the same body as signing in, below. A letter goes to the address asking them to confirm
it, and the account is signed in at once either way - confirming is not currently required to use
it. If that changes you will see `403` on a later sign-in rather than a different answer here.

| Code | Meaning |
| --- | --- |
| `400` | Not an email address, or a password shorter than 10 characters |
| `403` | Registration is switched off |
| `409` | That address already has an account |
| `429` | Too many attempts from here; the body carries `retryAfterSeconds` |

---

## 2. Sign in

```
POST /api/auth/login
Content-Type: application/json

{ "email": "someone@example.com", "password": "a long enough one", "client": "MCCP 1.4",
  "code": "123456" }
```

`code` is only wanted by accounts that have set up an authenticator app, and you do not have to
know in advance which those are: send the password on its own, and if a code is needed the refusal
says so. See below.

`200`:

```json
{
  "accessToken": "CfDJ8...",
  "expiresInSeconds": 1800,
  "refreshToken": "kQ7x...",
  "accountId": 41,
  "displayName": "Someone"
}
```

**Store the refresh token, never the password.** The refresh token is what keeps somebody signed in
between runs; the password should not survive the form it was typed into.

| Code | Meaning |
| --- | --- |
| `401` | No such address, or the wrong password. **One answer for both, word for word** - do not try to tell them apart, and do not word your own message as though you could |
| `401` with `"twoFactorRequired": true` | The password was right and a code from their authenticator app is missing or wrong. Ask for the code and post the whole thing again, password included |
| `403` | The address has not been confirmed (only possible once verification is switched on) |
| `429` | Too many attempts; `retryAfterSeconds` says how long |

---

## 3. Use it

```
Authorization: Bearer <accessToken>
```

The access token is good for **thirty minutes** and cannot be withdrawn inside that window, which is
why it is short. Do not try to decode it; treat it as opaque, because its contents are ours to
change.

```
GET /api/auth/me
```

`200` with `accountId`, `kind` (`Steam` or `Password`), `subject`, `displayName`, `verified`. `401`
if the token has expired, was edited, or names a login that has since been deleted.

---

## 4. Stay signed in

```
POST /api/auth/refresh
Content-Type: application/json

{ "refreshToken": "kQ7x...", "client": "MCCP 1.4" }
```

`200` with a fresh pair, exactly like signing in.

**A refresh token works once.** The one you sent is dead the moment this answers, and the answer
carries its replacement - so store the new one before you do anything else with it. A crash between
the two costs somebody a sign-in.

`401` means expired, already used, or signed out. The only thing to do with it is ask for the
password again.

---

## 5. Forgotten password

```
POST /api/auth/forgot
Content-Type: application/json

{ "email": "someone@example.com" }
```

`204`, always. A link to choose a new password is emailed if there is an account with that address,
and the answer is the same when there is not - otherwise this endpoint is a way for anybody to find
out which addresses are registered here.

The reset itself happens on the website, because the link goes to a mailbox and is opened in a
browser. Your program's part is this call and a sentence telling them to go and read their mail.
Once they have, the old password stops working and every refresh token on the account is dead, so
expect a `401` on your next refresh and ask for the password again.

`429` if it is asked too often, with `retryAfterSeconds`.

---

## 6. Sign out

```
POST /api/auth/logout
Content-Type: application/json

{ "refreshToken": "kQ7x..." }
```

`204`, always - including for a token that was never real, because answering differently would let
anybody ask this endpoint whether a token they found is genuine. The access token keeps working
until it expires; that is what a thirty-minute lifetime is for.

---

## Things that will bite you if nobody says them

**A person may have no password at all.** Somebody who signed in through Steam on the website has an
account with a Steam login and nothing else, and `/api/auth/login` will never accept them. They add a
password at `dasmaffin.com/auth/profile` while signed in, and then both work and both lead to the same
`accountId`. If your program has a "sign in" screen and no way to explain that, people who already
have an account will be certain it is broken.

**Handle `twoFactorRequired` or people with an authenticator app cannot sign in at all.** It is the
one thing a `401` here tells you that a wrong password does not. Read it, show a field for six
digits, and post `email`, `password` and `code` together - there is no separate endpoint and nothing
is remembered between the two attempts, so the password has to come with it. A wrong password never
carries this flag, so it can never be used to find out who has a second factor.

**A code works once.** If somebody mistypes and you retry with the same digits, it will be refused
even when it was right. Ask for the current ones.

**`accountId` is the identity. Nothing else is.** Not the email - a person can have several logins on
one account, and the address is one of them. Store the id.

**A password reset ends in a browser, not in your program.** The link is emailed and opened there,
because that is what proves the mailbox is theirs. `POST /api/auth/forgot` is the whole of your side
of it - after that, tell them to go and read their mail.

**Everything is counted.** Ten failures per address and per caller in fifteen minutes, and both
halves are counted at once. Retrying a wrong password in a loop locks the account out for everybody,
including whoever is typing it correctly on the website. Back off on a `429` and show the
`retryAfterSeconds`.

**A password account holds no permissions here, and no module has to accept it.** TGSC refuses them
outright: its pages answer `403` to a password session, because everything it knows about a person is
keyed on a SteamID64. That is a property of that module, not of the account.

**The tokens are not JWTs.** They are opaque strings signed by the site. Nothing in them is readable
and nothing in them should be parsed.
