apereo/cas · warning

Backchannel token delivery mode cannot grant access tokens

Error message

Backchannel token delivery mode cannot grant access tokens

What it means

In CIBA (Client Initiated Backchannel Authentication), only the POLL and PING delivery modes allow the client to obtain tokens from the backchannel token endpoint; PUSH delivers the token to the client's notification endpoint instead. This validator warns and returns false when the registered service's delivery mode is anything other than POLL or PING, refusing the access-token grant.

Solutions

  1. Set the service definition's backchannelTokenDeliveryMode to 'poll' (or 'ping') in the OIDC registered service.
  2. Make the client's CIBA backchannel_authentication_mode match the registered mode.
  3. Fix typos in the mode value; an unparsable string fails validation.
  4. If PUSH is genuinely required, implement the client's CIBA notification endpoint and receive tokens there instead of calling the token endpoint.
  5. Re-run the CIBA flow to confirm the token request now passes this validator.

Example fix

// before (service definition)
"backchannelTokenDeliveryMode": "push"
// after
"backchannelTokenDeliveryMode": "poll"
Defensive patterns

Strategy: validation

Validate before calling

// Validate the delivery mode before running a CIBA flow:
boolean isTokenGrantable(String mode) {
    String m = mode == null ? "" : mode.trim().toUpperCase(java.util.Locale.ENGLISH);
    return m.equals("POLL") || m.equals("PING");
}

Prevention

When it happens

Trigger: validateInternal validates a CIBA grant ticket; OidcBackchannelTokenDeliveryModes.valueOf(service.getBackchannelTokenDeliveryMode().toUpperCase()) resolves to PUSH (or a non-POLL/PING value), so granting an access token via the token endpoint is rejected.

Common situations: Service definition set to delivery mode 'push' while the client still polls the token endpoint; typo in the mode string so it never equals POLL/PING; client migrated to CIBA without updating the mode; test definitions copied from a PUSH-mode example.

Understand the failure class

Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.

Related errors


AI-assisted analysis of apereo/cas@e7288fc434 (2026-09-08). Data as JSON: /api/errors/d67ac57c2cfa8a9c. Report an issue: GitHub.

Appendix: source

Thrown at support/cas-server-support-oidc-core-api/src/main/java/org/apereo/cas/oidc/token/ciba/OidcAccessTokenCibaGrantRequestValidator.java:54

    }

    @Override
    protected boolean validateInternal(final WebContext context, final String grantType,
                                       final ProfileManager manager, final UserProfile userProfile) {
        val configurationContext = getConfigurationContext().getObject();
        val authRequestId = configurationContext.getRequestParameterResolver().resolveRequestParameter(context, OidcConstants.AUTH_REQ_ID).orElseThrow();
        val cibaFactory = (OidcCibaRequestFactory) configurationContext.getTicketFactory().get(OidcCibaRequest.class);
        val decodedId = cibaFactory.decodeId(authRequestId);
        val ticket = configurationContext.getTicketRegistry().getTicket(decodedId, OidcCibaRequest.class);
        val result = ticket != null && !ticket.isExpired() && ticket.isReady();
        LOGGER.debug("CIBA authentication request is [{}]", BooleanUtils.toString(result, "valid", "invalid"));

        if (result) {
            val registeredService = OAuth20Utils.getRegisteredOAuthServiceByClientId(
                configurationContext.getServicesManager(), ticket.getClientId(), OidcRegisteredService.class);
            val deliveryMode = OidcBackchannelTokenDeliveryModes.valueOf(registeredService.getBackchannelTokenDeliveryMode().toUpperCase(Locale.ENGLISH));
            if (deliveryMode != OidcBackchannelTokenDeliveryModes.POLL && deliveryMode != OidcBackchannelTokenDeliveryModes.PING) {
                LOGGER.warn("Backchannel token delivery mode cannot grant access tokens");
                return false;
            }
        }

        return result;
    }

    @Override
    public boolean supports(final WebContext context) {
        val authRequestId = getConfigurationContext().getObject().getRequestParameterResolver().resolveRequestParameter(context, OidcConstants.AUTH_REQ_ID);
        return super.supports(context) && authRequestId.isPresent();
    }

    @Override
    protected OAuth20GrantTypes getGrantType() {
        return OAuth20GrantTypes.CIBA;
    }
}

View on GitHub (pinned to e7288fc434)