Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions src/main/java/com/auth0/AuthenticationController.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Map;

/**
* Base Auth0 Authenticator class.
Expand Down Expand Up @@ -445,6 +446,93 @@ public RenewAuthRequest renewAuth(String refreshToken, HttpServletRequest reques
return requestProcessor.buildRenewAuthRequest(refreshToken, request);
}

/**
* Initiates a <a href="https://auth0.com/docs/get-started/authentication-and-authorization-flow/client-initiated-backchannel-authentication-flow">Client-Initiated
* Backchannel Authentication</a> (CIBA) request. This is the first step of the CIBA flow: it
* asks Auth0 to authenticate a user out-of-band (on their own device) and returns an
* {@code auth_req_id} used to poll for the result via {@link #backChannelPoll(String, String)}.
*
* <p>The application supplies the {@code domain} to target; store it alongside the returned
* {@code auth_req_id} so the poll step can target the same domain. For applications configured
* with a fixed domain, {@link #backChannelAuthorize(String, String, java.util.Map)} may be used
* instead.</p>
*
* <p>The library remains stateless: the application owns the polling loop, honoring the
* {@code interval} and {@code expires_in} returned by the initiate step.</p>
*
* @param scope the requested scope (e.g. {@code "openid profile"}).
* @param bindingMessage the human-readable message displayed to the user on their device.
* @param loginHint a map identifying the user, serialized to the {@code login_hint} JSON.
* Auth0 expects the {@code iss_sub} shape, e.g.
* {@code {"format": "iss_sub", "iss": "https://your-tenant.auth0.com/",
* "sub": "auth0|abc123"}}.
* @param domain the Auth0 domain to target.
* @return a {@link BackChannelAuthorizeRequest} to configure and execute.
*/
public BackChannelAuthorizeRequest backChannelAuthorize(String scope, String bindingMessage, Map<String, Object> loginHint, String domain) {
Validate.notNull(scope, "scope must not be null");
Validate.notNull(bindingMessage, "bindingMessage must not be null");
Validate.notNull(loginHint, "loginHint must not be null");
Validate.notNull(domain, "domain must not be null");
return requestProcessor.buildBackChannelAuthorizeRequest(scope, bindingMessage, loginHint, domain);
}

/**
* Initiates a CIBA backchannel authentication request using the statically configured domain.
* See {@link #backChannelAuthorize(String, String, java.util.Map, String)} for details.
*
* <p>This overload is only valid when the controller was configured with a fixed domain. When a
* {@code DomainResolver} is in use, call the overload that accepts a domain.</p>
*
* @param scope the requested scope.
* @param bindingMessage the human-readable message displayed to the user on their device.
* @param loginHint a map identifying the user, serialized to the {@code login_hint} JSON.
* @return a {@link BackChannelAuthorizeRequest} to configure and execute.
* @throws IllegalStateException if the controller was configured with a {@code DomainResolver}.
*/
public BackChannelAuthorizeRequest backChannelAuthorize(String scope, String bindingMessage, Map<String, Object> loginHint) {
Validate.notNull(scope, "scope must not be null");
Validate.notNull(bindingMessage, "bindingMessage must not be null");
Validate.notNull(loginHint, "loginHint must not be null");
return requestProcessor.buildBackChannelAuthorizeRequest(scope, bindingMessage, loginHint);
}

/**
* Builds a request to poll for the result of a CIBA backchannel authentication request. This is
* the second step of the CIBA flow: the application calls {@link BackChannelTokenRequest#execute()}
* repeatedly (no more frequently than the {@code interval} returned by the authorize step) until
* the user approves (yielding verified {@link Tokens}) or a terminal error occurs.
*
* <p>The application supplies the {@code domain} it stored at the authorize step, since polling
* commonly happens outside the initiating HTTP request. For applications configured with a fixed
* domain, {@link #backChannelPoll(String)} may be used instead.</p>
*
* @param authReqId the {@code auth_req_id} returned from the authorize step.
* @param domain the Auth0 domain to target.
* @return a {@link BackChannelTokenRequest} to execute.
*/
public BackChannelTokenRequest backChannelPoll(String authReqId, String domain) {
Validate.notNull(authReqId, "authReqId must not be null");
Validate.notNull(domain, "domain must not be null");
return requestProcessor.buildBackChannelTokenRequest(authReqId, domain);
}

