Skip to main content

OAuth 2.0 Authentication

Use OAuth 2.0 authorization code flow with PKCE to access user portfolio data with their consent.

Endpoints

EndpointURL
Authorizationhttps://connect.kryptos.io/oidc/auth
Tokenhttps://connect.kryptos.io/oidc/token
UserInfohttps://connect.kryptos.io/oidc/userinfo
Revocationhttps://connect.kryptos.io/oidc/token/revocation
Introspectionhttps://connect.kryptos.io/oidc/token/introspection
Discoveryhttps://connect.kryptos.io/oidc/.well-known/openid_configuration
JWKShttps://connect.kryptos.io/oidc/jwks

Available Scopes

Core Scopes

ScopeDescription
openidBasic OpenID Connect access (required)
profileUser profile (name, picture, updated_at)
emailEmail address and verified status
offline_accessAllow refresh tokens for offline data access

Data Scopes (Granular Read/Write)

ResourceRead ScopeWrite ScopeDescription
Holdingsholdings:readholdings:writePortfolio holdings and balances
Transactionstransactions:readtransactions:writeTransaction history and trades
DeFi Portfoliodefi-portfolio:readdefi-portfolio:writeDeFi positions, staking, LP pools
NFT Portfolionft-portfolio:readnft-portfolio:writeNFT collections and metadata
Ledgerledger:readledger:writeAccounting ledger entries
Taxtax:readtax:writeTax calculations and reports
Integrationsintegrations:readintegrations:writeThird-party exchange connections

Authorization Flow

Step 1: Register Your Client

Contact the Kryptos team to register your application and receive:

  • client_id
  • client_secret
  • Approved scopes

Email: support@kryptos.io

Step 2: Generate PKCE Parameters

PKCE (Proof Key for Code Exchange) adds security to the authorization flow, protecting against code interception attacks.

// Generate code verifier (random string 43-128 chars)
function generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64UrlEncode(array);
}

// Generate code challenge (SHA-256 hash of verifier)
async function generateCodeChallenge(verifier) {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const hash = await crypto.subtle.digest('SHA-256', data);
return base64UrlEncode(new Uint8Array(hash));
}

function base64UrlEncode(buffer) {
return btoa(String.fromCharCode(...buffer))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
ParameterDescription
code_verifierRandom string (43-128 chars) sent during token exchange
code_challengeBase64url encoded SHA-256 hash of the code verifier
code_challenge_methodAlways S256 for SHA-256

Step 3: Redirect to Authorization

Redirect the user to the authorization endpoint:

GET https://connect.kryptos.io/oidc/auth?
response_type=code&
client_id=YOUR_CLIENT_ID&
redirect_uri=YOUR_REDIRECT_URI&
scope=openid profile email holdings:read transactions:read defi-portfolio:read nft-portfolio:read offline_access&
state=RANDOM_STATE&
code_challenge=CODE_CHALLENGE&
code_challenge_method=S256

Example Scope Combinations:

  • Read-only: openid profile holdings:read transactions:read
  • Full access: openid profile holdings:read holdings:write transactions:read transactions:write
ParameterRequiredDescription
response_typeYesAlways code
client_idYesYour client ID
redirect_uriYesYour callback URL
scopeYesSpace-separated list of scopes
stateYesRandom string to prevent CSRF attacks
code_challengeYesPKCE code challenge
code_challenge_methodYesAlways S256

Step 4: Exchange Code for Token

After the user authorizes, they're redirected to your redirect_uri with an authorization code. Exchange it for tokens:

curl -X POST https://connect.kryptos.io/oidc/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=AUTH_CODE" \
-d "redirect_uri=YOUR_REDIRECT_URI" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "code_verifier=CODE_VERIFIER"

Response:

{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 86400,
"refresh_token": "def50200...",
"id_token": "eyJhbGciOiJSUzI1NiIs...",
"scope": "openid profile holdings:read transactions:read"
}
Token Lifetimes
  • Access Token: 24 hours
  • Refresh Token: 30 days
  • ID Token: 24 hours
  • Authorization Code: 10 minutes

Step 5: Call APIs

Use the access token to call APIs:

curl -X GET https://connect.kryptos.io/api/v1/holdings \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "X-Client-Id: YOUR_CLIENT_ID" \
-H "X-Client-Secret: YOUR_CLIENT_SECRET"

Required Headers

HeaderDescription
AuthorizationBearer {access_token}
X-Client-IdYour client ID
X-Client-SecretYour client secret

Code Examples

class KryptosClient {
constructor(clientId, clientSecret) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.baseUrl = 'https://connect.kryptos.io';
this.accessToken = null;
}

// Generate PKCE parameters
async generatePKCE() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
const verifier = this.base64UrlEncode(array);

const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const hash = await crypto.subtle.digest('SHA-256', data);
const challenge = this.base64UrlEncode(new Uint8Array(hash));

return { verifier, challenge };
}

base64UrlEncode(buffer) {
return btoa(String.fromCharCode(...buffer))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}

