How the Logto Consent Flow Works: OAuth 2.0 and OpenID Connect Implementation

Logto implements the OAuth 2.0/OpenID Connect consent flow through three coordinated layers: the OIDC core in packages/core/src/routes/oidc.ts checks for existing consent and parses prompt=consent parameters, the Consent API in packages/experience/src/apis/consent.ts handles data retrieval and persistence via GET and POST /api/consent endpoints, and the Experience SPA renders the consent interface using the ConsentInfoResponse schema from packages/schemas/src/types/consent.ts.

The consent flow ensures users explicitly authorize applications to access their data. In the logto-io/logto repository, this security requirement is implemented through a strict sequence of API validations, database transactions, and UI components that manage the application_user_consent_* tables.

Logto's consent implementation spans the authorization server, backend APIs, and frontend experience. When a client application initiates an OAuth flow with prompt=consent, the system executes a seven-step process to verify, display, record, and persist user authorization decisions.

1. Authorization Request Parsing

The flow begins at the OIDC endpoint. In packages/core/src/routes/oidc.ts, the authorization server parses incoming /oidc/authorize requests and extracts critical parameters including scope, prompt, and redirect_uri.

// Conceptual implementation based on packages/core/src/routes/oidc.ts
const handleAuthorize = async (ctx) => {
  const { prompt, scope } = ctx.query;
  
  if (prompt === 'consent' || !hasExistingConsent(userId, applicationId, scope)) {
    return redirectToConsentPage(userId, applicationId);
  }
};

The core checks whether the user has previously granted consent for the requested scopes. If the user has not consented, or if the client explicitly requests consent via the prompt parameter, the flow proceeds to the Consent API.

The Experience SPA fetches consent context through getConsentInfo() defined in packages/experience/src/apis/consent.ts. This function calls GET /api/consent/:userId/:applicationId to retrieve application metadata and required scopes.

// packages/experience/src/apis/consent.ts
export const getConsentInfo = async () => {
  return api.get('/api/consent/:userId/:applicationId').json<ConsentInfoResponse>();
};

The consent page component in packages/experience/src/pages/Consent/index.tsx consumes the ConsentInfoResponse data to render the application name, logo, and scope descriptions. This component validates the response against consentInfoResponseGuard from packages/schemas/src/types/consent.ts.

5. User Decision Processing

When the user clicks Allow, the frontend invokes the consent() function, which sends a POST /api/consent request with the granted scopes.

// packages/experience/src/apis/consent.ts
export const consent = async (grantedScopes: string[]) => {
  return api.post('/api/consent', { 
    json: { scopes: grantedScopes } 
  });
};

6. Database Persistence

The API handler updates the application_user_consent_* tables (defined in migration files under packages/schemas/alterations/). The consentInfoResponseGuard validates all payloads before persistence, ensuring data integrity across the consent records.

// packages/schemas/src/types/consent.ts
export const consentInfoResponseGuard = z.object({
  applicationName: z.string(),
  applicationLogo: z.string().optional(),
  scopes: z.array(z.object({
    name: z.string(),
    description: z.string()
  })),
  redirectUri: z.string()
});

7. OIDC Flow Completion

After persisting the consent record, the API redirects the user to the original redirect_uri with an authorization code. The OIDC core then processes the token exchange at /oidc/token to complete the flow.

Edge Cases and Special Behaviors

Device Flow Bypass: Device authorization flows do not require interactive UI consent. As implemented in packages/schemas/src/foundations/jsonb-types/oidc-module.ts, the core automatically stores consent for device flows without redirecting to the consent page.

Refresh Token Optimization: When prompt=consent is present but the user has already granted the requested scopes, Logto skips the UI and issues a new refresh token directly, optimizing the user experience while maintaining security compliance.

Scope Change Detection: If an application requests scopes not covered by existing consent records, Logto automatically re-prompts the user. This ensures that expanding application permissions always requires explicit user approval.

Summary

  • Entry Point: The OIDC handler in packages/core/src/routes/oidc.ts initiates consent checks during authorization requests
  • API Layer: packages/experience/src/apis/consent.ts provides getConsentInfo() and consent() for data retrieval and submission
  • Schema Validation: packages/schemas/src/types/consent.ts defines ConsentInfoResponse and consentInfoResponseGuard for type safety
  • Persistence: Consent records are stored in application_user_consent_* tables via migrations in packages/schemas/alterations/
  • Edge Handling: Device flows bypass UI consent, while scope changes trigger mandatory re-authorization

Frequently Asked Questions

The consent screen appears when a client sends an authorization request with prompt=consent, or when the user has not previously granted consent for the requested scopes. The OIDC core in packages/core/src/routes/oidc.ts checks existing consent records against the requested scopes before deciding to redirect to the consent endpoint.

Logto persists consent decisions in the application_user_consent_* tables, which are created through migration files in packages/schemas/alterations/. When a user approves consent, the POST /api/consent endpoint updates these tables with the granted scopes and timestamps, validated by the consentInfoResponseGuard schema.

Yes, but with a significant difference. According to the implementation in packages/schemas/src/foundations/jsonb-types/oidc-module.ts, device flows do not render the interactive consent UI. Instead, the core automatically stores consent records internally, bypassing the packages/experience/src/pages/Consent/index.tsx interface while maintaining audit trails.

What happens when an application requests new scopes?

Logto performs a scope comparison against existing application_user_consent_* records. If the authorization request contains scopes not present in the stored consent, the system automatically triggers the full consent flow again, requiring the user to explicitly approve the new permissions before proceeding.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →