Andamio Logo
Developer Guides

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.

CredentialPurposeGuide
API KeyApplication identity — required on most requestsAPI Keys
User JWTUser identity — wallet-signed, authorizes actionsThis page
Developer JWTAccount management — email/password loginDeveloper Accounts
Attestation JWTOffline identity proof for third partiesAccess 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:

  1. User connects wallet — CIP-30 browser wallet
  2. Request challenge — POST to /auth/challenge with address
  3. API returns nonce — random string that expires in ~5 minutes
  4. User signs challenge — wallet prompts for signature
  5. Verify signature — POST to /auth/verify with signature
  6. 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 token
  • expires_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

ActionRequired RoleEndpoint Pattern
View public courses/projectsNoneGET /course/public/*
List own coursesCourse OwnerGET /course/owner/*
Create courseCourse OwnerPOST /tx/instance/owner/course/create
Manage teachersCourse OwnerPOST /tx/course/owner/teachers/manage
Manage modulesTeacherPOST /tx/course/teacher/modules/*
Assess assignmentsTeacherPOST /tx/course/teacher/assignments/assess
Submit assignmentStudentPOST /tx/course/student/assignment/*
Claim credentialStudentPOST /tx/course/student/credential/claim
Create projectProject OwnerPOST /tx/instance/owner/project/create
Manage tasksManagerPOST /tx/project/manager/tasks/*
Commit to taskContributorPOST /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 apps

Never 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

ErrorCauseResolution
NO_ACCESS_TOKENUser hasn't minted an access tokenRedirect to mint flow
CHALLENGE_EXPIREDChallenge timed out before signingRequest new challenge
INVALID_SIGNATURESignature doesn't match challengeRetry sign flow
JWT_EXPIREDToken expiredRe-authenticate
FORBIDDENUser lacks role for actionCheck user's roles

Next Steps