Skip to main content
Callback endpoints complete the OAuth 2.0 flow by receiving the authorization code from providers, exchanging it for access tokens, and creating or updating user identities.

Common Flow

All callback endpoints follow this pattern:
  1. Validate state - Verify CSRF protection token
  2. Extract authorization code - From query parameters
  3. Exchange code for tokens - Call provider’s token endpoint
  4. Fetch user profile - Request user data with access token
  5. Process flow type:
    • Login: Set auth payload cookie and redirect to app callback
    • Bind: Create/update AuthIdentity in database and redirect to profile

Shared Callback Parameters

Providers redirect back with these query parameters:
code
string
required
Authorization code to exchange for access token
state
string
required
CSRF protection token (must match stored cookie)
error
string
OAuth error code if authorization failed

SecondMe Callback

Handles SecondMe OAuth callback and completes authentication.

Implementation Details

Source: src/app/api/auth/callback/route.ts:22-126 Token Exchange: exchangeCodeForTokens() in src/lib/secondme.ts:30-68 Profile Fetch: getUserProfile() in src/lib/secondme.ts:70-95 Token Endpoint: https://app.mindos.com/gate/lab/api/oauth/token/code User Info Endpoint: https://app.mindos.com/gate/lab/api/secondme/user/info

Request Flow

Provider redirects user to:
https://your-app.com/api/auth/callback?code={auth_code}&state={state_token}

Login Flow Response

1

Token Exchange

Exchanges authorization code for access token:
POST https://app.mindos.com/gate/lab/api/oauth/token/code
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code={auth_code}&
redirect_uri={redirect_uri}&
client_id={client_id}&
client_secret={client_secret}
Response format:
{
  "code": 0,
  "data": {
    "accessToken": "eyJ...",
    "refreshToken": "rt_...",
    "expiresIn": 3600,
    "tokenType": "Bearer",
    "scope": "user.info user.info.shades ..."
  }
}
2

Profile Fetch

Retrieves user profile data:
GET https://app.mindos.com/gate/lab/api/secondme/user/info
Authorization: Bearer {access_token}
Response format:
{
  "code": 0,
  "data": {
    "userId": "sm_123456",
    "name": "John Doe",
    "email": "[email protected]",
    "avatar": "https://cdn.second.me/avatars/...",
    "bio": "Software developer",
    "selfIntroduction": "I love coding"
  }
}
3

Auth Payload Cookie

Creates base64url-encoded payload cookie:
{
  "provider": "secondme",
  "providerAccountId": "sm_123456",
  "profile": { /* user profile data */ },
  "accessToken": "eyJ...",
  "refreshToken": "rt_...",
  "expiresAt": 1234567890000,
  "issuedAt": 1234564290000
}
Cookie name: secondme_auth_payloadExpiry: 2 minutes
4

Redirect

Redirects to: {appOrigin}/auth/callback/secondmeFrontend handles final session creation from auth payload cookie.

Bind Flow Response

1

Validate Target User

Checks for oauth_bind_target_user_id cookie.If missing, redirects to:
/profile?bind=failed&reason=no_target
2

Token Exchange & Profile Fetch

Same as login flow (Steps 1-2 above).
3

Conflict Detection

Queries database for existing identity:
await AuthIdentity.findOne({
  provider: 'secondme',
  providerAccountId: profile.id
}).select('canonicalUserId').lean();
If identity exists and canonicalUserId differs from bind target, redirects to:
/profile?bind=failed&reason=conflict&provider=secondme
4

Create/Update Identity

Upserts AuthIdentity record:
await AuthIdentity.findOneAndUpdate(
  { provider: 'secondme', providerAccountId: profile.id },
  {
    provider: 'secondme',
    providerAccountId: profile.id,
    canonicalUserId: bindTargetUserId,
    email: profile.email,
    name: profile.name,
    avatar: profile.avatar,
    accessToken: tokens.access_token,
    refreshToken: tokens.refresh_token,
    expiresAt: Date.now() + tokens.expires_in * 1000
  },
  { upsert: true, returnDocument: 'after' }
);
5

Success Redirect

Redirects to:
/profile?bind=success&provider=secondme

Error Responses

/?error=oauth_error
redirect
Provider returned error in callback
/?error=invalid_state
redirect
State token mismatch (CSRF attack detected)
/?error=no_code
redirect
Authorization code missing from callback
/?error=auth_failed
redirect
Token exchange or profile fetch failed

GitHub Callback

Handles GitHub OAuth callback and completes authentication.

Implementation Details

Source: src/app/api/auth/callback/github/route.ts:22-123 Token Exchange: exchangeGitHubCode() in src/lib/oauth.ts:33-61 Profile Fetch: getGitHubProfile() in src/lib/oauth.ts:63-98 Token Endpoint: https://github.com/login/oauth/access_token User Info Endpoint: https://api.github.com/user Emails Endpoint: https://api.github.com/user/emails

Request Flow

Provider redirects user to:
https://your-app.com/api/auth/callback/github?code={auth_code}&state={state_token}

Login Flow Response

1

Token Exchange

Exchanges authorization code for access token:
POST https://github.com/login/oauth/access_token
Content-Type: application/json
Accept: application/json

{
  "client_id": "{client_id}",
  "client_secret": "{client_secret}",
  "code": "{auth_code}",
  "redirect_uri": "{redirect_uri}"
}
Response:
{
  "access_token": "gho_...",
  "token_type": "bearer",
  "scope": "read:user,user:email"
}
2

Profile Fetch

