All Tutorials
Dev Tools
Intermediate
25 min

Build Apps with OSC User Delegation

Learn how to build applications that access Open Source Cloud on behalf of your users using OAuth 2.0 with PKCE for secure authentication and automatic token management.

What You'll Learn

OAuth 2.0 with PKCE authentication flow
Token storage and automatic refresh
Using OSC client libraries and APIs
Making API calls on behalf of users

Prerequisites

  • Basic understanding of OAuth 2.0
  • A web framework (Express, FastAPI, or Go net/http)
  • An Eyevinn OSC account

Step-by-Step Guide

1

Understand the OAuth 2.0 + PKCE Flow

Your application will use OAuth 2.0 with PKCE (Proof Key for Code Exchange) to authenticate users with OSC. This flow is secure for public clients like web apps and doesn't require exposing secrets to the browser.

The flow works as follows:

  1. User clicks "Sign in with OSC" in your app
  2. Your app generates a PKCE code verifier and challenge
  3. User is redirected to OSC to authenticate
  4. OSC redirects back with an authorization code
  5. Your app exchanges the code for access and refresh tokens
  6. Tokens are stored securely in the user's session

Tip: PKCE prevents authorization code interception attacks by binding the authorization request to the token exchange.

2

Create an OAuth App in OSC

Before implementing the OAuth flow, you need to register your application in the OSC web console to obtain client credentials.

  1. Go to app.osaas.io and sign in
  2. Navigate to My Apps in the sidebar
  3. Click on the OAuth Apps tab
  4. Click Create OAuth App and enter your app name
Create OAuth App dialog in OSC web console

After creating the app, you'll receive a Client ID and Client Secret. Store these securely in your application's environment variables:

CLIENT_ID=osc_xxxxxxxxxxxx
CLIENT_SECRET=osc_secret_xxxxxxxxxxxx

Important: Never expose your client secret in client-side code. Keep it secure on your server.

3

Set Up OAuth Discovery

OSC provides OpenID Connect discovery to find OAuth endpoints. Create a utility to discover and cache the metadata:

TypeScript
Python
Go
const OSC_ISSUER = "https://app.osaas.io";

let cachedMetadata: OAuthMetadata | null = null;

async function discoverMetadata() {
  if (cachedMetadata) return cachedMetadata;

  const url = `${OSC_ISSUER}/.well-known/oauth-authorization-server`;
  const res = await fetch(url);
  if (!res.ok) {
    // Fallback to known endpoints
    cachedMetadata = {
      authorization_endpoint: `${OSC_ISSUER}/api/connect/authorize`,
      token_endpoint: `${OSC_ISSUER}/api/connect/token`,
    };
  } else {
    cachedMetadata = await res.json();
  }
  return cachedMetadata;
}

Tip: Caching the discovery metadata avoids repeated network requests on every authentication attempt.

4

Implement PKCE Helpers

Create utility functions for generating PKCE parameters and state for CSRF protection:

TypeScript
Python
Go
import crypto from "crypto";

function base64URLEncode(buffer: Buffer): string {
  return buffer
    .toString("base64")
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=/g, "");
}

function generatePKCE() {
  const codeVerifier = base64URLEncode(crypto.randomBytes(32));
  const codeChallenge = base64URLEncode(
    crypto.createHash("sha256").update(codeVerifier).digest()
  );
  return { codeVerifier, codeChallenge };
}

function generateState(): string {
  return base64URLEncode(crypto.randomBytes(16));
}

Tip: The code verifier is a random string that only your server knows. The challenge is a SHA256 hash that's sent publicly during authorization.

5

Create the Sign-In Endpoint

Build an endpoint that initiates the OAuth flow. Use the client credentials from your environment variables, generate PKCE values, store them in the session, and redirect to OSC:

TypeScript
Python
Go
app.get("/auth/signin", async (req, res) => {
  const redirectUri = getRedirectUri(req);

  // Use credentials from environment variables
  req.session.clientId = process.env.CLIENT_ID;
  req.session.clientSecret = process.env.CLIENT_SECRET;

  const { codeVerifier, codeChallenge } = generatePKCE();
  const state = generateState();

  req.session.codeVerifier = codeVerifier;
  req.session.oauthState = state;

  const metadata = await discoverMetadata();
  const authUrl = new URL(metadata.authorization_endpoint);
  authUrl.searchParams.set("response_type", "code");
  authUrl.searchParams.set("client_id", req.session.clientId);
  authUrl.searchParams.set("redirect_uri", redirectUri);
  authUrl.searchParams.set("code_challenge", codeChallenge);
  authUrl.searchParams.set("code_challenge_method", "S256");
  authUrl.searchParams.set("state", state);

  res.redirect(authUrl.toString());
});

