KEYCLOAK-18594 CIBA Ping Mode

This commit is contained in:
mposolda 2021-07-22 15:42:42 +02:00 committed by Marek Posolda
parent 2418e31952
commit 643b3c4c5a
26 changed files with 692 additions and 92 deletions

View file

@ -128,6 +128,8 @@ public class OIDCClientRepresentation {
// OIDC CIBA
private String backchannel_token_delivery_mode;
private String backchannel_client_notification_endpoint;
private String backchannel_authentication_request_signing_alg;
// FAPI JARM
@ -510,6 +512,14 @@ public class OIDCClientRepresentation {
this.backchannel_token_delivery_mode = backchannel_token_delivery_mode;
}
public String getBackchannelClientNotificationEndpoint() {
return backchannel_client_notification_endpoint;
}
public void setBackchannelClientNotificationEndpoint(String backchannel_client_notification_endpoint) {
this.backchannel_client_notification_endpoint = backchannel_client_notification_endpoint;
}
public String getBackchannelAuthenticationRequestSigningAlg() {
return backchannel_authentication_request_signing_alg;
}

View file

@ -34,6 +34,8 @@ public class OAuth2DeviceCodeModel {
private static final String POLLING_INTERVAL_NOTE = "int";
private static final String NONCE_NOTE = "nonce";
private static final String SCOPE_NOTE = "scope";
private static final String CLIENT_NOTIFICATION_TOKEN_NOTE = "cnt";
private static final String AUTH_REQ_ID_NOTE = "ari";
private static final String USER_SESSION_ID_NOTE = "uid";
private static final String DENIED_NOTE = "denied";
private static final String ADDITIONAL_PARAM_PREFIX = "additional_param_";
@ -43,6 +45,8 @@ public class OAuth2DeviceCodeModel {
private final String deviceCode;
private final int expiration;
private final int pollingInterval;
private final String clientNotificationToken;
private final String authReqId;
private final String scope;
private final String nonce;
private final String userSessionId;
@ -50,30 +54,31 @@ public class OAuth2DeviceCodeModel {
private final Map<String, String> additionalParams;
public static OAuth2DeviceCodeModel create(RealmModel realm, ClientModel client,
String deviceCode, String scope, String nonce, int expiresIn, int pollingInterval, Map<String, String> additionalParams) {
String deviceCode, String scope, String nonce, int expiresIn, int pollingInterval,
String clientNotificationToken, String authReqId, Map<String, String> additionalParams) {
int expiration = Time.currentTime() + expiresIn;
return new OAuth2DeviceCodeModel(realm, client.getClientId(), deviceCode, scope, nonce, expiration, pollingInterval, null, null, additionalParams);
return new OAuth2DeviceCodeModel(realm, client.getClientId(), deviceCode, scope, nonce, expiration, pollingInterval, clientNotificationToken, authReqId, null, null, additionalParams);
}
public OAuth2DeviceCodeModel approve(String userSessionId) {
return new OAuth2DeviceCodeModel(realm, clientId, deviceCode, scope, nonce, expiration, pollingInterval, userSessionId, false, additionalParams);
return new OAuth2DeviceCodeModel(realm, clientId, deviceCode, scope, nonce, expiration, pollingInterval, clientNotificationToken, authReqId, userSessionId, false, additionalParams);
}
public OAuth2DeviceCodeModel approve(String userSessionId, Map<String, String> additionalParams) {
if (additionalParams != null) {
this.additionalParams.putAll(additionalParams);
}
return new OAuth2DeviceCodeModel(realm, clientId, deviceCode, scope, nonce, expiration, pollingInterval, userSessionId, false, this.additionalParams);
return new OAuth2DeviceCodeModel(realm, clientId, deviceCode, scope, nonce, expiration, pollingInterval, clientNotificationToken, authReqId, userSessionId, false, this.additionalParams);
}
public OAuth2DeviceCodeModel deny() {
return new OAuth2DeviceCodeModel(realm, clientId, deviceCode, scope, nonce, expiration, pollingInterval, null, true, additionalParams);
return new OAuth2DeviceCodeModel(realm, clientId, deviceCode, scope, nonce, expiration, pollingInterval, clientNotificationToken, authReqId, null, true, additionalParams);
}
private OAuth2DeviceCodeModel(RealmModel realm, String clientId,
String deviceCode, String scope, String nonce, int expiration, int pollingInterval,
String userSessionId, Boolean denied, Map<String, String> additionalParams) {
String deviceCode, String scope, String nonce, int expiration, int pollingInterval, String clientNotificationToken,
String authReqId, String userSessionId, Boolean denied, Map<String, String> additionalParams) {
this.realm = realm;
this.clientId = clientId;
this.deviceCode = deviceCode;
@ -81,6 +86,8 @@ public class OAuth2DeviceCodeModel {
this.nonce = nonce;
this.expiration = expiration;
this.pollingInterval = pollingInterval;
this.clientNotificationToken = clientNotificationToken;
this.authReqId = authReqId;
this.userSessionId = userSessionId;
this.denied = denied;
this.additionalParams = additionalParams;
@ -98,8 +105,8 @@ public class OAuth2DeviceCodeModel {
private OAuth2DeviceCodeModel(RealmModel realm, String deviceCode, Map<String, String> data) {
this(realm, data.get(CLIENT_ID), deviceCode, data.get(SCOPE_NOTE), data.get(NONCE_NOTE),
Integer.parseInt(data.get(EXPIRATION_NOTE)), Integer.parseInt(data.get(POLLING_INTERVAL_NOTE)), data.get(USER_SESSION_ID_NOTE),
Boolean.parseBoolean(data.get(DENIED_NOTE)), extractAdditionalParams(data));
Integer.parseInt(data.get(EXPIRATION_NOTE)), Integer.parseInt(data.get(POLLING_INTERVAL_NOTE)), data.get(CLIENT_NOTIFICATION_TOKEN_NOTE),
data.get(AUTH_REQ_ID_NOTE), data.get(USER_SESSION_ID_NOTE), Boolean.parseBoolean(data.get(DENIED_NOTE)), extractAdditionalParams(data));
}
private static Map<String, String> extractAdditionalParams(Map<String, String> data) {
@ -132,6 +139,14 @@ public class OAuth2DeviceCodeModel {
return pollingInterval;
}
public String getClientNotificationToken() {
return clientNotificationToken;
}
public String getAuthReqId() {
return authReqId;
}
public String getClientId() {
return clientId;
}
@ -164,6 +179,12 @@ public class OAuth2DeviceCodeModel {
Map<String, String> result = new HashMap<>();
result.put(REALM_ID, realm.getId());
if (clientNotificationToken != null) {
result.put(CLIENT_NOTIFICATION_TOKEN_NOTE, clientNotificationToken);
}
if (authReqId != null) {
result.put(AUTH_REQ_ID_NOTE, authReqId);
}
if (denied == null) {
result.put(CLIENT_ID, clientId);

View file

@ -16,11 +16,20 @@
*/
package org.keycloak.models;
import java.util.Arrays;
import java.util.List;
import org.keycloak.jose.jws.Algorithm;
import org.keycloak.utils.StringUtil;
public class CibaConfig extends AbstractConfig {
// Constants
public static final String CIBA_POLL_MODE = "poll";
public static final String CIBA_PING_MODE = "ping";
public static final String CIBA_PUSH_MODE = "push";
public static final List<String> CIBA_SUPPORTED_MODES = Arrays.asList(CIBA_POLL_MODE, CIBA_PING_MODE);
// realm attribute names
public static final String CIBA_BACKCHANNEL_TOKEN_DELIVERY_MODE = "cibaBackchannelTokenDeliveryMode";
public static final String CIBA_EXPIRES_IN = "cibaExpiresIn";
@ -28,7 +37,7 @@ public class CibaConfig extends AbstractConfig {
public static final String CIBA_AUTH_REQUESTED_USER_HINT = "cibaAuthRequestedUserHint";
// default value
public static final String DEFAULT_CIBA_POLICY_TOKEN_DELIVERY_MODE = "poll";
public static final String DEFAULT_CIBA_POLICY_TOKEN_DELIVERY_MODE = CIBA_POLL_MODE;
public static final int DEFAULT_CIBA_POLICY_EXPIRES_IN = 120;
public static final int DEFAULT_CIBA_POLICY_INTERVAL = 5;
public static final String DEFAULT_CIBA_POLICY_AUTH_REQUESTED_USER_HINT = "login_hint";
@ -41,6 +50,7 @@ public class CibaConfig extends AbstractConfig {
// client attribute names
public static final String OIDC_CIBA_GRANT_ENABLED = "oidc.ciba.grant.enabled";
public static final String CIBA_BACKCHANNEL_TOKEN_DELIVERY_MODE_PER_CLIENT = "ciba.backchannel.token.delivery.mode";
public static final String CIBA_BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT = "ciba.backchannel.client.notification.endpoint";
public static final String CIBA_BACKCHANNEL_AUTH_REQUEST_SIGNING_ALG = "ciba.backchannel.auth.request.signing.alg";
public CibaConfig(RealmModel realm) {
@ -146,4 +156,8 @@ public class CibaConfig extends AbstractConfig {
String alg = client.getAttribute(CIBA_BACKCHANNEL_AUTH_REQUEST_SIGNING_ALG);
return alg==null ? null : Enum.valueOf(Algorithm.class, alg);
}
public String getBackchannelClientNotificationEndpoint(ClientModel client) {
return client.getAttribute(CIBA_BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT);
}
}

View file

@ -87,8 +87,6 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
// KEYCLOAK-7451 OAuth Authorization Server Metadata for Proof Key for Code Exchange
public static final List<String> DEFAULT_CODE_CHALLENGE_METHODS_SUPPORTED = list(OAuth2Constants.PKCE_METHOD_PLAIN, OAuth2Constants.PKCE_METHOD_S256);
public static final List<String> DEFAULT_BACKCHANNEL_TOKEN_DELIVERY_MODES_SUPPORTED= list(CibaConfig.DEFAULT_CIBA_POLICY_TOKEN_DELIVERY_MODE);
private KeycloakSession session;
public OIDCWellKnownProvider(KeycloakSession session) {
@ -181,7 +179,7 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
config.setBackchannelLogoutSupported(true);
config.setBackchannelLogoutSessionSupported(true);
config.setBackchannelTokenDeliveryModesSupported(DEFAULT_BACKCHANNEL_TOKEN_DELIVERY_MODES_SUPPORTED);
config.setBackchannelTokenDeliveryModesSupported(CibaConfig.CIBA_SUPPORTED_MODES);
config.setBackchannelAuthenticationEndpoint(CibaGrantType.authorizationUrl(backendUriInfo.getBaseUriBuilder()).build(realm.getName()).toString());
config.setBackchannelAuthenticationRequestSigningAlgValuesSupported(getSupportedBackchannelAuthenticationRequestSigningAlgorithms());

View file

@ -0,0 +1,89 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.keycloak.protocol.oidc.grants.ciba;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.google.common.collect.Streams;
import org.keycloak.crypto.ClientSignatureVerifierProvider;
import org.keycloak.crypto.SignatureProvider;
import org.keycloak.jose.jws.Algorithm;
import org.keycloak.models.CibaConfig;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.validation.DefaultClientValidationProvider;
import org.keycloak.validation.ValidationContext;
import static org.keycloak.common.util.UriUtils.checkUrl;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class CibaClientValidation {
private final ValidationContext<ClientModel> context;
public CibaClientValidation(ValidationContext<ClientModel> context) {
this.context = context;
}
public void validate() {
ClientModel client = context.getObjectToValidate();
// Check only ping mode and poll mode allowed
CibaConfig cibaConfig = client.getRealm().getCibaPolicy();
String cibaMode = cibaConfig.getBackchannelTokenDeliveryMode(client);
if (!CibaConfig.CIBA_SUPPORTED_MODES.contains(cibaMode)) {
context.addError("cibaBackchannelTokenDeliveryMode", "Unsupported requested CIBA Backchannel Token Delivery Mode", "invalidCibaBackchannelTokenDeliveryMode");
}
// Check clientNotificationEndpoint URL configured for ping mode
if (CibaConfig.CIBA_PING_MODE.equals(cibaMode)) {
if (cibaConfig.getBackchannelClientNotificationEndpoint(client) == null) {
context.addError("cibaBackchannelClientNotificationEndpoint", "CIBA Backchannel Client Notification Endpoint must be set for the CIBA ping mode", "missingCibaBackchannelClientNotificationEndpoint");
}
}
// Validate clientNotificationEndpoint URL itself
try {
checkUrl(client.getRealm().getSslRequired(), cibaConfig.getBackchannelClientNotificationEndpoint(client), "backchannel_client_notification_endpoint");
} catch (RuntimeException re) {
context.addError("cibaBackchannelClientNotificationEndpoint", re.getMessage(), "invalidBackchannelClientNotificationEndpoint");
}
Algorithm alg = cibaConfig.getBackchannelAuthRequestSigningAlg(client);
if (alg != null && !isSupportedBackchannelAuthenticationRequestSigningAlg(context.getSession(), alg.name())) {
context.addError("cibaBackchannelAuthRequestSigningAlg", "Unsupported requested CIBA Backchannel Authentication Request Signing Algorithm", "invalidCibaBackchannelAuthRequestSigningAlg");
}
}
private static boolean isSupportedBackchannelAuthenticationRequestSigningAlg(KeycloakSession session, String alg) {
// Consider removing 'none' . Not sure if we should allow him based on the CIBA specification...
if (Algorithm.none.name().equals(alg)) {
return true;
}
// Only asymmetric algorithms supported for CIBA signed request according to the specification
SignatureProvider signatureProvider = session.getProvider(SignatureProvider.class, alg);
return signatureProvider.isAsymmetricAlgorithm();
}
}

View file

@ -91,6 +91,9 @@ public class CIBAAuthenticationRequest extends JsonWebToken {
@JsonIgnore
protected ClientModel client;
@JsonIgnore
protected String clientNotificationToken;
@JsonIgnore
protected UserModel user;
@ -171,6 +174,14 @@ public class CIBAAuthenticationRequest extends JsonWebToken {
return client;
}
public String getClientNotificationToken() {
return clientNotificationToken;
}
public void setClientNotificationToken(String clientNotificationToken) {
this.clientNotificationToken = clientNotificationToken;
}
public void setUser(UserModel user) {
this.user = user;
}

View file

@ -16,13 +16,16 @@
*/
package org.keycloak.protocol.oidc.grants.ciba.endpoints;
import org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.OAuthErrorException;
import org.keycloak.TokenVerifier;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.CibaConfig;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OAuth2DeviceCodeModel;
@ -42,12 +45,16 @@ import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.Map;
import static org.keycloak.protocol.oidc.grants.ciba.channel.AuthenticationChannelResponse.Status.CANCELLED;
public class BackchannelAuthenticationCallbackEndpoint extends AbstractCibaEndpoint {
private static final Logger logger = Logger.getLogger(BackchannelAuthenticationCallbackEndpoint.class);
@Context
private HttpRequest httpRequest;
@ -62,7 +69,10 @@ public class BackchannelAuthenticationCallbackEndpoint extends AbstractCibaEndpo
@Produces(MediaType.APPLICATION_JSON)
public Response processAuthenticationChannelResult(AuthenticationChannelResponse response) {
event.event(EventType.LOGIN);
AccessToken bearerToken = verifyAuthenticationRequest(httpRequest.getHttpHeaders());
BackchannelAuthCallbackContext ctx = verifyAuthenticationRequest(httpRequest.getHttpHeaders());
AccessToken bearerToken = ctx.bearerToken;
OAuth2DeviceCodeModel deviceModel = ctx.deviceModel;
Status status = response.getStatus();
if (status == null) {
@ -81,10 +91,17 @@ public class BackchannelAuthenticationCallbackEndpoint extends AbstractCibaEndpo
break;
}
// Call the notification endpoint
ClientModel client = session.getContext().getClient();
CibaConfig cibaConfig = realm.getCibaPolicy();
if (cibaConfig.getBackchannelTokenDeliveryMode(client).equals(CibaConfig.CIBA_PING_MODE)) {
sendClientNotificationRequest(client, cibaConfig, deviceModel);
}
return Response.ok(MediaType.APPLICATION_JSON_TYPE).build();
}
private AccessToken verifyAuthenticationRequest(HttpHeaders headers) {
private BackchannelAuthCallbackContext verifyAuthenticationRequest(HttpHeaders headers) {
String rawBearerToken = AppAuthManager.extractAuthorizationHeaderTokenOrReturnNull(headers);
if (rawBearerToken == null) {
@ -130,9 +147,10 @@ public class BackchannelAuthenticationCallbackEndpoint extends AbstractCibaEndpo
Response.Status.BAD_REQUEST);
}
session.getContext().setClient(issuedFor);
event.client(issuedFor);
return bearerToken;
return new BackchannelAuthCallbackContext(bearerToken, deviceCode);
}
private void cancelRequest(String authResultId) {
@ -158,4 +176,52 @@ public class BackchannelAuthenticationCallbackEndpoint extends AbstractCibaEndpo
store.deny(realm, authReqId.getId());
}
protected void sendClientNotificationRequest(ClientModel client, CibaConfig cibaConfig, OAuth2DeviceCodeModel deviceModel) {
String clientNotificationEndpoint = cibaConfig.getBackchannelClientNotificationEndpoint(client);
if (clientNotificationEndpoint == null) {
event.error(Errors.INVALID_REQUEST);
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Client notification endpoint not set for the client with the ping mode",
Response.Status.BAD_REQUEST);
}
logger.debugf("Sending request to client notification endpoint '%s' for the client '%s'", clientNotificationEndpoint, client.getClientId());
ClientNotificationEndpointRequest clientNotificationRequest = new ClientNotificationEndpointRequest();
clientNotificationRequest.setAuthReqId(deviceModel.getAuthReqId());
SimpleHttp simpleHttp = SimpleHttp.doPost(clientNotificationEndpoint, session)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
.json(clientNotificationRequest)
.auth(deviceModel.getClientNotificationToken());
try {
int notificationResponseStatus = simpleHttp.asStatus();
logger.tracef("Received status '%d' from request to client notification endpoint '%s' for the client '%s'", notificationResponseStatus, clientNotificationEndpoint, client.getClientId());
if (notificationResponseStatus != 200 && notificationResponseStatus != 204) {
logger.warnf("Invalid status returned from client notification endpoint '%s' of client '%s'", clientNotificationEndpoint, client.getClientId());
event.error(Errors.INVALID_REQUEST);
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Failed to send request to client notification endpoint",
Response.Status.BAD_REQUEST);
}
} catch (IOException ioe) {
logger.errorf(ioe, "Failed to send request to client notification endpoint '%s' of client '%s'", clientNotificationEndpoint, client.getClientId());
event.error(Errors.INVALID_REQUEST);
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Failed to send request to client notification endpoint",
Response.Status.BAD_REQUEST);
}
}
private class BackchannelAuthCallbackContext {
private final AccessToken bearerToken;
private final OAuth2DeviceCodeModel deviceModel;
private BackchannelAuthCallbackContext(AccessToken bearerToken, OAuth2DeviceCodeModel deviceModel) {
this.bearerToken = bearerToken;
this.deviceModel = deviceModel;
}
}
}

View file

@ -51,6 +51,7 @@ import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import java.util.Collections;
import java.util.Optional;
@ -96,7 +97,7 @@ public class BackchannelAuthenticationEndpoint extends AbstractCibaEndpoint {
CibaConfig cibaPolicy = realm.getCibaPolicy();
int poolingInterval = cibaPolicy.getPoolingInterval();
storeAuthenticationRequest(request, cibaPolicy);
storeAuthenticationRequest(request, cibaPolicy, authReqId);
ObjectNode response = JsonSerialization.createObjectNode();
@ -122,13 +123,19 @@ public class BackchannelAuthenticationEndpoint extends AbstractCibaEndpoint {
* but probably make the {@link OAuth2DeviceTokenStoreProvider} more generic for ciba, device, or any other use case
* that relies on cross-references for unsolicited user authentication requests from devices.
*/
private void storeAuthenticationRequest(CIBAAuthenticationRequest request, CibaConfig cibaConfig) {
private void storeAuthenticationRequest(CIBAAuthenticationRequest request, CibaConfig cibaConfig, String authReqId) {
ClientModel client = request.getClient();
int expiresIn = cibaConfig.getExpiresIn();
int poolingInterval = cibaConfig.getPoolingInterval();
String cibaMode = cibaConfig.getBackchannelTokenDeliveryMode(client);
// Set authReqId just for the ping mode as it is relatively big and not necessarily needed in the infinispan cache for the "poll" mode
if (!CibaConfig.CIBA_PING_MODE.equals(cibaMode)) {
authReqId = null;
}
OAuth2DeviceCodeModel deviceCode = OAuth2DeviceCodeModel.create(realm, client,
request.getId(), request.getScope(), null, expiresIn, poolingInterval,
request.getId(), request.getScope(), null, expiresIn, poolingInterval, request.getClientNotificationToken(), authReqId,
Collections.emptyMap());
String authResultId = request.getAuthResultId();
OAuth2DeviceUserCodeModel userCode = new OAuth2DeviceUserCodeModel(realm, deviceCode.getDeviceCode(),
@ -184,8 +191,19 @@ public class BackchannelAuthenticationEndpoint extends AbstractCibaEndpoint {
request.setScope(scopes.toString());
if (endpointRequest.getClientNotificationToken() != null) {
if (!policy.getBackchannelTokenDeliveryMode(client).equals(CibaConfig.CIBA_PING_MODE)) {
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST,
"Ping and push modes not supported. Use poll mode instead.", Response.Status.BAD_REQUEST);
"Client Notification token supported only for the ping mode", Response.Status.BAD_REQUEST);
}
if (endpointRequest.getClientNotificationToken().length() > 1024) {
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST,
"Client Notification token length is limited to 1024 characters", Response.Status.BAD_REQUEST);
}
request.setClientNotificationToken(endpointRequest.getClientNotificationToken());
}
if (endpointRequest.getClientNotificationToken() == null && policy.getBackchannelTokenDeliveryMode(client).equals(CibaConfig.CIBA_PING_MODE)) {
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST,
"Client Notification token needs to be provided with the ping mode", Response.Status.BAD_REQUEST);
}
if (endpointRequest.getUserCode() != null) {

View file

@ -0,0 +1,39 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.keycloak.protocol.oidc.grants.ciba.endpoints;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.keycloak.protocol.oidc.grants.ciba.CibaGrantType;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ClientNotificationEndpointRequest {
@JsonProperty(CibaGrantType.AUTH_REQ_ID)
private String authReqId;
public String getAuthReqId() {
return authReqId;
}
public void setAuthReqId(String authReqId) {
this.authReqId = authReqId;
}
}

View file

@ -125,7 +125,7 @@ public class DeviceEndpoint extends AuthorizationEndpointBase implements RealmRe
int interval = realm.getOAuth2DeviceConfig().getPoolingInterval(client);
OAuth2DeviceCodeModel deviceCode = OAuth2DeviceCodeModel.create(realm, client,
Base64Url.encode(KeycloakModelUtils.generateSecret()), request.getScope(), request.getNonce(), expiresIn, interval,
Base64Url.encode(KeycloakModelUtils.generateSecret()), request.getScope(), request.getNonce(), expiresIn, interval, null, null,
request.getAdditionalReqParams());
OAuth2DeviceUserCodeProvider userCodeProvider = session.getProvider(OAuth2DeviceUserCodeProvider.class);
String secret = userCodeProvider.generate();

View file

@ -24,6 +24,7 @@ import java.util.Optional;
import org.jboss.logging.Logger;
import org.keycloak.OAuthErrorException;
import org.keycloak.models.CibaConfig;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
@ -113,6 +114,10 @@ public class SecureClientUrisExecutor implements ClientPolicyExecutorProvider<Cl
// OIDD : requestUris
List<String> requestUris = getAttributeMultivalued(clientRep, OIDCConfigAttributes.REQUEST_URIS);
if (requestUris != null) confirmSecureUris(requestUris, "requestUris");
// CIBA : client notification endpoint
String clientNotificationEndpoint = Optional.ofNullable(clientRep.getAttributes()).orElse(Collections.emptyMap()).get(CibaConfig.CIBA_BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT);
if (clientNotificationEndpoint != null) confirmSecureUris(Arrays.asList(clientNotificationEndpoint), "cibaClientNotificationEndpoint");
}
private List<String> getAttributeMultivalued(ClientRepresentation clientRep, String attrKey) {

View file

@ -64,6 +64,7 @@ import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.keycloak.models.CibaConfig.CIBA_POLL_MODE;
import static org.keycloak.models.OAuth2DeviceConfig.OAUTH2_DEVICE_AUTHORIZATION_GRANT_ENABLED;
import static org.keycloak.models.CibaConfig.OIDC_CIBA_GRANT_ENABLED;
@ -187,27 +188,27 @@ public class DescriptionConverter {
configWrapper.setBackchannelLogoutRevokeOfflineTokens(clientOIDC.getBackchannelLogoutRevokeOfflineTokens());
}
// CIBA
String backchannelTokenDeliveryMode = clientOIDC.getBackchannelTokenDeliveryMode();
if (backchannelTokenDeliveryMode != null) {
if(isSupportedBackchannelTokenDeliveryMode(backchannelTokenDeliveryMode)) {
Map<String, String> attr = Optional.ofNullable(client.getAttributes()).orElse(new HashMap<>());
attr.put(CibaConfig.CIBA_BACKCHANNEL_TOKEN_DELIVERY_MODE_PER_CLIENT, backchannelTokenDeliveryMode);
client.setAttributes(attr);
} else {
throw new ClientRegistrationException("Unsupported requested backchannel_token_delivery_mode");
}
String backchannelClientNotificationEndpoint = clientOIDC.getBackchannelClientNotificationEndpoint();
if (backchannelClientNotificationEndpoint != null) {
Map<String, String> attr = Optional.ofNullable(client.getAttributes()).orElse(new HashMap<>());
attr.put(CibaConfig.CIBA_BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT, backchannelClientNotificationEndpoint);
client.setAttributes(attr);
}
String backchannelAuthenticationRequestSigningAlg = clientOIDC.getBackchannelAuthenticationRequestSigningAlg();
if (backchannelAuthenticationRequestSigningAlg != null) {
if(isSupportedBackchannelAuthenticationRequestSigningAlg(session, backchannelAuthenticationRequestSigningAlg)) {
Map<String, String> attr = Optional.ofNullable(client.getAttributes()).orElse(new HashMap<>());
attr.put(CibaConfig.CIBA_BACKCHANNEL_AUTH_REQUEST_SIGNING_ALG, backchannelAuthenticationRequestSigningAlg);
client.setAttributes(attr);
} else {
throw new ClientRegistrationException("Unsupported requested backchannel_authentication_request_signing_alg");
}
}
// PAR
Boolean requirePushedAuthorizationRequests = clientOIDC.getRequirePushedAuthorizationRequests();
if (requirePushedAuthorizationRequests != null) {
Map<String, String> attr = Optional.ofNullable(client.getAttributes()).orElse(new HashMap<>());
@ -225,18 +226,6 @@ public class DescriptionConverter {
client.setAttributes(attributes);
}
private static boolean isSupportedBackchannelTokenDeliveryMode(String mode) {
if (mode.equals(CibaConfig.DEFAULT_CIBA_POLICY_TOKEN_DELIVERY_MODE)) return true;
return false;
}
private static boolean isSupportedBackchannelAuthenticationRequestSigningAlg(KeycloakSession session, String alg) {
Stream<String> supportedAlgorithms = session.getKeycloakSessionFactory().getProviderFactoriesStream(ClientSignatureVerifierProvider.class)
.map(ProviderFactory::getId);
supportedAlgorithms = Streams.concat(supportedAlgorithms, Stream.of("none"));
return supportedAlgorithms.collect(Collectors.toList()).contains(alg);
}
private static List<String> getSupportedAlgorithms(KeycloakSession session, Class<? extends Provider> clazz, boolean includeNone) {
Stream<String> supportedAlgorithms = session.getKeycloakSessionFactory().getProviderFactoriesStream(clazz)
.map(ProviderFactory::getId);
@ -372,6 +361,10 @@ public class DescriptionConverter {
if (StringUtil.isNotBlank(mode)) {
response.setBackchannelTokenDeliveryMode(mode);
}
String clientNotificationEndpoint = client.getAttributes().get(CibaConfig.CIBA_BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT);
if (StringUtil.isNotBlank(clientNotificationEndpoint)) {
response.setBackchannelClientNotificationEndpoint(clientNotificationEndpoint);
}
String alg = client.getAttributes().get(CibaConfig.CIBA_BACKCHANNEL_AUTH_REQUEST_SIGNING_ALG);
if (StringUtil.isNotBlank(alg)) {
response.setBackchannelAuthenticationRequestSigningAlg(alg);

View file

@ -19,6 +19,7 @@ package org.keycloak.validation;
import org.keycloak.models.ClientModel;
import org.keycloak.protocol.ProtocolMapperConfigException;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.grants.ciba.CibaClientValidation;
import org.keycloak.protocol.oidc.mappers.PairwiseSubMapperHelper;
import org.keycloak.protocol.oidc.utils.PairwiseSubMapperUtils;
import org.keycloak.protocol.oidc.utils.PairwiseSubMapperValidator;
@ -113,6 +114,7 @@ public class DefaultClientValidationProvider implements ClientValidationProvider
public ValidationResult validate(ValidationContext<ClientModel> context) {
validateUrls(context);
validatePairwiseInClientModel(context);
new CibaClientValidation(context).validate();
return context.toResult();
}
@ -121,6 +123,7 @@ public class DefaultClientValidationProvider implements ClientValidationProvider
public ValidationResult validate(ClientValidationContext.OIDCContext context) {
validateUrls(context);
validatePairwiseInOIDCClient(context);
new CibaClientValidation(context).validate();
return context.toResult();
}

View file

@ -24,6 +24,7 @@ import org.keycloak.common.util.HtmlUtils;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.models.KeycloakSession;
import org.keycloak.protocol.oidc.grants.ciba.endpoints.ClientNotificationEndpointRequest;
import org.keycloak.representations.LogoutToken;
import org.keycloak.representations.adapters.action.LogoutAction;
import org.keycloak.representations.adapters.action.PushNotBeforeAction;
@ -64,6 +65,7 @@ public class TestApplicationResourceProvider implements RealmResourceProvider {
private final TestApplicationResourceProviderFactory.OIDCClientData oidcClientData;
private final ConcurrentMap<String, TestAuthenticationChannelRequest> authenticationChannelRequests;
private final ConcurrentMap<String, ClientNotificationEndpointRequest> cibaClientNotifications;
@Context
HttpRequest request;
@ -73,7 +75,8 @@ public class TestApplicationResourceProvider implements RealmResourceProvider {
BlockingQueue<PushNotBeforeAction> adminPushNotBeforeActions,
BlockingQueue<TestAvailabilityAction> adminTestAvailabilityAction,
TestApplicationResourceProviderFactory.OIDCClientData oidcClientData,
ConcurrentMap<String, TestAuthenticationChannelRequest> authenticationChannelRequests) {
ConcurrentMap<String, TestAuthenticationChannelRequest> authenticationChannelRequests,
ConcurrentMap<String, ClientNotificationEndpointRequest> cibaClientNotifications) {
this.session = session;
this.adminLogoutActions = adminLogoutActions;
this.backChannelLogoutTokens = backChannelLogoutTokens;
@ -81,6 +84,7 @@ public class TestApplicationResourceProvider implements RealmResourceProvider {
this.adminTestAvailabilityAction = adminTestAvailabilityAction;
this.oidcClientData = oidcClientData;
this.authenticationChannelRequests = authenticationChannelRequests;
this.cibaClientNotifications = cibaClientNotifications;
}
@POST
@ -233,7 +237,7 @@ public class TestApplicationResourceProvider implements RealmResourceProvider {
@Path("/oidc-client-endpoints")
public TestingOIDCEndpointsApplicationResource getTestingOIDCClientEndpoints() {
return new TestingOIDCEndpointsApplicationResource(oidcClientData, authenticationChannelRequests);
return new TestingOIDCEndpointsApplicationResource(oidcClientData, authenticationChannelRequests, cibaClientNotifications);
}
@Override

View file

@ -24,6 +24,7 @@ import org.keycloak.crypto.KeyType;
import org.keycloak.crypto.KeyUse;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.protocol.oidc.grants.ciba.endpoints.ClientNotificationEndpointRequest;
import org.keycloak.representations.LogoutToken;
import org.keycloak.representations.adapters.action.LogoutAction;
import org.keycloak.representations.adapters.action.PushNotBeforeAction;
@ -50,12 +51,13 @@ public class TestApplicationResourceProviderFactory implements RealmResourceProv
private BlockingQueue<TestAvailabilityAction> testAvailabilityActions = new LinkedBlockingDeque<>();
private final OIDCClientData oidcClientData = new OIDCClientData();
private ConcurrentMap<String, TestAuthenticationChannelRequest> authenticationChannelRequests = new ConcurrentHashMap<String, TestAuthenticationChannelRequest>();
private ConcurrentMap<String, TestAuthenticationChannelRequest> authenticationChannelRequests = new ConcurrentHashMap<>();
private ConcurrentMap<String, ClientNotificationEndpointRequest> cibaClientNotifications = new ConcurrentHashMap<>();
@Override
public RealmResourceProvider create(KeycloakSession session) {
TestApplicationResourceProvider provider = new TestApplicationResourceProvider(session, adminLogoutActions,
backChannelLogoutTokens, pushNotBeforeActions, testAvailabilityActions, oidcClientData, authenticationChannelRequests);
backChannelLogoutTokens, pushNotBeforeActions, testAvailabilityActions, oidcClientData, authenticationChannelRequests, cibaClientNotifications);
ResteasyProviderFactory.getInstance().injectProperties(provider);

View file

@ -25,6 +25,7 @@ import javax.ws.rs.BadRequestException;
import javax.ws.rs.Consumes;
import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.common.util.Base64;
import org.keycloak.common.util.Base64Url;
import org.keycloak.common.util.KeyUtils;
@ -51,8 +52,10 @@ import org.keycloak.protocol.oidc.grants.ciba.CibaGrantType;
import org.keycloak.protocol.oidc.grants.ciba.channel.AuthenticationChannelRequest;
import org.keycloak.protocol.oidc.grants.ciba.channel.HttpAuthenticationChannelProvider;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.grants.ciba.endpoints.ClientNotificationEndpointRequest;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.testsuite.rest.TestApplicationResourceProviderFactory;
import org.keycloak.testsuite.rest.representation.TestAuthenticationChannelRequest;
@ -99,12 +102,14 @@ public class TestingOIDCEndpointsApplicationResource {
private final TestApplicationResourceProviderFactory.OIDCClientData clientData;
private final ConcurrentMap<String, TestAuthenticationChannelRequest> authenticationChannelRequests;
private final ConcurrentMap<String, ClientNotificationEndpointRequest> cibaClientNotifications;
public TestingOIDCEndpointsApplicationResource(TestApplicationResourceProviderFactory.OIDCClientData oidcClientData,
ConcurrentMap<String, TestAuthenticationChannelRequest> authenticationChannelRequests) {
ConcurrentMap<String, TestAuthenticationChannelRequest> authenticationChannelRequests, ConcurrentMap<String, ClientNotificationEndpointRequest> cibaClientNotifications) {
this.clientData = oidcClientData;
this.authenticationChannelRequests = authenticationChannelRequests;
this.cibaClientNotifications = cibaClientNotifications;
}
@GET
@ -694,4 +699,33 @@ public class TestingOIDCEndpointsApplicationResource {
}
private static final String ChannelRequestDummyKey = "channel_request_dummy_key";
@POST
@Path("/push-ciba-client-notification")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@NoCache
public Response cibaClientNotificationEndpoint(@Context HttpHeaders headers, ClientNotificationEndpointRequest request) {
String clientNotificationToken = AppAuthManager.extractAuthorizationHeaderToken(headers);
ClientNotificationEndpointRequest existing = cibaClientNotifications.putIfAbsent(clientNotificationToken, request);
if (existing != null) {
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "There is already entry for clientNotification " + clientNotificationToken + ". Make sure to cleanup after previous tests.",
Response.Status.BAD_REQUEST);
}
return Response.noContent().build();
}
@GET
@Path("/get-pushed-ciba-client-notification")
@Produces(MediaType.APPLICATION_JSON)
@NoCache
public ClientNotificationEndpointRequest getPushedCibaClientNotification(@QueryParam("clientNotificationToken") String clientNotificationToken) {
ClientNotificationEndpointRequest request = cibaClientNotifications.remove(clientNotificationToken);
if (request == null) {
request = new ClientNotificationEndpointRequest();
}
return request;
}
}

View file

@ -51,4 +51,11 @@ public class TestApplicationResourceUrls {
.path(TestOIDCEndpointsApplicationResource.class, "getSectorIdentifierRedirectUris");
return builder.build().toString();
}
public static String cibaClientNotificationEndpointUri() {
UriBuilder builder = oidcClientEndpoints()
.path(TestOIDCEndpointsApplicationResource.class, "cibaClientNotificationEndpoint");
return builder.build().toString();
}
}

View file

@ -19,6 +19,7 @@ package org.keycloak.testsuite.client.resources;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.protocol.oidc.grants.ciba.endpoints.ClientNotificationEndpointRequest;
import org.keycloak.testsuite.rest.representation.TestAuthenticationChannelRequest;
import javax.ws.rs.Consumes;
@ -118,4 +119,30 @@ public interface TestOIDCEndpointsApplicationResource {
@Produces(MediaType.APPLICATION_JSON)
TestAuthenticationChannelRequest getAuthenticationChannel(@QueryParam("bindingMessage") String bindingMessage);
/**
* Invoke client notification endpoint. This will be called by Keycloak itself (by CIBA callback endpoint) not by testsuite
* @param request
*/
@POST
@Path("/push-ciba-client-notification")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@NoCache
void cibaClientNotificationEndpoint(ClientNotificationEndpointRequest request);
/**
* Return the authReqId in case that clientNotificationEndpoint was already called by Keycloak for the given clientNotificationToken. Otherwise underlying value of
* authReqId field from the returned JSON will be null in case that clientNotificationEndpoint was not yet called for the given clientNotificationToken.
*
* Pushed client notification will be removed after calling this.
*
* @param clientNotificationToken
* @return
*/
@GET
@Path("/get-pushed-ciba-client-notification")
@Produces(MediaType.APPLICATION_JSON)
@NoCache
ClientNotificationEndpointRequest getPushedCibaClientNotification(@QueryParam("clientNotificationToken") String clientNotificationToken);
}

View file

@ -715,10 +715,10 @@ public class OAuthClient {
}
public AuthenticationRequestAcknowledgement doBackchannelAuthenticationRequest(String clientId, String clientSecret, String userid, String bindingMessage, String acrValues) throws Exception {
return doBackchannelAuthenticationRequest(clientId, clientSecret, userid, bindingMessage, acrValues, null);
return doBackchannelAuthenticationRequest(clientId, clientSecret, userid, bindingMessage, acrValues, null, null);
}
public AuthenticationRequestAcknowledgement doBackchannelAuthenticationRequest(String clientId, String clientSecret, String userid, String bindingMessage, String acrValues, Map<String, String> additionalParams) throws Exception {
public AuthenticationRequestAcknowledgement doBackchannelAuthenticationRequest(String clientId, String clientSecret, String userid, String bindingMessage, String acrValues, String clientNotificationToken, Map<String, String> additionalParams) throws Exception {
try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
HttpPost post = new HttpPost(getBackchannelAuthenticationUrl());
@ -729,6 +729,7 @@ public class OAuthClient {
if (userid != null) parameters.add(new BasicNameValuePair(LOGIN_HINT_PARAM, userid));
if (bindingMessage != null) parameters.add(new BasicNameValuePair(BINDING_MESSAGE, bindingMessage));
if (acrValues != null) parameters.add(new BasicNameValuePair(OAuth2Constants.ACR_VALUES, acrValues));
if (clientNotificationToken != null) parameters.add(new BasicNameValuePair(CibaGrantType.CLIENT_NOTIFICATION_TOKEN, clientNotificationToken));
if (scope != null) {
parameters.add(new BasicNameValuePair(OAuth2Constants.SCOPE, OAuth2Constants.SCOPE_OPENID + " " + scope));
} else {

View file

@ -21,6 +21,7 @@ import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.containsString;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.core.Response.Status;
import org.apache.http.client.methods.CloseableHttpResponse;
@ -79,6 +80,7 @@ import org.keycloak.protocol.oidc.grants.ciba.clientpolicy.executor.SecureCibaAu
import org.keycloak.protocol.oidc.grants.ciba.clientpolicy.executor.SecureCibaSessionEnforceExecutorFactory;
import org.keycloak.protocol.oidc.grants.ciba.clientpolicy.executor.SecureCibaSignedAuthenticationRequestExecutor;
import org.keycloak.protocol.oidc.grants.ciba.clientpolicy.executor.SecureCibaSignedAuthenticationRequestExecutorFactory;
import org.keycloak.protocol.oidc.grants.ciba.endpoints.ClientNotificationEndpointRequest;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.JsonWebToken;
@ -127,6 +129,8 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Tests for the CIBA "poll" mode and generic CIBA functionality tests
*
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
@AuthServerContainerExclude({REMOTE})
@ -702,6 +706,209 @@ public class CIBATest extends AbstractClientPoliciesTest {
testBackchannelAuthenticationFlow(true, null);
}
@Test
public void testBackchannelClientValidations() throws Exception {
ClientResource clientResource = null;
ClientRepresentation clientRep = null;
try {
// prepare CIBA settings
clientResource = ApiUtil.findClientByClientId(adminClient.realm(TEST_REALM_NAME), TEST_CLIENT_NAME);
assertThat(clientResource, notNullValue());
clientRep = clientResource.toRepresentation();
Map<String, String> attributes = Optional.ofNullable(clientRep.getAttributes()).orElse(new HashMap<>());
// "Ping" mode without clientNotificationURL should fail
try {
attributes.put(CibaConfig.CIBA_BACKCHANNEL_TOKEN_DELIVERY_MODE_PER_CLIENT, "ping");
attributes.put(CibaConfig.CIBA_BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT, null);
attributes.put(CibaConfig.OIDC_CIBA_GRANT_ENABLED, Boolean.TRUE.toString());
clientRep.setAttributes(attributes);
clientResource.update(clientRep);
Assert.fail("Not expected to successfully update client");
} catch (BadRequestException bre) {
// Expected
}
// "Ping" mode with clientNotificationURL should success
attributes.put(CibaConfig.CIBA_BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT, TestApplicationResourceUrls.cibaClientNotificationEndpointUri());
clientResource.update(clientRep);
clientRep = clientResource.toRepresentation();
attributes = Optional.ofNullable(clientRep.getAttributes()).orElse(new HashMap<>());
Assert.assertEquals("ping", attributes.get(CibaConfig.CIBA_BACKCHANNEL_TOKEN_DELIVERY_MODE_PER_CLIENT));
Assert.assertEquals(TestApplicationResourceUrls.cibaClientNotificationEndpointUri(), attributes.get(CibaConfig.CIBA_BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT));
// Update to "Push" mode should fail
try {
attributes.put(CibaConfig.CIBA_BACKCHANNEL_TOKEN_DELIVERY_MODE_PER_CLIENT, "push");
clientResource.update(clientRep);
Assert.fail("Not expected to successfully update client");
} catch (BadRequestException bre) {
// Expected
}
// Update to unsupported algorithm should fail
try {
attributes.put(CibaConfig.CIBA_BACKCHANNEL_TOKEN_DELIVERY_MODE_PER_CLIENT, "ping");
attributes.put(CibaConfig.CIBA_BACKCHANNEL_AUTH_REQUEST_SIGNING_ALG, Algorithm.HS256);
clientResource.update(clientRep);
Assert.fail("Not expected to successfully update client");
} catch (BadRequestException bre) {
// Expected
}
// Should pass
attributes.put(CibaConfig.CIBA_BACKCHANNEL_AUTH_REQUEST_SIGNING_ALG, Algorithm.PS256);
clientResource.update(clientRep);
clientRep = clientResource.toRepresentation();
attributes = Optional.ofNullable(clientRep.getAttributes()).orElse(new HashMap<>());
Assert.assertEquals(Algorithm.PS256, attributes.get(CibaConfig.CIBA_BACKCHANNEL_AUTH_REQUEST_SIGNING_ALG));
// Revert algorithm
attributes.remove(CibaConfig.CIBA_BACKCHANNEL_AUTH_REQUEST_SIGNING_ALG);
clientResource.update(clientRep);
} finally {
revertCIBASettings(clientResource, clientRep);
}
}
// PING MODE TESTS
@Test
public void testPingMode_requestWithInvalidClientNotificationShouldFail() throws Exception {
ClientResource clientResource = null;
ClientRepresentation clientRep = null;
try {
// prepare CIBA settings
clientResource = ApiUtil.findClientByClientId(adminClient.realm(TEST_REALM_NAME), TEST_CLIENT_NAME);
assertThat(clientResource, notNullValue());
clientRep = clientResource.toRepresentation();
prepareCIBASettings(clientResource, clientRep, "ping");
// Backchannel Authentication Request without client_notification should fail
AuthenticationRequestAcknowledgement response = oauth.doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, "nutzername-rot", "BASTION_PING", null,null, Collections.emptyMap());
assertThat(response.getStatusCode(), is(equalTo(400)));
assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST));
// Backchannel Authentication Request without client_notification should fail
String clientNotificationLongerThan1024Characters = "123456789123456789123465789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789_123456789123456789123465789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789_123456789123456789123465789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789_123456789123456789123465789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789_123456789123456789123465789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789_123456789123456789123465789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789_123456789123456789123465789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789_123456789123456789123465789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789_123456789123456789123465789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789_123456789123456789123465789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789_123456789123456789123465789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789_123456789123456789123465789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789";
response = oauth.doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, "nutzername-rot", "BASTION_PING", null,clientNotificationLongerThan1024Characters, Collections.emptyMap());
assertThat(response.getStatusCode(), is(equalTo(400)));
assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST));
} finally {
revertCIBASettings(clientResource, clientRep);
}
}
@Test
public void testPingModeSuccess() throws Exception {
ClientResource clientResource = null;
ClientRepresentation clientRep = null;
try {
final String username = "nutzername-rot";
final String bindingMessage = "BASTION_PING";
final String clientNotificationToken = "client-notification-token-1";
Map<String, String> additionalParameters = new HashMap<>();
additionalParameters.put("user_device", "mobile");
// prepare CIBA settings
clientResource = ApiUtil.findClientByClientId(adminClient.realm(TEST_REALM_NAME), TEST_CLIENT_NAME);
assertThat(clientResource, notNullValue());
clientRep = clientResource.toRepresentation();
prepareCIBASettings(clientResource, clientRep, "ping");
long startTime = Time.currentTime();
// user Backchannel Authentication Request
AuthenticationRequestAcknowledgement response = doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, username, bindingMessage, clientNotificationToken, additionalParameters);
Assert.assertTrue(response.getInterval() > 0); // Even in the ping mode should be interval set according to the CIBA specification
// user Authentication Channel Request
TestAuthenticationChannelRequest testRequest = doAuthenticationChannelRequest(bindingMessage);
AuthenticationChannelRequest authenticationChannelReq = testRequest.getRequest();
assertThat(authenticationChannelReq.getBindingMessage(), is(equalTo(bindingMessage)));
assertThat(authenticationChannelReq.getScope(), is(containsString(OAuth2Constants.SCOPE_OPENID)));
assertThat(authenticationChannelReq.getAdditionalParameters().get("user_device"), is(equalTo("mobile")));
// Check clientNotification not yet available
ClientNotificationEndpointRequest pushedClientNotification = testingClient.testApp().oidcClientEndpoints().getPushedCibaClientNotification(clientNotificationToken);
Assert.assertNull(pushedClientNotification.getAuthReqId());
// user Authentication Channel completed
EventRepresentation loginEvent = doAuthenticationChannelCallback(testRequest);
String sessionId = loginEvent.getSessionId();
String codeId = loginEvent.getDetails().get(Details.CODE_ID);
String userId = loginEvent.getUserId();
// Check clientNotification exists now for our authReqId
pushedClientNotification = testingClient.testApp().oidcClientEndpoints().getPushedCibaClientNotification(clientNotificationToken);
Assert.assertEquals(pushedClientNotification.getAuthReqId(), response.getAuthReqId());
// user Token Request
OAuthClient.AccessTokenResponse tokenRes = doBackchannelAuthenticationTokenRequest(username, response.getAuthReqId());
IDToken idToken = oauth.verifyIDToken(tokenRes.getIdToken());
long currentTime = Time.currentTime();
long authTime = idToken.getAuth_time().longValue();
assertTrue(startTime -5 <= authTime);
assertTrue(authTime <= currentTime + 5);
// token introspection
String tokenResponse = doIntrospectAccessTokenWithClientCredential(tokenRes, username);
// token refresh
tokenRes = doRefreshTokenRequest(tokenRes.getRefreshToken(), username, sessionId, false);
// token introspection after token refresh
tokenResponse = doIntrospectAccessTokenWithClientCredential(tokenRes, username);
// logout by refresh token
EventRepresentation logoutEvent = doLogoutByRefreshToken(tokenRes.getRefreshToken(), sessionId, userId, false);
} finally {
revertCIBASettings(clientResource, clientRep);
}
}
@Test
public void testPingMode_clientNotificationSentEvenForUserCancel() throws Exception {
ClientResource clientResource = null;
ClientRepresentation clientRep = null;
try {
final String username = "nutzername-rot";
// prepare CIBA settings
clientResource = ApiUtil.findClientByClientId(adminClient.realm(TEST_REALM_NAME), TEST_CLIENT_NAME);
assertThat(clientResource, notNullValue());
clientRep = clientResource.toRepresentation();
prepareCIBASettings(clientResource, clientRep, "ping");
// user Backchannel Authentication Request
AuthenticationRequestAcknowledgement response = doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, username, "kwq26rfjs73", "client-notification-some", Collections.emptyMap());
// user Authentication Channel Request
TestAuthenticationChannelRequest authenticationChannelReq = doAuthenticationChannelRequest("kwq26rfjs73");
// user Authentication Channel completed
doAuthenticationChannelCallbackError(Status.OK, TEST_CLIENT_NAME, authenticationChannelReq, CANCELLED, username, Errors.NOT_ALLOWED);
// Check client notification is present even if user cancelled authentication
ClientNotificationEndpointRequest pushedClientNotification = testingClient.testApp().oidcClientEndpoints().getPushedCibaClientNotification("client-notification-some");
Assert.assertEquals(pushedClientNotification.getAuthReqId(), response.getAuthReqId());
// user Token Request
OAuthClient.AccessTokenResponse tokenRes = oauth.doBackchannelAuthenticationTokenRequest(TEST_CLIENT_PASSWORD, response.getAuthReqId());
assertThat(tokenRes.getStatusCode(), is(equalTo(Status.BAD_REQUEST.getStatusCode())));
assertThat(tokenRes.getError(), is(OAuthErrorException.ACCESS_DENIED));
} finally {
revertCIBASettings(clientResource, clientRep);
}
}
@Test
public void testMultipleUsersBackchannelAuthenticationFlows() throws Exception {
ClientResource clientResource = null;
@ -1178,11 +1385,6 @@ public class CIBATest extends AbstractClientPoliciesTest {
testBackchannelAuthenticationFlowWithSignedAuthenticationRequest(true, Algorithm.ES256);
}
@Test
public void testBackchannelAuthenticationFlowWithInvalidSignedAuthenticationRequestParam() throws Exception {
testBackchannelAuthenticationFlowWithInvalidSignedAuthenticationRequest(false, Algorithm.HS256, 400, "Signed algorithm is not allowed");
}
@Test
public void testBackchannelAuthenticationFlowWithInvalidSignedAuthenticationRequestUriParam() throws Exception {
testBackchannelAuthenticationFlowWithInvalidSignedAuthenticationRequest(true, "none", 400, "None signed algorithm is not allowed");
@ -1221,7 +1423,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
updatePolicies(json);
// user Backchannel Authentication Request
AuthenticationRequestAcknowledgement response = oauth.doBackchannelAuthenticationRequest(clientId, clientSecret, username, null, null, additionalParameters);
AuthenticationRequestAcknowledgement response = oauth.doBackchannelAuthenticationRequest(clientId, clientSecret, username, null, null, null, additionalParameters);
assertThat(response.getStatusCode(), is(equalTo(400)));
assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST));
assertThat(response.getErrorDescription(), is("Missing parameter: binding_message"));
@ -1276,7 +1478,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
registerSharedAuthenticationRequest(requestObject, TEST_CLIENT_NAME, sigAlg, useRequestUri);
// user Backchannel Authentication Request
AuthenticationRequestAcknowledgement response = oauth.doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, username, bindingMessage, null, null);
AuthenticationRequestAcknowledgement response = oauth.doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, username, bindingMessage, null, null, null);
assertThat(response.getStatusCode(), is(equalTo(400)));
assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST));
assertThat(response.getErrorDescription(), is("Missing parameter in the signed authentication request: exp"));
@ -1289,7 +1491,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
registerSharedAuthenticationRequest(requestObject, TEST_CLIENT_NAME, sigAlg, useRequestUri);
// user Backchannel Authentication Request
response = oauth.doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, username, bindingMessage, null, null);
response = oauth.doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, username, bindingMessage, null, null, null);
assertThat(response.getStatusCode(), is(equalTo(400)));
assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST));
assertThat(response.getErrorDescription(), is("Missing parameter in the signed authentication request: nbf"));
@ -1303,7 +1505,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
registerSharedAuthenticationRequest(requestObject, TEST_CLIENT_NAME, sigAlg, useRequestUri);
// user Backchannel Authentication Request
response = oauth.doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, username, bindingMessage, null, null);
response = oauth.doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, username, bindingMessage, null, null, null);
assertThat(response.getStatusCode(), is(equalTo(400)));
assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST));
assertThat(response.getErrorDescription(), is("signed authentication request's available period is long"));
@ -1318,7 +1520,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
registerSharedAuthenticationRequest(requestObject, TEST_CLIENT_NAME, sigAlg, useRequestUri);
// user Backchannel Authentication Request
response = oauth.doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, username, bindingMessage, null, null);
response = oauth.doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, username, bindingMessage, null, null, null);
assertThat(response.getStatusCode(), is(equalTo(400)));
assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST));
assertThat(response.getErrorDescription(), is("Missing parameter in the 'request' object: aud"));
@ -1333,7 +1535,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
registerSharedAuthenticationRequest(requestObject, TEST_CLIENT_NAME, sigAlg, useRequestUri);
// user Backchannel Authentication Request
response = oauth.doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, username, bindingMessage, null, null);
response = oauth.doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, username, bindingMessage, null, null, null);
assertThat(response.getStatusCode(), is(equalTo(400)));
assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST));
assertThat(response.getErrorDescription(), is("Invalid parameter in the 'request' object: aud"));
@ -1348,7 +1550,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
registerSharedAuthenticationRequest(requestObject, TEST_CLIENT_NAME, sigAlg, useRequestUri);
// user Backchannel Authentication Request
response = oauth.doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, username, bindingMessage, null, null);
response = oauth.doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, username, bindingMessage, null, null, null);
assertThat(response.getStatusCode(), is(equalTo(400)));
assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST));
assertThat(response.getErrorDescription(), is("Missing parameter in the 'request' object: iss"));
@ -1364,7 +1566,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
registerSharedAuthenticationRequest(requestObject, TEST_CLIENT_NAME, sigAlg, useRequestUri);
// user Backchannel Authentication Request
response = oauth.doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, username, bindingMessage, null, null);
response = oauth.doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, username, bindingMessage, null, null, null);
assertThat(response.getStatusCode(), is(equalTo(400)));
assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST));
assertThat(response.getErrorDescription(), is("Invalid parameter in the 'request' object: iss"));
@ -1382,7 +1584,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
registerSharedAuthenticationRequest(requestObject, TEST_CLIENT_NAME, sigAlg, useRequestUri);
// user Backchannel Authentication Request
response = oauth.doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, username, bindingMessage, null, null);
response = oauth.doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, username, bindingMessage, null, null, null);
assertThat(response.getStatusCode(), is(equalTo(400)));
assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST));
assertThat(response.getErrorDescription(), is("Missing parameter in the signed authentication request: iat"));
@ -1400,7 +1602,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
registerSharedAuthenticationRequest(requestObject, TEST_CLIENT_NAME, sigAlg, useRequestUri);
// user Backchannel Authentication Request
response = oauth.doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, username, bindingMessage, null, null);
response = oauth.doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, username, bindingMessage, null, null, null);
assertThat(response.getStatusCode(), is(equalTo(400)));
assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST));
assertThat(response.getErrorDescription(), is("Missing parameter in the signed authentication request: jti"));
@ -1484,7 +1686,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
updatePolicies(json);
// user Backchannel Authentication Request
AuthenticationRequestAcknowledgement response = oauth.doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, username, null, null, null);
AuthenticationRequestAcknowledgement response = oauth.doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, username, null, null, null, null);
assertThat(response.getStatusCode(), is(equalTo(400)));
assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST));
assertThat(response.getErrorDescription(), is("Missing parameter: binding_message"));
@ -1519,7 +1721,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
}
@Test
public void testExtendedClientPolicyIntefacesForBackchannelAuthenticationRequest() throws Exception {
public void testExtendedClientPolicyInterfacesForBackchannelAuthenticationRequest() throws Exception {
String clientId = generateSuffixedName("confidential-app");
String clientSecret = "app-secret";
createClientByAdmin(clientId, (ClientRepresentation clientRep) -> {
@ -1557,14 +1759,14 @@ public class CIBATest extends AbstractClientPoliciesTest {
ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm(REALM_NAME), clientId);
clientResource.roles().create(RoleBuilder.create().name(roleName).build());
AuthenticationRequestAcknowledgement response = oauth.doBackchannelAuthenticationRequest(clientId, clientSecret, TEST_USER_NAME, "Pjb9eD8w", null, null);
AuthenticationRequestAcknowledgement response = oauth.doBackchannelAuthenticationRequest(clientId, clientSecret, TEST_USER_NAME, "Pjb9eD8w", null, null, null);
assertEquals(400, response.getStatusCode());
assertEquals(ClientPolicyEvent.BACKCHANNEL_AUTHENTICATION_REQUEST.toString(), response.getError());
assertEquals("Exception thrown intentionally", response.getErrorDescription());
}
@Test
public void testExtendedClientPolicyIntefacesForBackchannelTokenRequest() throws Exception {
public void testExtendedClientPolicyInterfacesForBackchannelTokenRequest() throws Exception {
String clientId = generateSuffixedName("confidential-app");
String clientSecret = "app-secret";
createClientByAdmin(clientId, (ClientRepresentation clientRep) -> {
@ -1584,7 +1786,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
additionalParameters.put("user_device", "mobile");
// user Backchannel Authentication Request
AuthenticationRequestAcknowledgement response = oauth.doBackchannelAuthenticationRequest(clientId, clientSecret, TEST_USER_NAME, bindingMessage, null, additionalParameters);
AuthenticationRequestAcknowledgement response = oauth.doBackchannelAuthenticationRequest(clientId, clientSecret, TEST_USER_NAME, bindingMessage, null, null, additionalParameters);
assertThat(response.getStatusCode(), is(equalTo(200)));
Assert.assertNotNull(response.getAuthReqId());
@ -1819,7 +2021,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
prepareCIBASettings(clientResource, clientRep);
// user Backchannel Authentication Request
AuthenticationRequestAcknowledgement response = doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, username, bindingMessage, additionalParameters);
AuthenticationRequestAcknowledgement response = doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, username, bindingMessage, null, additionalParameters);
// user Authentication Channel Request
TestAuthenticationChannelRequest testRequest = doAuthenticationChannelRequest(bindingMessage);
@ -1904,7 +2106,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
updatePolicies(json);
// user Backchannel Authentication Request
AuthenticationRequestAcknowledgement response = oauth.doBackchannelAuthenticationRequest(clientPublicId, "app-secret", username, bindingMessage, null, additionalParameters);
AuthenticationRequestAcknowledgement response = oauth.doBackchannelAuthenticationRequest(clientPublicId, "app-secret", username, bindingMessage, null, null, additionalParameters);
assertThat(response.getStatusCode(), is(equalTo(400)));
assertThat(response.getError(), is(OAuthErrorException.INVALID_CLIENT));
assertThat(response.getErrorDescription(), is("invalid client access type"));
@ -1924,7 +2126,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
});
// user Backchannel Authentication Request
response = doBackchannelAuthenticationRequest(clientConfidentialId, clientConfidentialSecret, username, bindingMessage, additionalParameters);
response = doBackchannelAuthenticationRequest(clientConfidentialId, clientConfidentialSecret, username, bindingMessage, null, additionalParameters);
updateClientByAdmin(cidConfidential, (ClientRepresentation cRep) -> {
cRep.setPublicClient(Boolean.TRUE);
@ -1980,14 +2182,14 @@ public class CIBATest extends AbstractClientPoliciesTest {
updatePolicies(json);
// user Backchannel Authentication Request
AuthenticationRequestAcknowledgement response = oauth.doBackchannelAuthenticationRequest(clientConfidentialId, clientConfidentialSecret, username, bindingMessage, null, additionalParameters);
AuthenticationRequestAcknowledgement response = oauth.doBackchannelAuthenticationRequest(clientConfidentialId, clientConfidentialSecret, username, bindingMessage, null, null, additionalParameters);
assertThat(response.getStatusCode(), is(equalTo(400)));
assertThat(response.getError(), is(ClientPolicyEvent.BACKCHANNEL_AUTHENTICATION_REQUEST.name()));
assertThat(response.getErrorDescription(), is("Exception thrown intentionally"));
updatePolicies("{}");
response = oauth.doBackchannelAuthenticationRequest(clientConfidentialId, clientConfidentialSecret, username, bindingMessage, null, additionalParameters);
response = oauth.doBackchannelAuthenticationRequest(clientConfidentialId, clientConfidentialSecret, username, bindingMessage, null, null, additionalParameters);
assertThat(response.getStatusCode(), is(equalTo(200)));
Assert.assertNotNull(response.getAuthReqId());
@ -2258,8 +2460,13 @@ public class CIBATest extends AbstractClientPoliciesTest {
}
private void prepareCIBASettings(ClientResource clientResource, ClientRepresentation clientRep) {
prepareCIBASettings(clientResource, clientRep, "poll");
}
private void prepareCIBASettings(ClientResource clientResource, ClientRepresentation clientRep, String mode) {
Map<String, String> attributes = Optional.ofNullable(clientRep.getAttributes()).orElse(new HashMap<>());
attributes.put(CibaConfig.CIBA_BACKCHANNEL_TOKEN_DELIVERY_MODE_PER_CLIENT, "poll");
attributes.put(CibaConfig.CIBA_BACKCHANNEL_TOKEN_DELIVERY_MODE_PER_CLIENT, mode);
attributes.put(CibaConfig.CIBA_BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT, TestApplicationResourceUrls.cibaClientNotificationEndpointUri());
attributes.put(CibaConfig.OIDC_CIBA_GRANT_ENABLED, Boolean.TRUE.toString());
clientRep.setAttributes(attributes);
List<String> requestUris = new ArrayList<>(OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).getRequestUris());
@ -2272,6 +2479,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
Map<String, String> attributes = Optional.ofNullable(clientRep.getAttributes()).orElse(new HashMap<>());
attributes.remove(CibaConfig.CIBA_BACKCHANNEL_TOKEN_DELIVERY_MODE_PER_CLIENT);
attributes.remove(CibaConfig.OIDC_CIBA_GRANT_ENABLED, Boolean.TRUE.toString());
attributes.remove(CibaConfig.CIBA_BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT);
clientRep.setAttributes(attributes);
List<String> requestUris = new ArrayList<>(OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).getRequestUris());
requestUris.remove(TestApplicationResourceUrls.clientRequestUri());
@ -2301,11 +2509,11 @@ public class CIBATest extends AbstractClientPoliciesTest {
}
private AuthenticationRequestAcknowledgement doBackchannelAuthenticationRequest(String clientId, String clientSecret, String username, String bindingMessage) throws Exception {
return doBackchannelAuthenticationRequest(clientId, clientSecret, username, bindingMessage, null);
return doBackchannelAuthenticationRequest(clientId, clientSecret, username, bindingMessage, null, null);
}
private AuthenticationRequestAcknowledgement doBackchannelAuthenticationRequest(String clientId, String clientSecret, String username, String bindingMessage, Map<String, String> additionalParameters) throws Exception {
AuthenticationRequestAcknowledgement response = oauth.doBackchannelAuthenticationRequest(clientId, clientSecret, username, bindingMessage, null, additionalParameters);
private AuthenticationRequestAcknowledgement doBackchannelAuthenticationRequest(String clientId, String clientSecret, String username, String bindingMessage, String clientNotificationToken, Map<String, String> additionalParameters) throws Exception {
AuthenticationRequestAcknowledgement response = oauth.doBackchannelAuthenticationRequest(clientId, clientSecret, username, bindingMessage, null, clientNotificationToken, additionalParameters);
assertThat(response.getStatusCode(), is(equalTo(200)));
Assert.assertNotNull(response.getAuthReqId());
return response;
@ -2498,7 +2706,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
long startTime = Time.currentTime();
// user Backchannel Authentication Request
AuthenticationRequestAcknowledgement response = doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, username, bindingMessage, additionalParameters);
AuthenticationRequestAcknowledgement response = doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, username, bindingMessage, null, additionalParameters);
// user Authentication Channel Request
TestAuthenticationChannelRequest testRequest = doAuthenticationChannelRequest(bindingMessage);

View file

@ -477,7 +477,7 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
ClientRepresentation kcClient = getClient(response.getClientId());
Assert.assertEquals("poll", kcClient.getAttributes().get(CibaConfig.CIBA_BACKCHANNEL_TOKEN_DELIVERY_MODE_PER_CLIENT));
// update
// Create with ping mode (failes due missing clientNotificationEndpoint)
clientRep.setBackchannelTokenDeliveryMode("ping");
try {
reg.oidc().create(clientRep);
@ -485,6 +485,21 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
} catch (ClientRegistrationException e) {
assertEquals(ERR_MSG_CLIENT_REG_FAIL, e.getMessage());
}
// Create with ping mode (success)
clientRep.setBackchannelClientNotificationEndpoint("https://foo/bar");
response = reg.oidc().create(clientRep);
Assert.assertEquals("ping", response.getBackchannelTokenDeliveryMode());
Assert.assertEquals("https://foo/bar", response.getBackchannelClientNotificationEndpoint());
// Create with push mode (fails)
clientRep.setBackchannelTokenDeliveryMode("push");
try {
reg.oidc().create(clientRep);
fail();
} catch (ClientRegistrationException e) {
assertEquals(ERR_MSG_CLIENT_REG_FAIL, e.getMessage());
}
}
@Test

View file

@ -179,7 +179,7 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest {
// CIBA
assertEquals(oidcConfig.getBackchannelAuthenticationEndpoint(), oauth.getBackchannelAuthenticationUrl());
assertContains(oidcConfig.getGrantTypesSupported(), OAuth2Constants.CIBA_GRANT_TYPE);
Assert.assertNames(oidcConfig.getBackchannelTokenDeliveryModesSupported(), "poll");
Assert.assertNames(oidcConfig.getBackchannelTokenDeliveryModesSupported(), "poll", "ping");
Assert.assertNames(oidcConfig.getBackchannelAuthenticationRequestSigningAlgValuesSupported(), Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512);
Assert.assertTrue(oidcConfig.getBackchannelLogoutSupported());

View file

@ -407,8 +407,12 @@ request-object-encryption-alg=Request Object Encryption Algorithm
request-object-encryption-alg.tooltip=JWE algorithm, which client needs to use when sending OIDC request object specified by 'request' or 'request_uri' parameters. If set to 'any', encryption is optional and any algorithm is allowed.
request-object-encryption-enc=Request Object Content Encryption Algorithm
request-object-encryption-enc.tooltip=JWE algorithm, which client needs to use when encrypting the content of the OIDC request object specified by 'request' or 'request_uri' parameters. If set to 'any', any algorithm is allowed.
ciba-backchannel-token-delivery-mode=CIBA Backchannel Token Delivery Mode
ciba-backchannel-token-delivery-mode.tooltip= CIBA mode, which will be used by this client. If not set, defaults to realm attribute set at the CIBA Policy (defaults to 'poll')
ciba-backchannel-client-notification-endpoint=CIBA Backchannel Client Notification Endpoint
ciba-backchannel-client-notification-endpoint.tooltip=Client Notification Endpoint URL used by the CIBA Ping mode.
ciba-backchannel-auth-request-signing-alg=CIBA Backchannel Authentication Request Signature Algorithm
ciba-backchannel-auth-request-signing-alg.tooltip=JWA algorithm, which client needs to use when sending CIBA backchannel authentication request specified by 'request' or 'request_uri' parameters.
ciba-backchannel-auth-request-signing-alg.tooltip=JWA algorithm, which client needs to use when sending CIBA backchannel authentication request specified by 'request' or 'request_uri' parameters. Only asymmetric algorithms are allowed according CIBA specification. If set to 'any', any algorithm is allowed.
request-uris=Valid Request URIs
request-uris.tooltip=List of valid URIs, which can be used as values of 'request_uri' parameter during OpenID Connect authentication request. There is support for the same capabilities like for Valid Redirect URIs. For example wildcards or relative paths.
fine-saml-endpoint-conf=Fine Grain SAML Endpoint Configuration
@ -1347,7 +1351,7 @@ public-key-credential-aaguid=Public Key Credential AAGUID
public-key-credential-label=Public Key Credential Label
ciba-policy=CIBA Policy
ciba-backchannel-tokendelivery-mode=Backchannel Token Delivery Mode
ciba-backchannel-tokendelivery-mode.tooltip=Specifies how the CD(Consumption Device) gets the authentication result and related tokens.
ciba-backchannel-tokendelivery-mode.tooltip=Specifies how the CD(Consumption Device) gets the authentication result and related tokens. This mode will be used by default for the CIBA clients, which do not have other mode explicitly set. The default mode is 'poll'.
ciba-expires-in=Expires In
ciba-expires-in.tooltip=The expiration time of the "auth_req_id" in seconds since the authentication request was received.
ciba-interval=Interval

View file

@ -1320,6 +1320,9 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
var attrVal7 = $scope.client.attributes['request.object.encryption.enc'];
$scope.requestObjectEncryptionEnc = attrVal7==null ? 'any' : attrVal7;
var attrVal8 = $scope.client.attributes['ciba.backchannel.auth.request.signing.alg'];
$scope.cibaBackchannelAuthRequestSigningAlg = attrVal8==null ? 'any' : attrVal8;
if ($scope.client.attributes["exclude.session.state.from.auth.response"]) {
if ($scope.client.attributes["exclude.session.state.from.auth.response"] == "true") {
$scope.excludeSessionStateFromAuthResponse = true;
@ -1344,6 +1347,8 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
}
}
$scope.cibaBackchannelTokenDeliveryMode = $scope.client.attributes['ciba.backchannel.token.delivery.mode'];
if ($scope.client.attributes["use.refresh.tokens"]) {
if ($scope.client.attributes["use.refresh.tokens"] == "true") {
$scope.useRefreshTokens = true;
@ -1554,13 +1559,17 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
};
$scope.changeCibaBackchannelAuthRequestSigningAlg = function() {
if ($scope.cibaBackchannelAuthRequestSigningAlg === 'none') {
if ($scope.cibaBackchannelAuthRequestSigningAlg === 'any') {
$scope.clientEdit.attributes['ciba.backchannel.auth.request.signing.alg'] = null;
} else {
$scope.clientEdit.attributes['ciba.backchannel.auth.request.signing.alg'] = $scope.cibaBackchannelAuthRequestSigningAlg;
}
};
$scope.changeCibaBackchannelTokenDeliveryMode = function() {
$scope.clientEdit.attributes['ciba.backchannel.token.delivery.mode'] = $scope.cibaBackchannelTokenDeliveryMode;
};
$scope.changeAuthorizationSignedResponseAlg = function() {
$scope.clientEdit.attributes['authorization.signed.response.alg'] = $scope.authorizationSignedResponseAlg;
};

View file

@ -9,7 +9,8 @@
<div class="col-md-2">
<div>
<select id="tokendelivery" ng-model="realm.attributes.cibaBackchannelTokenDeliveryMode" class="form-control">
<option value="poll">Poll</option>
<option value="poll">poll</option>
<option value="ping">ping</option>
</select>
</div>
</div>

View file

@ -600,14 +600,35 @@
</div>
<kc-tooltip>{{:: 'request-object-required.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix block" data-ng-show="protocol == 'openid-connect' && !clientEdit.publicClient && !clientEdit.bearerOnly && serverInfo.featureEnabled('CIBA') && oidcCibaGrantEnabled == true"">
<div class="form-group clearfix block" data-ng-show="protocol == 'openid-connect' && !clientEdit.publicClient && !clientEdit.bearerOnly && serverInfo.featureEnabled('CIBA') && oidcCibaGrantEnabled == true">
<label class="col-md-2 control-label" for="cibaBackchannelTokenDeliveryMode">{{:: 'ciba-backchannel-token-delivery-mode' | translate}}</label>
<div class="col-sm-6">
<div>
<select class="form-control" id="cibaBackchannelTokenDeliveryMode"
ng-change="changeCibaBackchannelTokenDeliveryMode()"
ng-model="cibaBackchannelTokenDeliveryMode">
<option value="poll">poll</option>
<option value="ping">ping</option>
</select>
</div>
</div>
<kc-tooltip>{{:: 'ciba-backchannel-token-delivery-mode.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix block" data-ng-show="protocol == 'openid-connect' && !clientEdit.publicClient && !clientEdit.bearerOnly && serverInfo.featureEnabled('CIBA') && oidcCibaGrantEnabled == true && cibaBackchannelTokenDeliveryMode == 'ping'">
<label class="col-md-2 control-label" for="cibaBackchannelClientNotificationEndpoint">{{:: 'ciba-backchannel-client-notification-endpoint' | translate}}</label>
<div class="col-sm-6">
<input ng-model="clientEdit.attributes['ciba.backchannel.client.notification.endpoint']" class="form-control" type="text" name="cibaBackchannelClientNotificationEndpoint" id="cibaBackchannelClientNotificationEndpoint" />
</div>
<kc-tooltip>{{:: 'ciba-backchannel-client-notification-endpoint.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix block" data-ng-show="protocol == 'openid-connect' && !clientEdit.publicClient && !clientEdit.bearerOnly && serverInfo.featureEnabled('CIBA') && oidcCibaGrantEnabled == true">
<label class="col-md-2 control-label" for="cibaBackchannelAuthRequestSigningAlg">{{:: 'ciba-backchannel-auth-request-signing-alg' | translate}}</label>
<div class="col-sm-6">
<div>
<select class="form-control" id="cibaBackchannelAuthRequestSigningAlg"
ng-change="changeCibaBackchannelAuthRequestSigningAlg()"
ng-model="cibaBackchannelAuthRequestSigningAlg">
<option value="none">none</option>
<option value="any">any</option>
<option ng-repeat="provider in serverInfo.listProviderIds('clientSignature')" value="{{provider}}">{{provider}}</option>
</select>
</div>