📖 Looking for karrio's legacy docs? Visit docs.karrio.io

Authentication

Karrio Apps support multiple authentication methods to ensure secure access to platform resources and external services. This guide covers all authentication patterns and best practices.

Overview

Karrio Apps use a multi-layered authentication system:

  1. App Installation API Keys - Automatic API keys for each app installation
  2. OAuth2 Authorization - For external service integrations
  3. JWT Tokens - For embedded app authentication
  4. Session-based Auth - For user-context operations

App Installation API Keys

Every app installation automatically receives a unique API key that provides access to Karrio’s APIs with the app’s configured permissions.

Automatic API Key Generation

When an app is installed, Karrio automatically generates an API key:

1mutation InstallApp { 2 install_app( 3 input: { 4 app_id: "my-app" 5 app_type: "private" 6 access_scopes: ["manage_shipments"] 7 } 8 ) { 9 installation { 10 id 11 app_id 12 api_key # Automatically generated 13 access_scopes 14 is_active 15 } 16 } 17}

The API key format follows this pattern:

karrio_app_{app_id}_{random_suffix}

Using API Keys

Use the API key to authenticate requests to Karrio’s APIs:

Using fetch directly
1const response = await fetch("/api/v1/shipments", { 2 headers: { 3 Authorization: `Bearer ${app_api_key}`, 4 "Content-Type": "application/json", 5 }, 6}); 7 8// Using the Karrio client 9import { createKarrioClient } from "@karrio/app-store"; 10 11const karrio = createKarrioClient(app_api_key); 12const shipments = await karrio.getShipments();

API Key Management

Karrio provides utilities to manage API keys:

Rotate API key
1const rotateApiKey = async (installationId: string) => { 2 const result = await graphql( 3 ` 4 mutation RotateApiKey($id: String!) { 5 rotate_app_api_key(input: { id: $id }) { 6 installation { 7 id 8 api_key 9 } 10 } 11 } 12 `, 13 { id: installationId }, 14 ); 15 16 return result.rotate_app_api_key.installation.api_key; 17}; 18 19// Ensure API key exists 20const ensureApiKey = async (installationId: string) => { 21 const result = await graphql( 22 ` 23 mutation EnsureApiKey($id: String!) { 24 ensure_app_api_key(input: { id: $id }) { 25 installation { 26 id 27 api_key 28 } 29 } 30 } 31 `, 32 { id: installationId }, 33 ); 34 35 return result.ensure_app_api_key.installation.api_key; 36};

OAuth2 for External Services

For integrating with external services (like Shopify, WooCommerce, etc.), Karrio Apps support OAuth2 flows.

Setting Up OAuth2

Create an OAuth app for external service integration:

1mutation CreateOAuthApp { 2 create_oauth_app( 3 input: { 4 display_name: "Shopify Integration" 5 description: "Connect with Shopify stores" 6 launch_url: "https://myapp.com/shopify/launch" 7 redirect_uris: "https://myapp.com/shopify/callback" 8 features: ["oauth", "webhooks"] 9 } 10 ) { 11 oauth_app { 12 id 13 client_id 14 client_secret 15 redirect_uris 16 } 17 } 18}

OAuth2 Authorization Flow

Implement the standard OAuth2 authorization code flow:

1. Generate authorization URL
1const generateAuthUrl = (shopDomain: string, clientId: string) => { 2 const params = new URLSearchParams({ 3 client_id: clientId, 4 scope: "read_orders,write_shipping", 5 redirect_uri: "https://myapp.com/shopify/callback", 6 state: generateSecureState(), 7 response_type: "code", 8 }); 9 10 return `https://${shopDomain}/admin/oauth/authorize?${params}`; 11}; 12 13// 2. Handle callback and exchange code for token 14export async function handleOAuthCallback(code: string, state: string) { 15 // Verify state parameter 16 if (!verifyState(state)) { 17 throw new Error("Invalid state parameter"); 18 } 19 20 // Exchange code for access token 21 const response = await fetch( 22 `https://${shopDomain}/admin/oauth/access_token`, 23 { 24 method: "POST", 25 headers: { "Content-Type": "application/json" }, 26 body: JSON.stringify({ 27 client_id: process.env.SHOPIFY_CLIENT_ID, 28 client_secret: process.env.SHOPIFY_CLIENT_SECRET, 29 code, 30 }), 31 }, 32 ); 33 34 const { access_token } = await response.json(); 35 36 // Store encrypted token in app metafields 37 await storeAccessToken(access_token); 38}

Storing OAuth Credentials

Use app metafields to securely store OAuth credentials:

1const storeAccessToken = async ( 2 accessToken: string, 3 installationId: string, 4) => { 5 await updateAppInstallation({ 6 id: installationId, 7 metafields: [ 8 { 9 key: "shopify_access_token", 10 value: accessToken, 11 type: "password", // Automatically encrypted 12 is_required: true, 13 is_sensitive: true, 14 }, 15 { 16 key: "shopify_shop_domain", 17 value: shopDomain, 18 type: "string", 19 is_required: true, 20 is_sensitive: false, 21 }, 22 ], 23 }); 24};

JWT Authentication for Embedded Apps

For embedded apps running within the Karrio dashboard, JWT tokens provide secure authentication.

JWT Token Structure

Karrio generates JWT tokens with this payload:

1{ 2 "iss": "karrio-dashboard", 3 "aud": "karrio-api", 4 "sub": "app-{app_id}-{installation_id}", 5 "exp": 1640995200, 6 "iat": 1640991600, 7 "app_id": "my-app", 8 "installation_id": "inst_12345", 9 "user_id": "user_67890", 10 "org_id": "org_54321", 11 "access_scopes": ["manage_shipments", "manage_data"] 12}

Using JWT Tokens

JWT tokens are automatically provided to embedded apps:

In your embedded app component
1export default function MyApp({ app, context, jwt }: AppComponentProps) { 2 // JWT token is available for API requests 3 const makeAuthenticatedRequest = async (endpoint: string) => { 4 const response = await fetch(endpoint, { 5 headers: { 6 'Authorization': `Bearer ${jwt}`, 7 'Content-Type': 'application/json' 8 } 9 }); 10 11 return response.json(); 12 }; 13 14 return ( 15 <div> 16 {/* Your app UI */} 17 </div> 18 ); 19}

JWT Middleware

For server-side routes, Karrio provides JWT authentication middleware:

api/my-app/data/route.ts
1import { NextRequest, NextResponse } from "next/server"; 2import { authenticateAppRequest } from "@karrio/app-store/auth"; 3 4export async function GET(request: NextRequest) { 5 try { 6 // Authenticate the request 7 const context = await authenticateAppRequest("my-app", request); 8 9 // Access authenticated context 10 const { karrio, installation, user } = context; 11 12 // Make authenticated API calls 13 const data = await karrio.getShipments(); 14 15 return NextResponse.json(data); 16 } catch (error) { 17 return NextResponse.json( 18 { error: "Authentication failed" }, 19 { status: 401 }, 20 ); 21 } 22}

Dual Authentication Strategy

Karrio Apps support both session-independent and session-based authentication:

Session-Independent (Primary)

For server-to-server communication and webhook handling:

Using app's own API key
1const response = await fetch("/api/apps/my-app/webhook", { 2 method: "POST", 3 headers: { 4 Authorization: `Bearer ${app_api_key}`, 5 "X-App-API-Key": app_api_key, 6 "Content-Type": "application/json", 7 }, 8 body: JSON.stringify(webhookData), 9});

Session-Based (Fallback)

For user-context operations within the dashboard:

Dashboard makes request with user session
1const response = await fetch("/api/apps/my-app/user-data", { 2 headers: { 3 Cookie: userSessionCookie, 4 "X-CSRF-Token": csrfToken, 5 }, 6});

Security Best Practices

1. Credential Storage

Always store sensitive credentials in encrypted metafields:

✅ Correct: Use metafields with encryption
1await updateAppInstallation({ 2 id: installationId, 3 metafields: [ 4 { 5 key: "api_secret", 6 value: secretValue, 7 type: "password", // Automatically encrypted 8 is_sensitive: true, 9 }, 10 ], 11}); 12 13// ❌ Wrong: Don't store secrets in plain text 14const config = { 15 api_secret: secretValue, // Exposed in logs/database 16};

2. State Parameter Validation

Always validate OAuth state parameters:

1const generateState = () => { 2 return crypto.randomBytes(32).toString("hex"); 3}; 4 5const verifyState = (receivedState: string, expectedState: string) => { 6 return crypto.timingSafeEqual( 7 Buffer.from(receivedState), 8 Buffer.from(expectedState), 9 ); 10};

3. Token Expiration

Implement proper token expiration and refresh logic:

1const refreshAccessToken = async (refreshToken: string) => { 2 try { 3 const response = await fetch("/oauth/token", { 4 method: "POST", 5 headers: { "Content-Type": "application/json" }, 6 body: JSON.stringify({ 7 grant_type: "refresh_token", 8 refresh_token: refreshToken, 9 client_id: process.env.CLIENT_ID, 10 client_secret: process.env.CLIENT_SECRET, 11 }), 12 }); 13 14 const { access_token, expires_in } = await response.json(); 15 16 // Store new token with expiration 17 await storeAccessToken(access_token, Date.now() + expires_in * 1000); 18 19 return access_token; 20 } catch (error) { 21 // Handle refresh failure 22 throw new Error("Token refresh failed"); 23 } 24};

4. Request Validation

Validate all incoming requests with proper signatures:

1const validateWebhook = ( 2 payload: string, 3 signature: string, 4 secret: string, 5) => { 6 const expectedSignature = crypto 7 .createHmac("sha256", secret) 8 .update(payload) 9 .digest("hex"); 10 11 return crypto.timingSafeEqual( 12 Buffer.from(signature), 13 Buffer.from(expectedSignature), 14 ); 15};

Error Handling

Implement comprehensive error handling for authentication failures:

1const handleAuthError = (error: any) => { 2 switch (error.code) { 3 case "INVALID_TOKEN": 4 // Refresh token or re-authenticate 5 return refreshAuthentication(); 6 7 case "INSUFFICIENT_PERMISSIONS": 8 // Request additional permissions 9 return requestPermissions(); 10 11 case "RATE_LIMITED": 12 // Implement backoff strategy 13 return retryWithBackoff(); 14 15 default: 16 // Log error and notify user 17 console.error("Authentication error:", error); 18 throw new Error("Authentication failed"); 19 } 20};

Testing Authentication

Test your authentication flows thoroughly:

Test API key authentication
1describe("API Key Authentication", () => { 2 it("should authenticate with valid API key", async () => { 3 const response = await fetch("/api/test", { 4 headers: { 5 Authorization: `Bearer ${validApiKey}`, 6 }, 7 }); 8 9 expect(response.status).toBe(200); 10 }); 11 12 it("should reject invalid API key", async () => { 13 const response = await fetch("/api/test", { 14 headers: { 15 Authorization: "Bearer invalid-key", 16 }, 17 }); 18 19 expect(response.status).toBe(401); 20 }); 21}); 22 23// Test OAuth flow 24describe("OAuth Flow", () => { 25 it("should complete OAuth authorization", async () => { 26 const authUrl = generateAuthUrl("test-shop", "client-id"); 27 expect(authUrl).toContain("oauth/authorize"); 28 29 const token = await handleOAuthCallback("auth-code", "valid-state"); 30 expect(token).toBeDefined(); 31 }); 32});

Next Steps