> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hr-easy.nlead.ch/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> How a caller establishes and carries a session against the portal API.

## Sessions

The portal API is **session-based**. A caller authenticates once and carries a session
cookie; there is no separate API key, and there are no long-lived bearer tokens issued
to clients.

| Endpoint                | Purpose                                        |
| ----------------------- | ---------------------------------------------- |
| `GET /api/auth/session` | Returns the current session's user, or `null`. |
| `POST /api/auth/logout` | Ends the session.                              |

<Note>
  These two sit at `/api/auth/…`, not under the `/api/v1/portal/…` prefix the employee endpoints
  use. They are the platform's own session endpoints, included in this contract because the mobile
  client needs them.
</Note>

`GET /api/auth/session` documents **only a `200`**. Its `user` is nullable — an
unauthenticated caller gets `200` with `user: null` rather than a `401` — so a client can
ask "am I signed in?" without treating the answer as an error.

`POST /api/auth/logout` returns `200` with a literal `success: true`, or `500`.

## How a session is established

| Path                     | Available to                                                     |
| ------------------------ | ---------------------------------------------------------------- |
| Microsoft Entra ID (SSO) | Staff with a company account. **The only path for the iOS app.** |
| Magic link               | Web portal only — applicants and new hires without an account.   |

The mobile client is SSO-only by decision: a mobile app holding a long-lived
magic-link session is a materially worse credential story than a token from the
identity provider.

## Cookies

Session and CSRF cookies are `Secure` by default. The flag can be disabled only for
local development served over plain `http://localhost`, and never in a deployed
environment.

## Authorization

Every portal endpoint is **self-scoped**. There is no id parameter to substitute — the
subject is resolved from the session — so the classic IDOR shape does not exist on this
surface.

Two consequences for clients:

* A caller with no linked employee record gets `404` from the employee endpoints, not
  an empty object. The absence of an employment relationship is a distinct state from
  an empty result.
* Salary and compensation fields respect the platform's
  [visibility rules](/guides/people/compensation#salary-visibility). A masked value is
  masked in the response body — there is nothing hidden client-side to reveal.

## Status codes

The contract uses exactly these:

| Code  | Meaning                                                                 |
| ----- | ----------------------------------------------------------------------- |
| `200` | Success.                                                                |
| `201` | Created — a new leave request, sick-leave report or certificate.        |
| `400` | The request failed validation — a malformed path parameter or body.     |
| `401` | The caller is not authenticated.                                        |
| `403` | Authenticated, but not permitted.                                       |
| `404` | No resource for this caller — including "no employee record is linked". |
| `409` | A conflict with current state — for example an overlapping absence.     |
| `500` | An unexpected server error.                                             |

<Warning>
  Tokens and session identifiers never appear in URLs or in `Location:` headers. If you are building
  a client, do not put one there either — every proxy, CDN and access log in the chain records them.
</Warning>
