3-Legged OAuth Visual Walkthrough
Master APS 3-legged authentication with step-by-step diagrams and code
π― Why This Guide Exists
The 3-legged OAuth flow is the most confusing part of APS development. Developers consistently get lost in:
- The difference between authorization code vs access token
- When to redirect vs when to make API calls
- How callback URLs actually work
- Managing token refresh properly
This guide provides visual diagrams and working code to eliminate the confusion.
π The Complete Flow Diagram
ββββββββββββββββ 1. User clicks "Login" ββββββββββββββββ
β Your App β ββββββββββββββββββββββββββββββ β Autodesk β
β β β OAuth β
β β ββββββββββββββββββββββββββββββ β Server β
β β 2. Redirect to /authorize β β
ββββββββββββββββ ββββββββββββββββ
β β
β 3. User enters credentials β
β 4. User grants permissions β
β β
ββββββββββββββββ 5. Redirect with ?code=... ββββββββββββββββ
β Callback β βββββββββββββββββββββββββββββββ β Autodesk β
β Endpoint β β β
β β ββββββββββββββββββββββββββββββ β β
β β 6. Exchange code for token β β
ββββββββββββββββ ββββββββββββββββ
β
β 7. Store tokens, redirect to app
β
ββββββββββββββββ
β App Home β Now authenticated!
ββββββββββββββββ
π Step-by-Step Breakdown
Step 1: Initiate Authentication
What happens: User clicks βLogin with Autodeskβ in your app
Your code does:
// Construct authorization URL
const authUrl = 'https://developer.api.autodesk.com/authentication/v2/authorize?' +
'response_type=code' +
'&client_id=' + CLIENT_ID +
'&redirect_uri=' + encodeURIComponent(REDIRECT_URI) +
'&scope=' + encodeURIComponent('data:read data:write');
// Redirect user to Autodesk
window.location.href = authUrl;
What user sees: Redirected to Autodesk login page
RAPS equivalent: raps auth login --3legged
Step 2: User Authentication
What happens: User logs into their Autodesk account
User experience:
βββββββββββββββββββββββββββββββββββ
β Autodesk Login Page β
β β
β Email: user@company.com β
β Password: β’β’β’β’β’β’β’β’ β
β β
β [Login] [Forgot Password?] β
βββββββββββββββββββββββββββββββββββ
Your app: Waits for callback (no action needed)
Step 3: Permission Grant
What happens: User sees permission request and clicks βAllowβ
User experience:
βββββββββββββββββββββββββββββββββββ
β Authorization Request β
β β
β YourApp wants to: β
β β Read your files β
β β Write to your projects β
β β
β [Allow] [Deny] β
βββββββββββββββββββββββββββββββββββ
Your app: Still waiting for callback
Step 4: Authorization Code Return
What happens: Autodesk redirects back to your app with authorization code
HTTP Request to your callback:
GET /callback?code=ABC123...&state=optional HTTP/1.1
Host: yourapp.com
Your callback endpoint receives:
app.get('/callback', (req, res) => {
const authCode = req.query.code;
const state = req.query.state; // verify if you sent one
if (!authCode) {
// User denied permission or error occurred
return res.redirect('/login?error=access_denied');
}
// Continue to Step 5...
});
Step 5: Exchange Code for Tokens
What happens: Your server exchanges authorization code for access token
API Call:
const tokenResponse = await fetch('https://developer.api.autodesk.com/authentication/v2/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: authCode,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
redirect_uri: REDIRECT_URI
})
});
const tokens = await tokenResponse.json();
Response:
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...",
"refresh_token": "OjfMGp8pF7gQPq5wYiRnwP1kLp...",
"token_type": "Bearer",
"expires_in": 3600
}
Step 6: Store Tokens and Complete Login
What happens: Your app stores tokens securely and redirects user to app
Security considerations:
// β
SECURE: Server-side session storage
req.session.accessToken = tokens.access_token;
req.session.refreshToken = tokens.refresh_token;
// β INSECURE: Client-side storage
localStorage.setItem('token', tokens.access_token); // Don't do this!
Complete the flow:
// Store tokens securely
await storeUserTokens(userId, tokens);
// Redirect to app home
res.redirect('/dashboard?login=success');
π Flow Summary Table
| Step | Endpoint | What You Send | What You Get | Who Initiates |
|---|---|---|---|---|
| 1 | /authorize |
client_id, redirect_uri, scope, response_type=code | Redirect to Autodesk login | Your app |
| 2-3 | (user action) | - | User logs in and grants permissions | User |
| 4 | /callback |
(receive code parameter) | Authorization code | Autodesk |
| 5 | /token |
code, client_id, client_secret, grant_type=authorization_code | access_token, refresh_token | Your app |
π§ Complete Working Example
Frontend (HTML + JavaScript)
<!DOCTYPE html>
<html>
<head>
<title>APS 3-Legged Auth Demo</title>
</head>
<body>
<div id="loginSection">
<h1>APS Authentication Demo</h1>
<button onclick="startAuth()">Login with Autodesk</button>
</div>
<div id="userSection" style="display: none;">
<h1>Welcome!</h1>
<p>Access Token: <span id="tokenDisplay"></span></p>
<button onclick="logout()">Logout</button>
</div>
<script>
const CLIENT_ID = 'your_client_id';
const REDIRECT_URI = 'http://localhost:3000/callback';
const SCOPES = 'data:read data:write';
function startAuth() {
const authUrl = `https://developer.api.autodesk.com/authentication/v2/authorize?` +
`response_type=code&` +
`client_id=${CLIENT_ID}&` +
`redirect_uri=${encodeURIComponent(REDIRECT_URI)}&` +
`scope=${encodeURIComponent(SCOPES)}`;
window.location.href = authUrl;
}
// Check if we just returned from auth
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('token')) {
showUserSection(urlParams.get('token'));
}
function showUserSection(token) {
document.getElementById('loginSection').style.display = 'none';
document.getElementById('userSection').style.display = 'block';
document.getElementById('tokenDisplay').textContent = token.substring(0, 20) + '...';
}
function logout() {
// Clear session
fetch('/logout', { method: 'POST' })
.then(() => window.location.reload());
}
</script>
</body>
</html>
Backend (Node.js + Express)
const express = require('express');
const session = require('express-session');
const app = express();
// Configure session
app.use(session({
secret: 'your-session-secret',
resave: false,
saveUninitialized: false
}));
const CLIENT_ID = process.env.APS_CLIENT_ID;
const CLIENT_SECRET = process.env.APS_CLIENT_SECRET;
const REDIRECT_URI = 'http://localhost:3000/callback';
// Serve static files
app.use(express.static('public'));
// OAuth callback endpoint
app.get('/callback', async (req, res) => {
const authCode = req.query.code;
if (!authCode) {
return res.redirect('/?error=access_denied');
}
try {
// Exchange code for tokens
const response = await fetch('https://developer.api.autodesk.com/authentication/v2/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: authCode,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
redirect_uri: REDIRECT_URI
})
});
const tokens = await response.json();
if (tokens.access_token) {
// Store in session
req.session.accessToken = tokens.access_token;
req.session.refreshToken = tokens.refresh_token;
// Redirect with success
res.redirect(`/?token=${tokens.access_token}`);
} else {
res.redirect('/?error=token_exchange_failed');
}
} catch (error) {
console.error('Token exchange error:', error);
res.redirect('/?error=server_error');
}
});
// Logout endpoint
app.post('/logout', (req, res) => {
req.session.destroy();
res.json({ success: true });
});
// API endpoint example
app.get('/api/user-info', async (req, res) => {
const token = req.session.accessToken;
if (!token) {
return res.status(401).json({ error: 'Not authenticated' });
}
try {
const userResponse = await fetch('https://developer.api.autodesk.com/userprofile/v1/users/@me', {
headers: {
'Authorization': `Bearer ${token}`
}
});
const userData = await userResponse.json();
res.json(userData);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch user info' });
}
});
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
π¨ Common Mistakes & Solutions
1. Callback URL Mismatch
β Problem: redirect_uri_mismatch error
Cause: URL in code doesnβt exactly match APS app settings
β Solution:
// In APS app settings: http://localhost:3000/callback
// In your code:
const REDIRECT_URI = 'http://localhost:3000/callback'; // Must match exactly
2. Storing Tokens Insecurely
β Problem: Tokens in localStorage or client-side cookies
Cause: Security vulnerability - tokens can be stolen by XSS
β Solution:
// β
SECURE: Server-side session storage
req.session.accessToken = tokens.access_token;
// β
SECURE: Encrypted HTTP-only cookies
res.cookie('session_id', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'strict'
});
3. Not Handling Token Refresh
β Problem: App breaks when access token expires (after 1 hour)
Cause: No token refresh logic
β Solution:
async function refreshTokenIfNeeded() {
const tokenAge = Date.now() - req.session.tokenTimestamp;
if (tokenAge > 55 * 60 * 1000) { // Refresh 5 min before expiry
const response = await fetch('https://developer.api.autodesk.com/authentication/v2/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: req.session.refreshToken,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET
})
});
const newTokens = await response.json();
req.session.accessToken = newTokens.access_token;
req.session.tokenTimestamp = Date.now();
}
}
4. Forgetting State Parameter
β Problem: CSRF attacks possible
Cause: No state validation
β Solution:
// Generate state when starting auth
const state = crypto.randomBytes(16).toString('hex');
req.session.oauthState = state;
const authUrl = `https://developer.api.autodesk.com/authentication/v2/authorize?` +
`state=${state}&` + // Add state parameter
`response_type=code&...`;
// Validate state in callback
app.get('/callback', (req, res) => {
if (req.query.state !== req.session.oauthState) {
return res.redirect('/?error=invalid_state');
}
// Continue with token exchange...
});
π― Testing Your Implementation
1. Test the Authorization URL
Manually visit:
https://developer.api.autodesk.com/authentication/v2/authorize?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&scope=data%3Aread%20data%3Awrite
Expected: Autodesk login page loads
2. Test Callback Handling
Mock callback:
http://localhost:3000/callback?code=test123&state=abc
Expected: Your callback handler receives the code
3. Test API Calls
With valid token:
fetch('/api/user-info')
.then(r => r.json())
.then(data => console.log(data));
Expected: User profile data returned
π‘ RAPS CLI Alternative
Instead of implementing this yourself:
# RAPS handles the entire flow automatically
raps auth login --3legged
# Check authentication status
raps auth status
# Make authenticated API calls
raps dm projects
Benefits of RAPS:
- β Automatic token refresh
- β Secure token storage
- β Built-in error handling
- β No callback URL setup needed
- β Works across all platforms
π Related Resources
APS Documentation
Other RAPS Guides
- ACC Provisioning Checklist β Fix 403 errors
- Token Refresh Patterns β Production-ready token management
Tools
- Token Decoder Tool β Inspect your tokens
- Token Cost Estimator β Calculate API costs
β Still Confused?
Common questions:
Q: Why do I need both authorization code AND access token? A: Security. The authorization code proves user consent. The access token proves your appβs identity. Two-step verification prevents token theft.
Q: Can I skip the callback URL? A: No. Itβs required by OAuth 2.0 spec. Even for mobile apps, you need a custom scheme callback.
Q: What if my callback URL changes?
A: Update it in your APS app settings. Must match exactly or youβll get redirect_uri_mismatch.
Q: How long do tokens last? A: Access tokens: 1 hour. Refresh tokens: 14 days. Plan accordingly.
Last Updated: January 2026 | RAPS v4.2.1
This guide eliminates 90% of OAuth implementation confusion