Retrieves user profile:
GET https://api.github.com/user
Authorization: Bearer {access_token}
Accept: application/vnd.github+json
Response:
{
  "id": 12345678,
  "login": "johndoe",
  "name": "John Doe",
  "email": "[email protected]",
  "avatar_url": "https://avatars.githubusercontent.com/u/12345678"
}
If email is null, fetches from /user/emails:
GET https://api.github.com/user/emails
Authorization: Bearer {access_token}
Returns first verified primary email.
3

Auth Payload Cookie

Creates github_auth_payload cookie with payload:
{
  "provider": "github",
  "providerAccountId": "12345678",
  "profile": { /* GitHub profile */ },
  "accessToken": "gho_...",
  "refreshToken": null,
  "expiresAt": null,
  "issuedAt": 1234564290000
}
4

Redirect

Redirects to: {appOrigin}/auth/callback/oauth?provider=github-oauth

Bind Flow Response

Same as SecondMe bind flow, but:
  • Queries for provider: 'github'
  • Redirects to /profile?bind=success&provider=github on success
  • Redirects to /profile?bind=failed&reason=conflict&provider=github on conflict

Error Responses

Same as SecondMe callback errors.

Google Callback

Handles Google OAuth callback and completes authentication.

Implementation Details

Source: src/app/api/auth/callback/google/route.ts:22-124 Token Exchange: exchangeGoogleCode() in src/lib/oauth.ts:114-157 Profile Fetch: getGoogleProfile() in src/lib/oauth.ts:159-183 Token Endpoint: https://oauth2.googleapis.com/token User Info Endpoint: https://www.googleapis.com/oauth2/v3/userinfo

Request Flow

Provider redirects user to:
https://your-app.com/api/auth/callback/google?code={auth_code}&state={state_token}

Login Flow Response

1

Token Exchange

Exchanges authorization code for access token:
POST https://oauth2.googleapis.com/token
Content-Type: application/x-www-form-urlencoded

code={auth_code}&
client_id={client_id}&
client_secret={client_secret}&
redirect_uri={redirect_uri}&
grant_type=authorization_code
Response:
{
  "access_token": "ya29...",
  "refresh_token": "1//...",
  "expires_in": 3599,
  "token_type": "Bearer",
  "scope": "openid profile email"
}
2

Profile Fetch

Retrieves user profile:
GET https://www.googleapis.com/oauth2/v3/userinfo
Authorization: Bearer {access_token}
Response:
{
  "sub": "110169484474386276334",
  "name": "John Doe",
  "email": "[email protected]",
  "picture": "https://lh3.googleusercontent.com/a/...",
  "email_verified": true
}
3

Auth Payload Cookie

Creates google_auth_payload cookie with payload:
{
  "provider": "google",
  "providerAccountId": "110169484474386276334",
  "profile": { /* Google profile */ },
  "accessToken": "ya29...",
  "refreshToken": "1//...",
  "expiresAt": 1234567890000,
  "issuedAt": 1234564290000
}
4

Redirect

Redirects to: {appOrigin}/auth/callback/oauth?provider=google-oauth

Bind Flow Response

Same as SecondMe bind flow, but:
  • Queries for provider: 'google'
  • Stores both accessToken and refreshToken
  • Redirects to /profile?bind=success&provider=google on success
  • Redirects to /profile?bind=failed&reason=conflict&provider=google on conflict

Error Responses

Same as SecondMe callback errors.

AuthIdentity Database Schema

Bind flow creates/updates documents in the AuthIdentity collection:
{
  provider: 'secondme' | 'github' | 'google',
  providerAccountId: string,  // Provider's user ID
  canonicalUserId: string,     // Internal user ID
  email?: string,
  name: string,
  avatar?: string,
  accessToken: string,
  refreshToken?: string,
  expiresAt?: number,          // Token expiration timestamp
  createdAt: Date,
  updatedAt: Date
}
Indexes:
  • Unique compound index: { provider, providerAccountId }
  • Index on: { canonicalUserId }

Security Considerations

State Validation is CriticalAll callbacks MUST validate the state parameter matches the stored cookie. Missing or mismatched state indicates a CSRF attack.
if (!state || !storedState || state !== storedState) {
  return NextResponse.redirect('/?error=invalid_state');
}
Identity Conflict PreventionDuring bind flow, the system prevents binding a provider account that’s already linked to a different user:
if (existing?.canonicalUserId && existing.canonicalUserId !== bindTargetUserId) {
  return NextResponse.redirect('/profile?bind=failed&reason=conflict');
}
Cookie CleanupAll callback handlers delete OAuth cookies after processing:
  • {provider}_oauth_state
  • {provider}_oauth_origin
  • {provider}_oauth_redirect_uri
  • oauth_login_flow
  • oauth_bind_target_user_id (if applicable)
Token StorageAccess tokens are:
  • Never exposed to client-side JavaScript
  • Stored in MongoDB with appropriate encryption at rest
  • Associated with canonical user ID for multi-provider support
  • Refreshed as needed (Google and SecondMe support refresh tokens)

Error Handling Best Practices

// Frontend callback handler
export default function AuthCallbackPage() {
  const searchParams = useSearchParams();
  const error = searchParams.get('error');

  useEffect(() => {
    if (error) {
      const errorMessages = {
        oauth_error: 'Authorization failed. Please try again.',
        invalid_state: 'Invalid request. Please start over.',
        no_code: 'Authorization code missing.',
        auth_failed: 'Authentication failed. Please try again.'
      };
      
      toast.error(errorMessages[error] || 'Unknown error occurred');
      router.push('/');
    }
  }, [error]);
  
  // Process auth payload cookie...
}

Authentication Overview

Complete OAuth 2.0 system architecture

Login Endpoints

Initiate OAuth flows for all providers

Build docs developers (and LLMs) love