OAuth uses two main endpoints: the authorization endpoint (user-facing) and the token endpoint (server-to-server). Getting these URLs correct is essential for a working OAuth integration.
Key Takeaways
- 1Authorization endpoint: User authenticates here (GET request)
- 2Token endpoint: Exchange code for tokens (POST request)
- 3Most providers publish these in OpenID Connect discovery
- 4Token endpoint requires server-side calls (not browser)
- 5Always use HTTPS for both endpoints
"The token endpoint is used by the client to obtain an access token by presenting its authorization grant or refresh token."— RFC 6749, Section 3.2
OAuth Endpoint Types
This section covers the four main OAuth endpoints you'll encounter. Each serves a distinct purpose in the authentication flow, and understanding which is which helps you troubleshoot integration issues.
Here's an overview of OAuth endpoints and how they're used:
| Endpoint | Method | Purpose | Who Calls It |
|---|---|---|---|
| Authorization | GET (redirect) | User login/consent | Browser |
| Token | POST | Exchange code for tokens | Your server |
| Userinfo | GET | Get user profile | Your server |
| Revocation | POST | Revoke tokens | Your server |
Notice that the authorization endpoint is the only one the browser calls directly (via redirect). All other endpoints are server-to-server, which is why CORS errors are common when developers accidentally try to call the token endpoint from browser JavaScript.
Major Provider URLs
Different OAuth providers use different URL structures. The following examples show the most common providers you'll integrate with. Keep in mind that some URLs include placeholders like {tenant} that you need to replace with your actual values.
// Google
const google = {
authorization: 'https://accounts.google.com/o/oauth2/v2/auth',
token: 'https://oauth2.googleapis.com/token',
userinfo: 'https://openidconnect.googleapis.com/v1/userinfo',
discovery: 'https://accounts.google.com/.well-known/openid-configuration'
};
// GitHub
const github = {
authorization: 'https://github.com/login/oauth/authorize',
token: 'https://github.com/login/oauth/access_token',
userinfo: 'https://api.github.com/user'
};
// Microsoft/Azure AD
const microsoft = {
authorization: 'https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize',
token: 'https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token',
discovery: 'https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration'
};
// Auth0
const auth0 = {
authorization: 'https://{domain}/authorize',
token: 'https://{domain}/oauth/token',
userinfo: 'https://{domain}/userinfo',
discovery: 'https://{domain}/.well-known/openid-configuration'
};Google and Microsoft follow OpenID Connect standards with well-known discovery URLs. GitHub uses a simpler OAuth-only approach without discovery. Auth0 uses your custom domain in all URLs, which you configure when creating your tenant.
OpenID Connect Discovery
Instead of hardcoding endpoint URLs, you can fetch them dynamically using OpenID Connect Discovery. This makes your code more resilient to provider changes and reduces configuration errors.
Most providers support auto-discovery of endpoints:
// Fetch endpoints automatically
async function discoverEndpoints(issuer) {
const discoveryUrl = issuer + '/.well-known/openid-configuration';
const response = await fetch(discoveryUrl);
const config = await response.json();
return {
authorization: config.authorization_endpoint,
token: config.token_endpoint,
userinfo: config.userinfo_endpoint,
jwks: config.jwks_uri,
revocation: config.revocation_endpoint,
issuer: config.issuer
};
}
// Usage
const endpoints = await discoverEndpoints('https://accounts.google.com');
console.log(endpoints.token);
// "https://oauth2.googleapis.com/token"The discovery document at /.well-known/openid-configuration is a JSON object containing all the endpoints, supported features, and signing keys. Fetching this once on startup lets you dynamically configure your OAuth client.
Token Endpoint Request
The token endpoint is where you exchange your authorization code for access tokens. This is a server-side POST request with your client credentials and the code you received from the callback.
// Exchange authorization code for tokens
async function exchangeCode(code, codeVerifier) {
const response = await fetch(TOKEN_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
// Some providers require Basic auth
// 'Authorization': 'Basic ' + btoa(CLIENT_ID + ':' + CLIENT_SECRET)
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET, // For confidential clients
code_verifier: codeVerifier // For PKCE
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error_description || error.error);
}
return response.json();
// { access_token, refresh_token, expires_in, token_type, id_token }
}The request body uses application/x-www-form-urlencoded format, not JSON. Some providers accept client credentials in the body (as shown), while others require Basic authentication in the header. Check your provider's documentation for the expected format.
Token Refresh
Access tokens are typically short-lived (an hour or less). When they expire, you can use the refresh token to obtain new access tokens without requiring user interaction.
// Refresh access token
async function refreshAccessToken(refreshToken) {
const response = await fetch(TOKEN_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET
})
});
return response.json();
}The refresh request uses the same token endpoint but with grant_type: 'refresh_token'. Store refresh tokens securely—they're long-lived credentials that can generate new access tokens, making them a high-value target for attackers.
Common Errors
Token endpoint requests can fail for various reasons. Understanding error codes helps you diagnose integration issues quickly. Here are the most common errors and their solutions:
| Error | Cause | Fix |
|---|---|---|
| invalid_client | Wrong client_id or secret | Check credentials |
| invalid_grant | Code expired or already used | Restart OAuth flow |
| invalid_redirect_uri | URI doesn't match registered | Check exact URI match |
| unsupported_grant_type | Wrong grant_type value | Use authorization_code |
The invalid_grant error is particularly common during development. Authorization codes expire quickly (usually within 10 minutes) and can only be used once. If your code exchange fails and you retry with the same code, you'll get this error.