Authentication
User login, JWT creation, and session management in Andamio applications
Authentication
Andamio uses four credential types. This page covers User JWT — wallet-based authentication for end users.
| Credential | Purpose | Guide |
|---|---|---|
| API Key | Application identity — required on most requests | API Keys |
| User JWT | User identity — wallet-signed, authorizes actions | This page |
| Developer JWT | Account management — email/password login | Developer Accounts |
| Attestation JWT | Offline identity proof for third parties | Access Token Verification |
Wallet Login
Users sign a cryptographic challenge with their Cardano wallet and receive a JWT that authorizes API requests.
How Login Works
The authentication sequence:
- User connects wallet — CIP-30 browser wallet
- Request challenge — POST to
/auth/challengewith address - API returns nonce — random string that expires in ~5 minutes
- User signs challenge — wallet prompts for signature
- Verify signature — POST to
/auth/verifywith signature - Receive JWT — token authorizes subsequent API calls
1. User Connects Wallet
Connect a CIP-30 wallet (Lace, Eternl, Nami, etc.):
import { BrowserWallet } from "@meshsdk/core";
// Get available wallets
const wallets = BrowserWallet.getInstalledWallets();
// Connect to user's chosen wallet
const wallet = await BrowserWallet.enable(walletName);
const address = await wallet.getChangeAddress();2. Verify Access Token
Verify the user has an Andamio Access Token in their wallet:
const ACCESS_TOKEN_POLICY = "29aa6a65f5c890cfa428d59b15dec6293bf4ff0a94305c957508dc78";
const utxos = await wallet.getUtxos();
const hasAccessToken = utxos.some(utxo =>
utxo.output.amount.some(asset =>
asset.unit.startsWith(ACCESS_TOKEN_POLICY)
)
);
if (!hasAccessToken) {
// Redirect to mint access token flow
}If no Access Token, redirect to Getting Started to mint one.
3. Request Challenge
const API = "https://preprod.api.andamio.io/api/v2";
const challengeRes = await fetch(`${API}/auth/challenge`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ address }),
});
const { challenge, expires_at } = await challengeRes.json();4. User Signs Challenge
Sign the challenge to prove wallet ownership:
// CIP-30 signData
const signature = await wallet.signData(address, challenge);5. Exchange for JWT
const verifyRes = await fetch(`${API}/auth/verify`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
address,
signature,
challenge, // Include original challenge
}),
});
const { jwt, expires_at, user } = await verifyRes.json();The response includes:
jwt— The authentication tokenexpires_at— When the JWT expires (typically 24 hours)user— User profile (alias, roles, etc.)
What the JWT Enables
Different actions require different roles:
Actions by Role
| Action | Required Role | Endpoint Pattern |
|---|---|---|
| View public courses/projects | None | GET /course/public/* |
| List own courses | Course Owner | GET /course/owner/* |
| Create course | Course Owner | POST /tx/instance/owner/course/create |
| Manage teachers | Course Owner | POST /tx/course/owner/teachers/manage |
| Manage modules | Teacher | POST /tx/course/teacher/modules/* |
| Assess assignments | Teacher | POST /tx/course/teacher/assignments/assess |
| Submit assignment | Student | POST /tx/course/student/assignment/* |
| Claim credential | Student | POST /tx/course/student/credential/claim |
| Create project | Project Owner | POST /tx/instance/owner/project/create |
| Manage tasks | Manager | POST /tx/project/manager/tasks/* |
| Commit to task | Contributor | POST /tx/project/contributor/task/commit |
JWT Claims
{
"sub": "addr1q9...", // Wallet address
"alias": "alice", // Access token alias
"access_token": "abc123...", // Access token asset ID
"iat": 1709568000, // Issued at
"exp": 1709654400, // Expires at
"iss": "andamio.io" // Issuer
}Session Management
Storing the JWT
// Option 1: Memory only (most secure, lost on refresh)
let jwt = null;
// Option 2: Session storage (cleared when tab closes)
sessionStorage.setItem("andamio_jwt", jwt);
// Option 3: HTTP-only cookie (set by backend)
// Most secure for server-rendered appsNever store JWTs in localStorage for production apps — they persist indefinitely and are vulnerable to XSS.
Checking Expiration
Check if the JWT is expired before making requests:
function isJwtExpired(jwt: string): boolean {
try {
const payload = JSON.parse(atob(jwt.split('.')[1]));
return Date.now() >= payload.exp * 1000;
} catch {
return true;
}
}
// Before API calls
if (isJwtExpired(jwt)) {
// Re-authenticate
await login();
}Refresh Strategy
No refresh tokens. When a JWT expires, the user signs a new challenge. Check expiration before sensitive actions and prompt re-authentication gracefully.
Logout
Discard the JWT:
function logout() {
jwt = null;
sessionStorage.removeItem("andamio_jwt");
// Redirect to home or login page
}No server-side logout endpoint is needed — JWTs are stateless.
Complete Login Example
import { BrowserWallet } from "@meshsdk/core";
const API = "https://preprod.api.andamio.io/api/v2";
const ACCESS_TOKEN_POLICY = "29aa6a65f5c890cfa428d59b15dec6293bf4ff0a94305c957508dc78";
interface AuthResult {
jwt: string;
user: { alias: string; address: string };
expiresAt: Date;
}
async function login(walletName: string): Promise<AuthResult> {
// 1. Connect wallet
const wallet = await BrowserWallet.enable(walletName);
const address = await wallet.getChangeAddress();
// 2. Verify access token exists
const utxos = await wallet.getUtxos();
const accessToken = utxos
.flatMap(u => u.output.amount)
.find(a => a.unit.startsWith(ACCESS_TOKEN_POLICY));
if (!accessToken) {
throw new Error("NO_ACCESS_TOKEN");
}
// 3. Request challenge
const challengeRes = await fetch(`${API}/auth/challenge`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ address }),
});
if (!challengeRes.ok) {
throw new Error("CHALLENGE_FAILED");
}
const { challenge } = await challengeRes.json();
// 4. Sign challenge
const signature = await wallet.signData(address, challenge);
// 5. Exchange for JWT
const verifyRes = await fetch(`${API}/auth/verify`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ address, signature, challenge }),
});
if (!verifyRes.ok) {
throw new Error("VERIFY_FAILED");
}
const { jwt, expires_at, user } = await verifyRes.json();
return {
jwt,
user,
expiresAt: new Date(expires_at),
};
}Error Handling
| Error | Cause | Resolution |
|---|---|---|
NO_ACCESS_TOKEN | User hasn't minted an access token | Redirect to mint flow |
CHALLENGE_EXPIRED | Challenge timed out before signing | Request new challenge |
INVALID_SIGNATURE | Signature doesn't match challenge | Retry sign flow |
JWT_EXPIRED | Token expired | Re-authenticate |
FORBIDDEN | User lacks role for action | Check user's roles |
Next Steps
- Developer Accounts — Developer registration, email verification, Attestation JWT
- API Keys — Request, rotate, and manage API keys
- API Integration — Making authenticated requests
- Transaction Handling — Build and submit transactions