Authentication¶
The AppGantry API authenticates every request through one of four mechanisms, picked by route:
| Mechanism | For | Carried as |
|---|---|---|
| Access JWT | Developers in the web app or programmatic clients | Authorization: Bearer <jwt> or dfdeveloper cookie |
| Project access token | CI and other automation | Authorization: Bearer ag_prj_... |
| Personal access token | Individual scripts, SDKs, interactive tooling | Authorization: Bearer ag_pat_... |
| Invite token | Testers installing builds | invite_token in the URL path |
The web app sets cookies and uses JWTs; CI uses project access tokens; personal access tokens cover individual scripting; testers use single-use install links.
Login¶
POST /api/v1/login/email
Content-Type: application/json
{"email": "alice@example.com", "password": "..."}
On success the body returns both tokens for programmatic clients:
…and the same two values are set as cookies for the web app:
| Cookie | Lifetime | Scope |
|---|---|---|
dfdeveloper |
TOKEN_LIFETIME_IN_SECONDS + 10 (≈10 minutes) |
Whole API |
dfdeveloper_refresh |
Long-lived | Path-scoped to /api/v1/login |
Both cookies are HttpOnly, SameSite=Lax, and Secure outside local
dev.
Access JWT¶
| Property | Value |
|---|---|
| Algorithm | RS256 |
Issuer (iss) |
appgantry (prod) or appgantry-local (dev) |
Audience (aud) |
appgantry-api |
| Lifetime | 10 minutes |
| Required claims | sub, email, iss, aud, iat, exp |
A token missing any required claim is rejected at verification (401), not silently defaulted.
Refresh¶
…or, for programmatic clients:
Returns a fresh {token, refresh_token} pair. Refresh tokens are
single-use. The returned refresh_token replaces the one you used.
Logout¶
Logout sets the developer's min-issued-at to now. Every JWT
issued before that moment is rejected by every subsequent
verify_identity call. There's no per-token revocation list, just a
single per-developer floor. This means a logout instantly invalidates
all outstanding sessions for that developer.
Project access tokens¶
Project access tokens are the credential for CI and other automation. They are bound to a single project (and its organization), carry a project role, and are owned by the project rather than by any developer, so they outlive personnel changes.
| Property | Value |
|---|---|
| Format | ag_prj_<random> |
| Bound to | One project + its organization |
| Grants | A project role, capped at the creator's own role |
| Lifetime | Until revoked (or the expiry you set at creation) |
| Use in | CI, build pipelines, automation |
Create, rotate, and revoke project access tokens from a project's Access tokens page in the web app (see CI with project access tokens).
Personal access tokens¶
Personal access tokens are bound to a developer and an organization, carry a fixed set of scopes, and survive password rotations. They suit individual scripting and interactive tooling; for CI, prefer a project access token.
| Property | Value |
|---|---|
| Format | ag_pat_<random> |
| Bound to | One developer + one organization |
| Lifetime | Until revoked (or the expiry you set at creation) |
| Use in | Individual scripts, SDKs, interactive tooling |
Create and revoke PATs from Personal access tokens in the web app.
Where long-lived tokens aren't accepted¶
Both project and personal access tokens are deliberately rejected on a few surfaces:
- BYOSA management routes: setup, diagnose, teardown require an interactive login (a fresh JWT). Long-lived tokens are too broad for the blast radius BYOSA management carries.
- Account-level routes: password change, email change, MFA enrollment require a JWT.
- Token management routes: creating, rotating, or revoking project access tokens requires an interactive developer session, so a project access token cannot manage other tokens.
These return HTTP 403 with error: "operation_not_allowed" and a
message explaining which credential is expected.
Email verification¶
New accounts must verify their email before they can do anything state-changing. While unverified, the API returns:
…with HTTP 403. Read-only routes (list orgs you're a member of, see your own profile) still work.
Resend the verification email with:
POST /api/v1/developers/resend-verification
Content-Type: application/json
{"email": "alice@example.com"}
The verification landing page is served at:
The token in the email link is one-time and time-bound.
Password reset¶
Two routes:
POST /api/v1/developers/reset_password_request
Content-Type: application/json
{"email": "alice@example.com"}
The response is always 204, regardless of whether the email is on file - this prevents account enumeration. If the email is registered, a reset email goes out.
POST /api/v1/developers/reset_password
Content-Type: application/json
{"token": "...", "new_password": "..."}
The reset token in the email is one-time and time-bound.
Anti-enumeration policy¶
Login, password reset, and email verification deliberately return identical responses whether the email exists or not. This is a trade-off. A real user with a typo'd email sees the same success message a probing attacker would, but it removes the account-enumeration oracle attackers use to build target lists.
The web app's UX mirrors this: every signup/login/reset success message is identical regardless of outcome.
Invite tokens (testers)¶
Testers don't log in. They receive a link of the form:
The invite token authenticates the request, scopes it to a single build (or single tester record across builds), and is one-time-use for state-changing actions. The install page renders an OS-appropriate install button; tapping it streams the build through a signed download URL.
See also¶
- Authorization: roles, scopes, and what each identity can do.
- API conventions: error envelope, headers, rate limits.
- CI with project access tokens: token creation walkthrough for CI.