// Get authorization URL
// Scopes use granular :read and :write format
getAuthUrl(redirectUri, codeChallenge, scopes = [
'openid', // Required - basic OIDC
'profile', // Read: name, picture
'email', // Read: email address
'holdings:read', // Read: portfolio holdings
'transactions:read', // Read: transaction history
'defi-portfolio:read', // Read: DeFi positions
'nft-portfolio:read', // Read: NFT collections
'offline_access' // Enable refresh tokens
]) {
const params = new URLSearchParams({
response_type: 'code',
client_id: this.clientId,
redirect_uri: redirectUri,
scope: scopes.join(' '),
state: Math.random().toString(36).substring(7),
code_challenge: codeChallenge,
code_challenge_method: 'S256'
});
return `${this.baseUrl}/oidc/auth?${params}`;
}

// Exchange authorization code for tokens
async exchangeCode(code, redirectUri, codeVerifier) {
const response = await fetch(`${this.baseUrl}/oidc/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: redirectUri,
client_id: this.clientId,
client_secret: this.clientSecret,
code_verifier: codeVerifier
})
});

const tokens = await response.json();
this.accessToken = tokens.access_token;
return tokens;
}

// Get user holdings
async getHoldings() {
const response = await fetch(`${this.baseUrl}/api/v1/holdings`, {
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'X-Client-Id': this.clientId,
'X-Client-Secret': this.clientSecret
}
});
return response.json();
}

// Get user info
async getUserInfo() {
const response = await fetch(`${this.baseUrl}/api/v1/userinfo`, {
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'X-Client-Id': this.clientId,
'X-Client-Secret': this.clientSecret
}
});
return response.json();
}
}

// Usage
const client = new KryptosClient('your_client_id', 'your_client_secret');

// 1. Generate PKCE and redirect user
const { verifier, challenge } = await client.generatePKCE();
sessionStorage.setItem('code_verifier', verifier);
window.location.href = client.getAuthUrl('http://localhost:3000/callback', challenge);

// 2. Handle callback
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const storedVerifier = sessionStorage.getItem('code_verifier');
await client.exchangeCode(code, 'http://localhost:3000/callback', storedVerifier);

// 3. Get data
const holdings = await client.getHoldings();

Token Refresh

Access tokens expire after 24 hours. Use refresh tokens to get new ones:

curl -X POST https://connect.kryptos.io/oidc/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "refresh_token=YOUR_REFRESH_TOKEN"

Token Revocation

Revoke tokens when user logs out:

curl -X POST https://connect.kryptos.io/oidc/token/revocation \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "token=TOKEN_TO_REVOKE" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET"

UserInfo Endpoint

Get user information using access token:

curl -X GET https://connect.kryptos.io/oidc/userinfo \
-H "Authorization: Bearer ACCESS_TOKEN"

Response:

{
"sub": "firebase_user_uid",
"name": "John Doe",
"email": "john@example.com",
"email_verified": true,
"picture": "https://example.com/avatar.jpg",
"updated_at": 1642248600,
"holdings_access": true,
"transactions_access": true,
"defi_portfolio_access": true,
"nft_portfolio_access": false,
"ledger_access": false,
"tax_access": true,
"integrations_access": true
}

Error Handling

OIDC Error Codes

Error CodeDescription
invalid_requestRequest is missing required parameter
invalid_clientClient authentication failed
invalid_grantAuthorization grant is invalid or expired
unauthorized_clientClient not authorized for this grant type
unsupported_grant_typeGrant type not supported
invalid_scopeRequested scope is invalid
access_deniedUser denied authorization
invalid_tokenToken is invalid or expired

Error Handling Example

async function makeApiCall(endpoint, accessToken, refreshToken) {
const response = await fetch(endpoint, {
headers: { 'Authorization': `Bearer ${accessToken}` }
});

if (response.status === 401) {
// Token expired - try refresh
const newTokens = await refreshAccessToken(refreshToken);
return makeApiCall(endpoint, newTokens.access_token, newTokens.refresh_token);
}

if (response.status === 403) {
// Insufficient scope
throw new Error('Insufficient permissions for this resource');
}

if (!response.ok) {
const error = await response.json();
throw new Error(`API Error: ${error.error} - ${error.error_description}`);
}

return response.json();
}

Security Best Practices

  1. Always use PKCE - Protects against authorization code interception
  2. Store secrets securely - Never expose client secrets in frontend code
  3. Use HTTPS - Always use secure connections
  4. Validate state parameter - Prevent CSRF attacks
  5. Handle token expiration - Implement refresh token logic
  6. Request minimal scopes - Only request necessary permissions

Token Response

FieldDescription
access_tokenJWT token for API authentication
id_tokenOIDC ID token with user claims
token_typeAlways Bearer
expires_inToken validity in seconds (86400 = 24hrs)
refresh_tokenToken for obtaining new access tokens
scopeGranted scopes

Rate Limits

Endpoint TypeLimitWindow
Token Endpoint100 requestsper minute
UserInfo Endpoint1000 requestsper hour
Other OIDC200 requestsper hour

Rate limits are per client ID. Exceeding limits returns HTTP 429 with retry_after header.