agg-labs-sso-sdk
v1.0.5
Published
Officail AGG Labs SSO SDK for JavaScript/TypeScript
Readme
AGG SSO ID SDK
Official TypeScript/JavaScript SDK for integrating with AGG Single Sign-On (SSO) Identity services. This library provides a production-ready implementation of OAuth 2.0 Authorization Code Flow with PKCE (Proof Key for Code Exchange) and full OpenID Connect (OIDC) compliance.
Table of Contents
- Features
- Installation
- Configuration
- Authentication Flow
- API Reference
- Advanced Usage
- Security Best Practices
- Examples
- Troubleshooting
Features
Core Capabilities
- Zero External Dependencies: Built exclusively using native Web Crypto and Fetch APIs, ensuring minimal bundle size and maximum compatibility.
- PKCE Built-in: Automatic generation and management of code_verifier and code_challenge parameters for enhanced security.
- Full OIDC Support: Implements OpenID Connect protocol with ID token validation, user info endpoints, and refresh token rotation.
- Isomorphic Runtime: Seamlessly operates in multiple JavaScript environments including Node.js, browsers, Next.js, Cloudflare Workers, and other edge computing platforms.
- Strong TypeScript Support: Complete type definitions with comprehensive JSDoc documentation for excellent IDE integration.
- Client Secret Support: Optional server-side client secret configuration for confidential client applications.
- Token Management: Built-in refresh token handling, token revocation, and secure token storage patterns.
Installation
Install the SDK using npm:
npm install agg-labs-sso-sdkOr using yarn:
yarn add agg-labs-sso-sdkOr using pnpm:
pnpm add agg-labs-sso-sdkConfiguration
Basic Configuration (Public Client)
For browser-based applications without a backend:
import { AggClient } from 'agg-labs-sso-sdk';
const auth = new AggClient({
clientId: 'your-service-provider-id',
redirectUri: 'https://your-app.com/api/auth/callback'
});Advanced Configuration (Confidential Client)
For server-side applications with a client secret:
import { AggClient } from 'agg-labs-sso-sdk';
const auth = new AggClient({
clientId: 'your-service-provider-id',
clientSecret: process.env.AGG_CLIENT_SECRET, // Server-side only
redirectUri: 'https://your-app.com/api/auth/callback',
scopes: ['openid', 'profile', 'email', 'offline_access'],
issuerUrl: 'https://sso.agg.com' // Pre-configured, override if needed
});Configuration Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| clientId | string | Yes | Your application's unique identifier issued by AGG SSO |
| clientSecret | string | No | Server-side secret for token exchange. Use only in backend environments |
| redirectUri | string | Yes | The exact callback URL registered in your SSO application settings |
| scopes | string[] | No | OIDC scopes to request. Defaults to ['openid', 'profile', 'email'] |
| issuerUrl | string | No | AGG SSO issuer URL. Pre-configured to https://sso.agg.com |
Authentication Flow
OAuth 2.0 Authorization Code Flow with PKCE
The SDK implements the industry-standard OAuth 2.0 Authorization Code Flow optimized for OIDC:
┌──────────────┐ ┌──────────────────┐
│ │ │ │
│ Your App │ │ AGG SSO │
│ (Client) │ │ (Auth Server) │
│ │ │ │
└──────────────┘ └──────────────────┘
│ │
│ 1. createAuthorizeUrl() │
│ (Generate code_verifier, state) │
│ │
│ 2. Redirect to Authorization URL │
├──────────────────────────────────────────────>│
│ │
│ User logs in
│ │
│ 3. Redirect to Callback URL │
│ (with authorization code, state) │
│<──────────────────────────────────────────────┤
│ │
│ 4. exchangeToken() │
│ (Exchange code + code_verifier) │
├──────────────────────────────────────────────>│
│ │
│ 5. Return access_token, id_token │
│<──────────────────────────────────────────────┤
│ │
│ 6. getUserInfo() │
│ (Optional user profile fetch) │
├──────────────────────────────────────────────>│
│ │
│ 7. User Profile Data │
│<──────────────────────────────────────────────┤
│ │
API Reference
createAuthorizeUrl(scopes?: string[]): Promise<AuthorizeUrlResponse>
Generates the authorization URL that the user should be redirected to for login.
Parameters:
scopes(optional): Array of OIDC scopes. Defaults to client's configured scopes.
Returns:
interface AuthorizeUrlResponse {
url: string; // Complete authorization URL
state: string; // CSRF protection token
codeVerifier: string; // PKCE code verifier (must be stored securely)
}Example:
const { url, state, codeVerifier } = await auth.createAuthorizeUrl();
// Store in secure session/cookie
session.set('auth_state', state);
session.set('code_verifier', codeVerifier);
// Redirect user
window.location.href = url;Security Note: Always store state and codeVerifier in secure, httpOnly cookies or server-side sessions.
exchangeToken(code: string, codeVerifier: string): Promise<TokenResponse>
Exchanges the authorization code for access and ID tokens.
Parameters:
code: Authorization code from the callback URL (query parametercode)codeVerifier: The code verifier stored duringcreateAuthorizeUrl()
Returns:
interface TokenResponse {
access_token: string; // JWT for API access
id_token: string; // JWT containing user identity claims
refresh_token?: string; // Token for obtaining new access tokens
token_type: string; // Always 'Bearer'
expires_in: number; // Access token expiration in seconds
}Example:
try {
const tokens = await auth.exchangeToken(
req.query.code,
session.get('code_verifier')
);
// Securely store tokens
session.set('access_token', tokens.access_token);
session.set('id_token', tokens.id_token);
if (tokens.refresh_token) {
session.set('refresh_token', tokens.refresh_token);
}
} catch (error) {
console.error('Token exchange failed:', error);
// Redirect to login
}refreshToken(refreshToken: string): Promise<TokenResponse>
Obtains a new access token using a valid refresh token. Use this when the access token expires.
Parameters:
refreshToken: The refresh token obtained fromexchangeToken()
Returns: TokenResponse (same as exchangeToken)
Example:
try {
const newTokens = await auth.refreshToken(session.get('refresh_token'));
session.set('access_token', newTokens.access_token);
session.set('expires_at', Date.now() + newTokens.expires_in * 1000);
} catch (error) {
console.error('Token refresh failed:', error);
// Redirect to login
}getUserInfo(accessToken: string): Promise<UserInfo>
Retrieves user profile information from the UserInfo endpoint.
Parameters:
accessToken: Valid access token
Returns:
interface UserInfo {
sub: string; // Subject (user unique ID)
email?: string; // User email
email_verified?: boolean; // Email verification status
name?: string; // Full name
given_name?: string; // First name
family_name?: string; // Last name
picture?: string; // Profile picture URL
[key: string]: any; // Additional custom claims
}Example:
const userInfo = await auth.getUserInfo(tokens.access_token);
console.log(`Authenticated as: ${userInfo.email}`);
console.log(`User ID: ${userInfo.sub}`);revokeToken(token: string): Promise<void>
Revokes a refresh or access token, invalidating it immediately.
Parameters:
token: The token to revoke
Example:
try {
await auth.revokeToken(session.get('refresh_token'));
console.log('Token revoked successfully');
} catch (error) {
console.error('Token revocation failed:', error);
}logout(cookieHeader?: string): Promise<void>
Initiates a global logout from the AGG SSO system.
Parameters:
cookieHeader(optional): Cookie header string for server-side logout requests
Example:
// Client-side logout
await auth.logout();
session.clear();
window.location.href = '/';
// Server-side logout
await auth.logout(req.headers.cookie);Advanced Usage
Session Management and Token Refresh
Implement automatic token refresh for seamless user experience:
class AuthManager {
private tokens: TokenResponse;
private refreshInterval: NodeJS.Timeout;
constructor(private auth: AggClient) {}
async initialize(code: string, codeVerifier: string) {
this.tokens = await this.auth.exchangeToken(code, codeVerifier);
this.setupAutoRefresh();
}
private setupAutoRefresh() {
const expiresIn = this.tokens.expires_in;
// Refresh 1 minute before expiration
const refreshTime = (expiresIn - 60) * 1000;
this.refreshInterval = setInterval(async () => {
try {
this.tokens = await this.auth.refreshToken(this.tokens.refresh_token);
} catch (error) {
console.error('Auto-refresh failed:', error);
this.logout();
}
}, refreshTime);
}
getAccessToken(): string {
return this.tokens.access_token;
}
async logout() {
clearInterval(this.refreshInterval);
try {
await this.auth.revokeToken(this.tokens.refresh_token);
} catch (error) {
console.error('Token revocation during logout failed:', error);
}
}
}Using with Server-Side Client Secret
For applications with a secure backend:
// Backend initialization (Node.js/Express)
const auth = new AggClient({
clientId: process.env.AGG_CLIENT_ID,
clientSecret: process.env.AGG_CLIENT_SECRET, // Keep secret on backend only
redirectUri: 'https://your-app.com/api/auth/callback',
scopes: ['openid', 'profile', 'email', 'offline_access']
});
// In your auth callback route
app.get('/api/auth/callback', async (req, res) => {
const { code, state } = req.query;
// Verify state
if (state !== req.session.auth_state) {
return res.status(400).json({ error: 'Invalid state parameter' });
}
try {
const tokens = await auth.exchangeToken(
code as string,
req.session.code_verifier
);
// Store tokens securely
req.session.tokens = tokens;
req.session.userId = jwt.decode(tokens.id_token).sub;
res.redirect('/dashboard');
} catch (error) {
console.error('Authentication failed:', error);
res.redirect('/login?error=auth_failed');
}
});Implementing Protected Routes
// Middleware to check authentication
async function authRequired(req, res, next) {
if (!req.session.tokens?.access_token) {
return res.redirect('/login');
}
try {
// Check if token needs refresh
const decoded = jwt.decode(req.session.tokens.access_token);
const expiresAt = decoded.exp * 1000;
if (Date.now() > expiresAt - 60000) { // Refresh if expiring soon
req.session.tokens = await auth.refreshToken(
req.session.tokens.refresh_token
);
}
req.user = jwt.decode(req.session.tokens.id_token);
next();
} catch (error) {
req.session.destroy();
res.redirect('/login?error=session_expired');
}
}
app.get('/api/user/profile', authRequired, (req, res) => {
res.json(req.user);
});Security Best Practices
1. Store Credentials Securely
Never expose clientSecret in client-side code:
// GOOD: Server-side only
const auth = new AggClient({
clientId: process.env.AGG_CLIENT_ID,
clientSecret: process.env.AGG_CLIENT_SECRET // Backend only
});
// BAD: Never do this
const auth = new AggClient({
clientSecret: 'sk_live_...' // Client-side exposure
});2. Use HTTPS Only
Always use HTTPS for production. Redirect URIs must use HTTPS:
// Production redirect URI
redirectUri: 'https://your-app.com/api/auth/callback'
// Development only
redirectUri: 'http://localhost:3000/api/auth/callback'3. Validate State Parameter
Always verify the state parameter to prevent CSRF attacks:
const { url, state } = await auth.createAuthorizeUrl();
session.set('auth_state', state);
// In callback handler
if (req.query.state !== session.get('auth_state')) {
throw new Error('State validation failed - possible CSRF attack');
}4. Use HttpOnly Cookies
Store tokens in httpOnly cookies to protect against XSS:
// Express.js example
res.cookie('access_token', tokens.access_token, {
httpOnly: true,
secure: true, // HTTPS only
sameSite: 'strict', // CSRF protection
maxAge: tokens.expires_in * 1000
});5. Implement Token Refresh
Keep access tokens short-lived and use refresh tokens:
// Set up automatic refresh before expiration
const expiresIn = tokens.expires_in;
setTimeout(() => {
refreshToken(tokens.refresh_token);
}, (expiresIn - 300) * 1000); // Refresh 5 minutes before expiryExamples
Next.js Integration
// pages/api/auth/login.ts
import { NextApiRequest, NextApiResponse } from 'next';
import { AggClient } from 'agg-labs-sso-sdk';
const auth = new AggClient({
clientId: process.env.NEXT_PUBLIC_AGG_CLIENT_ID!,
redirectUri: `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/callback`
});
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const { url, state, codeVerifier } = await auth.createAuthorizeUrl();
// Store in HTTP-only cookie
res.setHeader('Set-Cookie', [
`auth_state=${state}; HttpOnly; Secure; Path=/`,
`code_verifier=${codeVerifier}; HttpOnly; Secure; Path=/`
]);
res.redirect(url);
}React with Context API
import React, { createContext, useState, useCallback } from 'react';
import { AggClient, TokenResponse } from 'agg-labs-sso-sdk';
const AuthContext = createContext<AuthContextType | undefined>(undefined);
const auth = new AggClient({
clientId: process.env.REACT_APP_AGG_CLIENT_ID!,
redirectUri: `${window.location.origin}/auth/callback`
});
export function AuthProvider({ children }) {
const [tokens, setTokens] = useState<TokenResponse | null>(null);
const [user, setUser] = useState(null);
const login = useCallback(async () => {
const { url, state, codeVerifier } = await auth.createAuthorizeUrl();
localStorage.setItem('auth_state', state);
localStorage.setItem('code_verifier', codeVerifier);
window.location.href = url;
}, []);
const handleCallback = useCallback(async (code: string) => {
const codeVerifier = localStorage.getItem('code_verifier');
const newTokens = await auth.exchangeToken(code, codeVerifier!);
const userInfo = await auth.getUserInfo(newTokens.access_token);
setTokens(newTokens);
setUser(userInfo);
}, []);
const logout = useCallback(async () => {
if (tokens?.refresh_token) {
await auth.revokeToken(tokens.refresh_token);
}
setTokens(null);
setUser(null);
}, [tokens]);
return (
<AuthContext.Provider value={{ tokens, user, login, logout, handleCallback }}>
{children}
</AuthContext.Provider>
);
}Troubleshooting
Common Issues
Issue: "Invalid redirect_uri"
Cause: The redirect URI in your code doesn't match what's registered in AGG SSO.
Solution: Verify the exact redirect URI registered in your SSO application settings:
const auth = new AggClient({
clientId: 'your-client-id',
redirectUri: 'https://your-app.com/api/auth/callback' // Must match exactly
});Issue: "PKCE required" or "code_challenge missing"
Cause: The SDK wasn't able to generate PKCE parameters.
Solution: Ensure Web Crypto API is available. For older Node.js versions:
// Node.js < 15
import crypto from 'crypto';
global.crypto = crypto.webcrypto;Issue: "Token expired" or 401 errors
Cause: The access token has expired.
Solution: Implement token refresh logic:
if (error.status === 401) {
const newTokens = await auth.refreshToken(refreshToken);
// Retry original request with new access token
}Issue: CORS errors when calling getUserInfo
Cause: Cross-origin requests are blocked.
Solution: Call getUserInfo from the backend, not the browser:
// Backend
app.get('/api/user', authRequired, async (req, res) => {
const userInfo = await auth.getUserInfo(req.session.access_token);
res.json(userInfo);
});
// Frontend
const response = await fetch('/api/user');
const userInfo = await response.json();Support and Contributing
For issues, questions, or contributions, please visit the GitHub repository.
License
MIT License - See LICENSE file for details
License
ISC © AGG Labs