/**
* Builds a request to poll for the result of a CIBA backchannel authentication request using the
* statically configured domain. See {@link #backChannelPoll(String, String)} for details.
*
* <p>This overload is only valid when the controller was configured with a fixed domain. When a
* {@code DomainResolver} is in use, call the overload that accepts a domain.</p>
*
* @param authReqId the {@code auth_req_id} returned from the authorize step.
* @return a {@link BackChannelTokenRequest} to execute.
* @throws IllegalStateException if the controller was configured with a {@code DomainResolver}.
*/
public BackChannelTokenRequest backChannelPoll(String authReqId) {
Validate.notNull(authReqId, "authReqId must not be null");
return requestProcessor.buildBackChannelTokenRequest(authReqId);
}

/**
* Builds a request to exchange an external {@code subject_token} for a new set of
* {@link Tokens} via <a href="https://auth0.com/docs/authenticate/custom-token-exchange">Custom
Expand Down
69 changes: 69 additions & 0 deletions src/main/java/com/auth0/BackChannelAuthorizationException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.auth0;

/**
* Represents an error returned while polling the Auth0 token endpoint for CIBA (Client-Initiated
* Backchannel Authentication) login status. The OAuth error codes returned during polling are
* {@code authorization_pending} (user hasn't approved yet — normal, keep polling), {@code
* slow_down} (poll less frequently), {@code expired_token} (the auth_req_id expired), and {@code
* access_denied} (user rejected).
*
* <p>The non-terminal cases ({@link #isAuthorizationPending()} and {@link #isSlowDown()}) indicate
* the caller should sleep and retry. The terminal cases ({@link #isExpiredToken()} and {@link
* #isAccessDenied()}) indicate the polling loop must stop.
*
* <p><b>Note on the type hierarchy:</b> this extends {@link IdentityVerificationException} so a
* single catch clause can cover the whole CIBA poll path, but {@code authorization_pending} and
* {@code slow_down} are polling <em>control-flow signals</em>, not ID token verification failures.
* Callers should branch on the {@code isX()} helpers rather than treating every instance as a
* verification error.
*
* @see AuthenticationController#backChannelPoll(String, String)
*/
@SuppressWarnings("WeakerAccess")
public class BackChannelAuthorizationException extends IdentityVerificationException {

public static final String AUTHORIZATION_PENDING = "authorization_pending";
public static final String SLOW_DOWN = "slow_down";
public static final String EXPIRED_TOKEN = "expired_token";
public static final String ACCESS_DENIED = "access_denied";

BackChannelAuthorizationException(String code, String message) {
this(code, message, null);
}

BackChannelAuthorizationException(String code, String message, Throwable cause) {
super(code, message, cause);
}

/**
* @return true if the error is due to the user not yet approving the authentication request.
* The caller should sleep and retry.
*/
public boolean isAuthorizationPending() {
return AUTHORIZATION_PENDING.equals(getCode());
}

/**
* @return true if the polling interval should be increased. The caller should back off — the
* common convention is to add 5 seconds to the current interval — and then retry.
*/
public boolean isSlowDown() {
return SLOW_DOWN.equals(getCode());
}

/**
* @return true if the {@code auth_req_id} has expired. This is a terminal error; the polling
* loop must stop.
*/
public boolean isExpiredToken() {
return EXPIRED_TOKEN.equals(getCode());
}

/**
* @return true if the user rejected the authentication request. This is a terminal error; the
* polling loop must stop.
*/
public boolean isAccessDenied() {
return ACCESS_DENIED.equals(getCode());
}
}
100 changes: 100 additions & 0 deletions src/main/java/com/auth0/BackChannelAuthorizeRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package com.auth0;

import com.auth0.client.auth.AuthAPI;
import com.auth0.exception.Auth0Exception;
import com.auth0.json.auth.BackChannelAuthorizeResponse;

import java.util.Map;