Helper function to construct redirect URI that works behind proxies:

TypeScript
Python
Go
function getRedirectUri(req: express.Request): string {
  const protocol = req.headers["x-forwarded-proto"] || req.protocol;
  const host = req.headers["x-forwarded-host"] || req.get("host");
  return `${protocol}://${host}/auth/callback`;
}
6

Handle the OAuth Callback

When OSC redirects back, validate the state parameter and exchange the authorization code for tokens:

TypeScript
Python
Go
app.get("/auth/callback", async (req, res) => {
  const { code, state } = req.query;

  // Validate state for CSRF protection
  if (state !== req.session.oauthState) {
    return res.status(400).send("Invalid state parameter");
  }

  const metadata = await discoverMetadata();
  const tokenRes = await fetch(metadata.token_endpoint, {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "authorization_code",
      code: code as string,
      redirect_uri: getRedirectUri(req),
      client_id: req.session.clientId,
      client_secret: req.session.clientSecret,
      code_verifier: req.session.codeVerifier,
    }),
  });

  const tokens = await tokenRes.json();

  // Store tokens in session
  req.session.accessToken = tokens.access_token;
  req.session.refreshToken = tokens.refresh_token;
  req.session.tokenExpiresAt = Date.now() + (tokens.expires_in - 60) * 1000;

  // Clean up PKCE values
  delete req.session.codeVerifier;
  delete req.session.oauthState;

  res.redirect("/");
});

Important: The 60-second buffer before expiry prevents race conditions where tokens expire during API requests.

7

Implement Token Refresh

Create a middleware function that automatically refreshes tokens when they're about to expire:

TypeScript
Python
Go
async function ensureValidToken(req: express.Request): Promise<boolean> {
  if (!req.session.accessToken) return false;

  const now = Date.now();
  if (req.session.tokenExpiresAt && now >= req.session.tokenExpiresAt) {
    // Token expired, try to refresh
    if (!req.session.refreshToken) {
      delete req.session.accessToken;
      return false;
    }

    try {
      const metadata = await discoverMetadata();
      const res = await fetch(metadata.token_endpoint, {
        method: "POST",
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
        body: new URLSearchParams({
          grant_type: "refresh_token",
          refresh_token: req.session.refreshToken,
          client_id: req.session.clientId,
          client_secret: req.session.clientSecret,
        }),
      });

      const tokens = await res.json();
      req.session.accessToken = tokens.access_token;
      if (tokens.refresh_token) {
        req.session.refreshToken = tokens.refresh_token;
      }
      req.session.tokenExpiresAt = Date.now() + (tokens.expires_in - 60) * 1000;
    } catch (error) {
      delete req.session.accessToken;
      delete req.session.refreshToken;
      return false;
    }
  }
  return true;
}
8

Make API Calls on Behalf of Users

Use the OSC client SDK to make API calls with the user's access token. The Context object manages authentication:

TypeScript
Python
Go
import { Context, listInstances } from "@osaas/client-core";

app.get("/api/instances", async (req, res) => {
  if (!await ensureValidToken(req)) {
    return res.status(401).json({ error: "Not authenticated" });
  }

  // Create context with user's access token
  const ctx = new Context({
    personalAccessToken: req.session.accessToken
  });

  // Get service access token for specific service
  const serviceId = "minio-minio";
  const sat = await ctx.getServiceAccessToken(serviceId);

  // List instances for this user
  const instances = await listInstances(ctx, serviceId, sat);
  res.json(instances);
});

You can also make direct HTTP requests using the access token:

TypeScript
Python
Go
// Check user's token balance
const response = await fetch("https://money.svc.prod.osaas.io/mytokencount", {
  headers: {
    "x-pat-jwt": `Bearer ${req.session.accessToken}`,
  },
});
const { tokens } = await response.json();

Tip: Service Access Tokens (SATs) provide scoped access to individual services. Always use the appropriate SAT for each service you interact with.

Congratulations!

You've implemented OAuth 2.0 user delegation for OSC. Your application can now securely authenticate users and access OSC services on their behalf with automatic token refresh.

Key Concepts

PKCE Protection

PKCE binds the authorization request to your server, preventing code interception attacks in public clients.

Session Storage

Tokens are stored server-side in HTTP sessions, never exposed to the browser for maximum security.

Automatic Refresh

Token refresh happens transparently before expiry, providing uninterrupted access for users.

Service Access Tokens

SATs provide scoped access to individual OSC services, following the principle of least privilege.

Ready to Build More?

Explore other tutorials or check out the OSC SDK documentation for more advanced features.