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
Prerequisites
- Basic understanding of OAuth 2.0
- A web framework (Express, FastAPI, or Go net/http)
- An Eyevinn OSC account
Step-by-Step Guide
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:
- User clicks "Sign in with OSC" in your app
- Your app generates a PKCE code verifier and challenge
- User is redirected to OSC to authenticate
- OSC redirects back with an authorization code
- Your app exchanges the code for access and refresh tokens
- 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.
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.
- Go to app.osaas.io and sign in
- Navigate to My Apps in the sidebar
- Click on the OAuth Apps tab
- Click Create OAuth App and enter your app name

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_xxxxxxxxxxxxImportant: Never expose your client secret in client-side code. Keep it secure on your server.
Set Up OAuth Discovery
OSC provides OpenID Connect discovery to find OAuth endpoints. Create a utility to discover and cache the metadata:
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.
Implement PKCE Helpers
Create utility functions for generating PKCE parameters and state for CSRF protection:
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.
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:
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:
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`;
}Handle the OAuth Callback
When OSC redirects back, validate the state parameter and exchange the authorization code for tokens:
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.
Implement Token Refresh
Create a middleware function that automatically refreshes tokens when they're about to expire:
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;
}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:
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:
// 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.