/**
* Class to initiate a Client-Initiated Backchannel Authentication (CIBA) backchannel authorization
* request (POST /bc-authorize). This is the first step of the CIBA flow: the application requests
* authentication from Auth0 for a user identified via login hints, and Auth0 returns an
* {@code auth_req_id} that can be polled to obtain the authentication result.
* <p>
* The returned {@link BackChannelAuthorizeResponse} carries:
* <ul>
* <li>{@code auth_req_id}: a unique identifier for this authentication request, used in the
* subsequent poll step to fetch the result.</li>
* <li>{@code expires_in}: the lifetime of the authentication request in seconds; after this
* period, the auth_req_id is no longer valid.</li>
* <li>{@code interval}: the minimum number of seconds the application should wait between
* consecutive poll requests.</li>
* </ul>
* <p>
* The library remains stateless: the application owns the polling loop, storage of the
* {@code auth_req_id}, and any state associated with the authentication request. See
* {@link AuthenticationController} for the entry point to obtain an instance.
* <p>
* Optional parameters ({@code audience}, {@code requested_expiry}) can be configured via
* {@link #withAudience(String)} and {@link #withRequestedExpiry(Integer)}.
*/
@SuppressWarnings({"UnusedReturnValue", "WeakerAccess", "unused"})
public class BackChannelAuthorizeRequest {

private final AuthAPI client;
private final String scope;
private final String bindingMessage;
private final Map<String, Object> loginHint;
private final String domain;
private final String issuer;
private String audience;
private Integer requestedExpiry;

BackChannelAuthorizeRequest(AuthAPI client, String scope, String bindingMessage,
Map<String, Object> loginHint, String domain, String issuer) {
this.client = client;
this.scope = scope;
this.bindingMessage = bindingMessage;
this.loginHint = loginHint;
this.domain = domain;
this.issuer = issuer;
}

/**
* Sets the audience (API identifier) for the access token to be obtained during the
* authentication flow. When not set, Auth0 uses the default audience configured for the
* application.
*
* @param audience the audience to request a token for.
* @return this request instance for fluent chaining.
*/
public BackChannelAuthorizeRequest withAudience(String audience) {
this.audience = audience;
return this;
}

/**
* Sets the requested expiry (lifetime) of the authentication request in seconds. This value
* is sent to Auth0 as {@code requested_expiry} and controls how long the {@code auth_req_id}
* remains valid for polling.
*
* @param seconds the requested lifetime of the authentication request.
* @return this request instance for fluent chaining.
*/
public BackChannelAuthorizeRequest withRequestedExpiry(Integer seconds) {
this.requestedExpiry = seconds;
return this;
}

/**
* Executes the backchannel authorization request against Auth0 and returns the response
* containing the {@code auth_req_id}, expiration, and polling interval.
*
* @return the {@link BackChannelAuthorizeResponse} containing {@code auth_req_id},
* {@code expires_in}, and {@code interval}.
* @throws Auth0Exception if the request to the Auth0 server failed.
*/
public BackChannelAuthorizeResponse execute() throws Auth0Exception {
com.auth0.net.Request<BackChannelAuthorizeResponse> request;

if (audience == null && requestedExpiry == null) {
request = client.authorizeBackChannel(scope, bindingMessage, loginHint);
} else {
request = client.authorizeBackChannel(scope, bindingMessage, loginHint,
audience, requestedExpiry);
}

return request.execute().getBody();
}
}
50 changes: 50 additions & 0 deletions src/main/java/com/auth0/BackChannelTokenRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.auth0;

import com.auth0.exception.Auth0Exception;

/**
* Class to poll Auth0's token endpoint for the result of a CIBA (Client-Initiated Backchannel
* Authentication) backchannel authentication request.
* <p>
* The library remains stateless: the application owns the polling loop. It must call
* {@link #execute()} no more frequently than the {@code interval} returned by the initiate step,
* and handle {@link BackChannelAuthorizationException} to decide whether to keep polling
* ({@code authorization_pending} / {@code slow_down}) or stop ({@code expired_token} /
* {@code access_denied}).
* <p>
* On success, the returned ID token is verified (signature, issuer, org claims) just like the
* Custom Token Exchange login path.
* <p>
* Obtain an instance via {@link AuthenticationController#backChannelPoll(String, String)}.
*/
@SuppressWarnings({"UnusedReturnValue", "WeakerAccess", "unused"})
public class BackChannelTokenRequest {

private final RequestProcessor processor;
private final String authReqId;
private final String domain;
private final String issuer;

BackChannelTokenRequest(RequestProcessor processor, String authReqId, String domain,
String issuer) {
this.processor = processor;
this.authReqId = authReqId;
this.domain = domain;
this.issuer = issuer;
}

/**
* Polls the Auth0 token endpoint for the result of the backchannel authentication request.
*
* @return the verified {@link Tokens} once the user has approved the request.
* @throws BackChannelAuthorizationException while the request is still pending
* ({@code authorization_pending} / {@code slow_down})
* or on a terminal poll error ({@code expired_token}
* / {@code access_denied}).
* @throws IdentityVerificationException if the returned ID token fails verification.
* @throws Auth0Exception if the request to the Auth0 server failed.
*/
public Tokens execute() throws IdentityVerificationException, Auth0Exception {
return processor.executeBackChannelPoll(authReqId, domain, issuer);
}
}
Loading