KEYCLOAK-18594 CIBA Ping Mode
This commit is contained in:
parent
2418e31952
commit
643b3c4c5a
26 changed files with 692 additions and 92 deletions
|
@ -128,6 +128,8 @@ public class OIDCClientRepresentation {
|
||||||
// OIDC CIBA
|
// OIDC CIBA
|
||||||
private String backchannel_token_delivery_mode;
|
private String backchannel_token_delivery_mode;
|
||||||
|
|
||||||
|
private String backchannel_client_notification_endpoint;
|
||||||
|
|
||||||
private String backchannel_authentication_request_signing_alg;
|
private String backchannel_authentication_request_signing_alg;
|
||||||
|
|
||||||
// FAPI JARM
|
// FAPI JARM
|
||||||
|
@ -510,6 +512,14 @@ public class OIDCClientRepresentation {
|
||||||
this.backchannel_token_delivery_mode = backchannel_token_delivery_mode;
|
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() {
|
public String getBackchannelAuthenticationRequestSigningAlg() {
|
||||||
return backchannel_authentication_request_signing_alg;
|
return backchannel_authentication_request_signing_alg;
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,8 @@ public class OAuth2DeviceCodeModel {
|
||||||
private static final String POLLING_INTERVAL_NOTE = "int";
|
private static final String POLLING_INTERVAL_NOTE = "int";
|
||||||
private static final String NONCE_NOTE = "nonce";
|
private static final String NONCE_NOTE = "nonce";
|
||||||
private static final String SCOPE_NOTE = "scope";
|
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 USER_SESSION_ID_NOTE = "uid";
|
||||||
private static final String DENIED_NOTE = "denied";
|
private static final String DENIED_NOTE = "denied";
|
||||||
private static final String ADDITIONAL_PARAM_PREFIX = "additional_param_";
|
private static final String ADDITIONAL_PARAM_PREFIX = "additional_param_";
|
||||||
|
@ -43,6 +45,8 @@ public class OAuth2DeviceCodeModel {
|
||||||
private final String deviceCode;
|
private final String deviceCode;
|
||||||
private final int expiration;
|
private final int expiration;
|
||||||
private final int pollingInterval;
|
private final int pollingInterval;
|
||||||
|
private final String clientNotificationToken;
|
||||||
|
private final String authReqId;
|
||||||
private final String scope;
|
private final String scope;
|
||||||
private final String nonce;
|
private final String nonce;
|
||||||
private final String userSessionId;
|
private final String userSessionId;
|
||||||
|
@ -50,30 +54,31 @@ public class OAuth2DeviceCodeModel {
|
||||||
private final Map<String, String> additionalParams;
|
private final Map<String, String> additionalParams;
|
||||||
|
|
||||||
public static OAuth2DeviceCodeModel create(RealmModel realm, ClientModel client,
|
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;
|
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) {
|
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) {
|
public OAuth2DeviceCodeModel approve(String userSessionId, Map<String, String> additionalParams) {
|
||||||
if (additionalParams != null) {
|
if (additionalParams != null) {
|
||||||
this.additionalParams.putAll(additionalParams);
|
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() {
|
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,
|
private OAuth2DeviceCodeModel(RealmModel realm, String clientId,
|
||||||
String deviceCode, String scope, String nonce, int expiration, int pollingInterval,
|
String deviceCode, String scope, String nonce, int expiration, int pollingInterval, String clientNotificationToken,
|
||||||
String userSessionId, Boolean denied, Map<String, String> additionalParams) {
|
String authReqId, String userSessionId, Boolean denied, Map<String, String> additionalParams) {
|
||||||
this.realm = realm;
|
this.realm = realm;
|
||||||
this.clientId = clientId;
|
this.clientId = clientId;
|
||||||
this.deviceCode = deviceCode;
|
this.deviceCode = deviceCode;
|
||||||
|
@ -81,6 +86,8 @@ public class OAuth2DeviceCodeModel {
|
||||||
this.nonce = nonce;
|
this.nonce = nonce;
|
||||||
this.expiration = expiration;
|
this.expiration = expiration;
|
||||||
this.pollingInterval = pollingInterval;
|
this.pollingInterval = pollingInterval;
|
||||||
|
this.clientNotificationToken = clientNotificationToken;
|
||||||
|
this.authReqId = authReqId;
|
||||||
this.userSessionId = userSessionId;
|
this.userSessionId = userSessionId;
|
||||||
this.denied = denied;
|
this.denied = denied;
|
||||||
this.additionalParams = additionalParams;
|
this.additionalParams = additionalParams;
|
||||||
|
@ -98,8 +105,8 @@ public class OAuth2DeviceCodeModel {
|
||||||
|
|
||||||
private OAuth2DeviceCodeModel(RealmModel realm, String deviceCode, Map<String, String> data) {
|
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),
|
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),
|
Integer.parseInt(data.get(EXPIRATION_NOTE)), Integer.parseInt(data.get(POLLING_INTERVAL_NOTE)), data.get(CLIENT_NOTIFICATION_TOKEN_NOTE),
|
||||||
Boolean.parseBoolean(data.get(DENIED_NOTE)), extractAdditionalParams(data));
|
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) {
|
private static Map<String, String> extractAdditionalParams(Map<String, String> data) {
|
||||||
|
@ -132,6 +139,14 @@ public class OAuth2DeviceCodeModel {
|
||||||
return pollingInterval;
|
return pollingInterval;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getClientNotificationToken() {
|
||||||
|
return clientNotificationToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAuthReqId() {
|
||||||
|
return authReqId;
|
||||||
|
}
|
||||||
|
|
||||||
public String getClientId() {
|
public String getClientId() {
|
||||||
return clientId;
|
return clientId;
|
||||||
}
|
}
|
||||||
|
@ -164,6 +179,12 @@ public class OAuth2DeviceCodeModel {
|
||||||
Map<String, String> result = new HashMap<>();
|
Map<String, String> result = new HashMap<>();
|
||||||
|
|
||||||
result.put(REALM_ID, realm.getId());
|
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) {
|
if (denied == null) {
|
||||||
result.put(CLIENT_ID, clientId);
|
result.put(CLIENT_ID, clientId);
|
||||||
|
|
|
@ -16,11 +16,20 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.models;
|
package org.keycloak.models;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import org.keycloak.jose.jws.Algorithm;
|
import org.keycloak.jose.jws.Algorithm;
|
||||||
import org.keycloak.utils.StringUtil;
|
import org.keycloak.utils.StringUtil;
|
||||||
|
|
||||||
public class CibaConfig extends AbstractConfig {
|
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
|
// realm attribute names
|
||||||
public static final String CIBA_BACKCHANNEL_TOKEN_DELIVERY_MODE = "cibaBackchannelTokenDeliveryMode";
|
public static final String CIBA_BACKCHANNEL_TOKEN_DELIVERY_MODE = "cibaBackchannelTokenDeliveryMode";
|
||||||
public static final String CIBA_EXPIRES_IN = "cibaExpiresIn";
|
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";
|
public static final String CIBA_AUTH_REQUESTED_USER_HINT = "cibaAuthRequestedUserHint";
|
||||||
|
|
||||||
// default value
|
// 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_EXPIRES_IN = 120;
|
||||||
public static final int DEFAULT_CIBA_POLICY_INTERVAL = 5;
|
public static final int DEFAULT_CIBA_POLICY_INTERVAL = 5;
|
||||||
public static final String DEFAULT_CIBA_POLICY_AUTH_REQUESTED_USER_HINT = "login_hint";
|
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
|
// client attribute names
|
||||||
public static final String OIDC_CIBA_GRANT_ENABLED = "oidc.ciba.grant.enabled";
|
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_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 static final String CIBA_BACKCHANNEL_AUTH_REQUEST_SIGNING_ALG = "ciba.backchannel.auth.request.signing.alg";
|
||||||
|
|
||||||
public CibaConfig(RealmModel realm) {
|
public CibaConfig(RealmModel realm) {
|
||||||
|
@ -146,4 +156,8 @@ public class CibaConfig extends AbstractConfig {
|
||||||
String alg = client.getAttribute(CIBA_BACKCHANNEL_AUTH_REQUEST_SIGNING_ALG);
|
String alg = client.getAttribute(CIBA_BACKCHANNEL_AUTH_REQUEST_SIGNING_ALG);
|
||||||
return alg==null ? null : Enum.valueOf(Algorithm.class, alg);
|
return alg==null ? null : Enum.valueOf(Algorithm.class, alg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getBackchannelClientNotificationEndpoint(ClientModel client) {
|
||||||
|
return client.getAttribute(CIBA_BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,8 +87,6 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
|
||||||
// KEYCLOAK-7451 OAuth Authorization Server Metadata for Proof Key for Code Exchange
|
// 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_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;
|
private KeycloakSession session;
|
||||||
|
|
||||||
public OIDCWellKnownProvider(KeycloakSession session) {
|
public OIDCWellKnownProvider(KeycloakSession session) {
|
||||||
|
@ -181,7 +179,7 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
|
||||||
config.setBackchannelLogoutSupported(true);
|
config.setBackchannelLogoutSupported(true);
|
||||||
config.setBackchannelLogoutSessionSupported(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.setBackchannelAuthenticationEndpoint(CibaGrantType.authorizationUrl(backendUriInfo.getBaseUriBuilder()).build(realm.getName()).toString());
|
||||||
config.setBackchannelAuthenticationRequestSigningAlgValuesSupported(getSupportedBackchannelAuthenticationRequestSigningAlgorithms());
|
config.setBackchannelAuthenticationRequestSigningAlgValuesSupported(getSupportedBackchannelAuthenticationRequestSigningAlgorithms());
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -91,6 +91,9 @@ public class CIBAAuthenticationRequest extends JsonWebToken {
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
protected ClientModel client;
|
protected ClientModel client;
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
protected String clientNotificationToken;
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
protected UserModel user;
|
protected UserModel user;
|
||||||
|
|
||||||
|
@ -171,6 +174,14 @@ public class CIBAAuthenticationRequest extends JsonWebToken {
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getClientNotificationToken() {
|
||||||
|
return clientNotificationToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClientNotificationToken(String clientNotificationToken) {
|
||||||
|
this.clientNotificationToken = clientNotificationToken;
|
||||||
|
}
|
||||||
|
|
||||||
public void setUser(UserModel user) {
|
public void setUser(UserModel user) {
|
||||||
this.user = user;
|
this.user = user;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,13 +16,16 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.protocol.oidc.grants.ciba.endpoints;
|
package org.keycloak.protocol.oidc.grants.ciba.endpoints;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||||
import org.jboss.resteasy.spi.HttpRequest;
|
import org.jboss.resteasy.spi.HttpRequest;
|
||||||
import org.keycloak.OAuthErrorException;
|
import org.keycloak.OAuthErrorException;
|
||||||
import org.keycloak.TokenVerifier;
|
import org.keycloak.TokenVerifier;
|
||||||
|
import org.keycloak.broker.provider.util.SimpleHttp;
|
||||||
import org.keycloak.events.Errors;
|
import org.keycloak.events.Errors;
|
||||||
import org.keycloak.events.EventBuilder;
|
import org.keycloak.events.EventBuilder;
|
||||||
import org.keycloak.events.EventType;
|
import org.keycloak.events.EventType;
|
||||||
|
import org.keycloak.models.CibaConfig;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.OAuth2DeviceCodeModel;
|
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.HttpHeaders;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.keycloak.protocol.oidc.grants.ciba.channel.AuthenticationChannelResponse.Status.CANCELLED;
|
import static org.keycloak.protocol.oidc.grants.ciba.channel.AuthenticationChannelResponse.Status.CANCELLED;
|
||||||
|
|
||||||
public class BackchannelAuthenticationCallbackEndpoint extends AbstractCibaEndpoint {
|
public class BackchannelAuthenticationCallbackEndpoint extends AbstractCibaEndpoint {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(BackchannelAuthenticationCallbackEndpoint.class);
|
||||||
|
|
||||||
@Context
|
@Context
|
||||||
private HttpRequest httpRequest;
|
private HttpRequest httpRequest;
|
||||||
|
|
||||||
|
@ -62,7 +69,10 @@ public class BackchannelAuthenticationCallbackEndpoint extends AbstractCibaEndpo
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
public Response processAuthenticationChannelResult(AuthenticationChannelResponse response) {
|
public Response processAuthenticationChannelResult(AuthenticationChannelResponse response) {
|
||||||
event.event(EventType.LOGIN);
|
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();
|
Status status = response.getStatus();
|
||||||
|
|
||||||
if (status == null) {
|
if (status == null) {
|
||||||
|
@ -81,10 +91,17 @@ public class BackchannelAuthenticationCallbackEndpoint extends AbstractCibaEndpo
|
||||||
break;
|
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();
|
return Response.ok(MediaType.APPLICATION_JSON_TYPE).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private AccessToken verifyAuthenticationRequest(HttpHeaders headers) {
|
private BackchannelAuthCallbackContext verifyAuthenticationRequest(HttpHeaders headers) {
|
||||||
String rawBearerToken = AppAuthManager.extractAuthorizationHeaderTokenOrReturnNull(headers);
|
String rawBearerToken = AppAuthManager.extractAuthorizationHeaderTokenOrReturnNull(headers);
|
||||||
|
|
||||||
if (rawBearerToken == null) {
|
if (rawBearerToken == null) {
|
||||||
|
@ -130,9 +147,10 @@ public class BackchannelAuthenticationCallbackEndpoint extends AbstractCibaEndpo
|
||||||
Response.Status.BAD_REQUEST);
|
Response.Status.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
session.getContext().setClient(issuedFor);
|
||||||
event.client(issuedFor);
|
event.client(issuedFor);
|
||||||
|
|
||||||
return bearerToken;
|
return new BackchannelAuthCallbackContext(bearerToken, deviceCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void cancelRequest(String authResultId) {
|
private void cancelRequest(String authResultId) {
|
||||||
|
@ -158,4 +176,52 @@ public class BackchannelAuthenticationCallbackEndpoint extends AbstractCibaEndpo
|
||||||
|
|
||||||
store.deny(realm, authReqId.getId());
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,6 +51,7 @@ import javax.ws.rs.core.Context;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.MultivaluedMap;
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@ -96,7 +97,7 @@ public class BackchannelAuthenticationEndpoint extends AbstractCibaEndpoint {
|
||||||
CibaConfig cibaPolicy = realm.getCibaPolicy();
|
CibaConfig cibaPolicy = realm.getCibaPolicy();
|
||||||
int poolingInterval = cibaPolicy.getPoolingInterval();
|
int poolingInterval = cibaPolicy.getPoolingInterval();
|
||||||
|
|
||||||
storeAuthenticationRequest(request, cibaPolicy);
|
storeAuthenticationRequest(request, cibaPolicy, authReqId);
|
||||||
|
|
||||||
ObjectNode response = JsonSerialization.createObjectNode();
|
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
|
* 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.
|
* 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();
|
ClientModel client = request.getClient();
|
||||||
int expiresIn = cibaConfig.getExpiresIn();
|
int expiresIn = cibaConfig.getExpiresIn();
|
||||||
int poolingInterval = cibaConfig.getPoolingInterval();
|
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,
|
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());
|
Collections.emptyMap());
|
||||||
String authResultId = request.getAuthResultId();
|
String authResultId = request.getAuthResultId();
|
||||||
OAuth2DeviceUserCodeModel userCode = new OAuth2DeviceUserCodeModel(realm, deviceCode.getDeviceCode(),
|
OAuth2DeviceUserCodeModel userCode = new OAuth2DeviceUserCodeModel(realm, deviceCode.getDeviceCode(),
|
||||||
|
@ -184,8 +191,19 @@ public class BackchannelAuthenticationEndpoint extends AbstractCibaEndpoint {
|
||||||
request.setScope(scopes.toString());
|
request.setScope(scopes.toString());
|
||||||
|
|
||||||
if (endpointRequest.getClientNotificationToken() != null) {
|
if (endpointRequest.getClientNotificationToken() != null) {
|
||||||
|
if (!policy.getBackchannelTokenDeliveryMode(client).equals(CibaConfig.CIBA_PING_MODE)) {
|
||||||
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST,
|
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) {
|
if (endpointRequest.getUserCode() != null) {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -125,7 +125,7 @@ public class DeviceEndpoint extends AuthorizationEndpointBase implements RealmRe
|
||||||
int interval = realm.getOAuth2DeviceConfig().getPoolingInterval(client);
|
int interval = realm.getOAuth2DeviceConfig().getPoolingInterval(client);
|
||||||
|
|
||||||
OAuth2DeviceCodeModel deviceCode = OAuth2DeviceCodeModel.create(realm, 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());
|
request.getAdditionalReqParams());
|
||||||
OAuth2DeviceUserCodeProvider userCodeProvider = session.getProvider(OAuth2DeviceUserCodeProvider.class);
|
OAuth2DeviceUserCodeProvider userCodeProvider = session.getProvider(OAuth2DeviceUserCodeProvider.class);
|
||||||
String secret = userCodeProvider.generate();
|
String secret = userCodeProvider.generate();
|
||||||
|
|
|
@ -24,6 +24,7 @@ import java.util.Optional;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.OAuthErrorException;
|
import org.keycloak.OAuthErrorException;
|
||||||
|
import org.keycloak.models.CibaConfig;
|
||||||
import org.keycloak.models.Constants;
|
import org.keycloak.models.Constants;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
|
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
|
||||||
|
@ -113,6 +114,10 @@ public class SecureClientUrisExecutor implements ClientPolicyExecutorProvider<Cl
|
||||||
// OIDD : requestUris
|
// OIDD : requestUris
|
||||||
List<String> requestUris = getAttributeMultivalued(clientRep, OIDCConfigAttributes.REQUEST_URIS);
|
List<String> requestUris = getAttributeMultivalued(clientRep, OIDCConfigAttributes.REQUEST_URIS);
|
||||||
if (requestUris != null) confirmSecureUris(requestUris, "requestUris");
|
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) {
|
private List<String> getAttributeMultivalued(ClientRepresentation clientRep, String attrKey) {
|
||||||
|
|
|
@ -64,6 +64,7 @@ import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
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.OAuth2DeviceConfig.OAUTH2_DEVICE_AUTHORIZATION_GRANT_ENABLED;
|
||||||
import static org.keycloak.models.CibaConfig.OIDC_CIBA_GRANT_ENABLED;
|
import static org.keycloak.models.CibaConfig.OIDC_CIBA_GRANT_ENABLED;
|
||||||
|
|
||||||
|
@ -187,27 +188,27 @@ public class DescriptionConverter {
|
||||||
configWrapper.setBackchannelLogoutRevokeOfflineTokens(clientOIDC.getBackchannelLogoutRevokeOfflineTokens());
|
configWrapper.setBackchannelLogoutRevokeOfflineTokens(clientOIDC.getBackchannelLogoutRevokeOfflineTokens());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CIBA
|
||||||
String backchannelTokenDeliveryMode = clientOIDC.getBackchannelTokenDeliveryMode();
|
String backchannelTokenDeliveryMode = clientOIDC.getBackchannelTokenDeliveryMode();
|
||||||
if (backchannelTokenDeliveryMode != null) {
|
if (backchannelTokenDeliveryMode != null) {
|
||||||
if(isSupportedBackchannelTokenDeliveryMode(backchannelTokenDeliveryMode)) {
|
|
||||||
Map<String, String> attr = Optional.ofNullable(client.getAttributes()).orElse(new HashMap<>());
|
Map<String, String> attr = Optional.ofNullable(client.getAttributes()).orElse(new HashMap<>());
|
||||||
attr.put(CibaConfig.CIBA_BACKCHANNEL_TOKEN_DELIVERY_MODE_PER_CLIENT, backchannelTokenDeliveryMode);
|
attr.put(CibaConfig.CIBA_BACKCHANNEL_TOKEN_DELIVERY_MODE_PER_CLIENT, backchannelTokenDeliveryMode);
|
||||||
client.setAttributes(attr);
|
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();
|
String backchannelAuthenticationRequestSigningAlg = clientOIDC.getBackchannelAuthenticationRequestSigningAlg();
|
||||||
if (backchannelAuthenticationRequestSigningAlg != null) {
|
if (backchannelAuthenticationRequestSigningAlg != null) {
|
||||||
if(isSupportedBackchannelAuthenticationRequestSigningAlg(session, backchannelAuthenticationRequestSigningAlg)) {
|
|
||||||
Map<String, String> attr = Optional.ofNullable(client.getAttributes()).orElse(new HashMap<>());
|
Map<String, String> attr = Optional.ofNullable(client.getAttributes()).orElse(new HashMap<>());
|
||||||
attr.put(CibaConfig.CIBA_BACKCHANNEL_AUTH_REQUEST_SIGNING_ALG, backchannelAuthenticationRequestSigningAlg);
|
attr.put(CibaConfig.CIBA_BACKCHANNEL_AUTH_REQUEST_SIGNING_ALG, backchannelAuthenticationRequestSigningAlg);
|
||||||
client.setAttributes(attr);
|
client.setAttributes(attr);
|
||||||
} else {
|
|
||||||
throw new ClientRegistrationException("Unsupported requested backchannel_authentication_request_signing_alg");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PAR
|
||||||
Boolean requirePushedAuthorizationRequests = clientOIDC.getRequirePushedAuthorizationRequests();
|
Boolean requirePushedAuthorizationRequests = clientOIDC.getRequirePushedAuthorizationRequests();
|
||||||
if (requirePushedAuthorizationRequests != null) {
|
if (requirePushedAuthorizationRequests != null) {
|
||||||
Map<String, String> attr = Optional.ofNullable(client.getAttributes()).orElse(new HashMap<>());
|
Map<String, String> attr = Optional.ofNullable(client.getAttributes()).orElse(new HashMap<>());
|
||||||
|
@ -225,18 +226,6 @@ public class DescriptionConverter {
|
||||||
client.setAttributes(attributes);
|
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) {
|
private static List<String> getSupportedAlgorithms(KeycloakSession session, Class<? extends Provider> clazz, boolean includeNone) {
|
||||||
Stream<String> supportedAlgorithms = session.getKeycloakSessionFactory().getProviderFactoriesStream(clazz)
|
Stream<String> supportedAlgorithms = session.getKeycloakSessionFactory().getProviderFactoriesStream(clazz)
|
||||||
.map(ProviderFactory::getId);
|
.map(ProviderFactory::getId);
|
||||||
|
@ -372,6 +361,10 @@ public class DescriptionConverter {
|
||||||
if (StringUtil.isNotBlank(mode)) {
|
if (StringUtil.isNotBlank(mode)) {
|
||||||
response.setBackchannelTokenDeliveryMode(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);
|
String alg = client.getAttributes().get(CibaConfig.CIBA_BACKCHANNEL_AUTH_REQUEST_SIGNING_ALG);
|
||||||
if (StringUtil.isNotBlank(alg)) {
|
if (StringUtil.isNotBlank(alg)) {
|
||||||
response.setBackchannelAuthenticationRequestSigningAlg(alg);
|
response.setBackchannelAuthenticationRequestSigningAlg(alg);
|
||||||
|
|
|
@ -19,6 +19,7 @@ package org.keycloak.validation;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.protocol.ProtocolMapperConfigException;
|
import org.keycloak.protocol.ProtocolMapperConfigException;
|
||||||
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
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.mappers.PairwiseSubMapperHelper;
|
||||||
import org.keycloak.protocol.oidc.utils.PairwiseSubMapperUtils;
|
import org.keycloak.protocol.oidc.utils.PairwiseSubMapperUtils;
|
||||||
import org.keycloak.protocol.oidc.utils.PairwiseSubMapperValidator;
|
import org.keycloak.protocol.oidc.utils.PairwiseSubMapperValidator;
|
||||||
|
@ -113,6 +114,7 @@ public class DefaultClientValidationProvider implements ClientValidationProvider
|
||||||
public ValidationResult validate(ValidationContext<ClientModel> context) {
|
public ValidationResult validate(ValidationContext<ClientModel> context) {
|
||||||
validateUrls(context);
|
validateUrls(context);
|
||||||
validatePairwiseInClientModel(context);
|
validatePairwiseInClientModel(context);
|
||||||
|
new CibaClientValidation(context).validate();
|
||||||
|
|
||||||
return context.toResult();
|
return context.toResult();
|
||||||
}
|
}
|
||||||
|
@ -121,6 +123,7 @@ public class DefaultClientValidationProvider implements ClientValidationProvider
|
||||||
public ValidationResult validate(ClientValidationContext.OIDCContext context) {
|
public ValidationResult validate(ClientValidationContext.OIDCContext context) {
|
||||||
validateUrls(context);
|
validateUrls(context);
|
||||||
validatePairwiseInOIDCClient(context);
|
validatePairwiseInOIDCClient(context);
|
||||||
|
new CibaClientValidation(context).validate();
|
||||||
|
|
||||||
return context.toResult();
|
return context.toResult();
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import org.keycloak.common.util.HtmlUtils;
|
||||||
import org.keycloak.jose.jws.JWSInput;
|
import org.keycloak.jose.jws.JWSInput;
|
||||||
import org.keycloak.jose.jws.JWSInputException;
|
import org.keycloak.jose.jws.JWSInputException;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.protocol.oidc.grants.ciba.endpoints.ClientNotificationEndpointRequest;
|
||||||
import org.keycloak.representations.LogoutToken;
|
import org.keycloak.representations.LogoutToken;
|
||||||
import org.keycloak.representations.adapters.action.LogoutAction;
|
import org.keycloak.representations.adapters.action.LogoutAction;
|
||||||
import org.keycloak.representations.adapters.action.PushNotBeforeAction;
|
import org.keycloak.representations.adapters.action.PushNotBeforeAction;
|
||||||
|
@ -64,6 +65,7 @@ public class TestApplicationResourceProvider implements RealmResourceProvider {
|
||||||
private final TestApplicationResourceProviderFactory.OIDCClientData oidcClientData;
|
private final TestApplicationResourceProviderFactory.OIDCClientData oidcClientData;
|
||||||
|
|
||||||
private final ConcurrentMap<String, TestAuthenticationChannelRequest> authenticationChannelRequests;
|
private final ConcurrentMap<String, TestAuthenticationChannelRequest> authenticationChannelRequests;
|
||||||
|
private final ConcurrentMap<String, ClientNotificationEndpointRequest> cibaClientNotifications;
|
||||||
|
|
||||||
@Context
|
@Context
|
||||||
HttpRequest request;
|
HttpRequest request;
|
||||||
|
@ -73,7 +75,8 @@ public class TestApplicationResourceProvider implements RealmResourceProvider {
|
||||||
BlockingQueue<PushNotBeforeAction> adminPushNotBeforeActions,
|
BlockingQueue<PushNotBeforeAction> adminPushNotBeforeActions,
|
||||||
BlockingQueue<TestAvailabilityAction> adminTestAvailabilityAction,
|
BlockingQueue<TestAvailabilityAction> adminTestAvailabilityAction,
|
||||||
TestApplicationResourceProviderFactory.OIDCClientData oidcClientData,
|
TestApplicationResourceProviderFactory.OIDCClientData oidcClientData,
|
||||||
ConcurrentMap<String, TestAuthenticationChannelRequest> authenticationChannelRequests) {
|
ConcurrentMap<String, TestAuthenticationChannelRequest> authenticationChannelRequests,
|
||||||
|
ConcurrentMap<String, ClientNotificationEndpointRequest> cibaClientNotifications) {
|
||||||
this.session = session;
|
this.session = session;
|
||||||
this.adminLogoutActions = adminLogoutActions;
|
this.adminLogoutActions = adminLogoutActions;
|
||||||
this.backChannelLogoutTokens = backChannelLogoutTokens;
|
this.backChannelLogoutTokens = backChannelLogoutTokens;
|
||||||
|
@ -81,6 +84,7 @@ public class TestApplicationResourceProvider implements RealmResourceProvider {
|
||||||
this.adminTestAvailabilityAction = adminTestAvailabilityAction;
|
this.adminTestAvailabilityAction = adminTestAvailabilityAction;
|
||||||
this.oidcClientData = oidcClientData;
|
this.oidcClientData = oidcClientData;
|
||||||
this.authenticationChannelRequests = authenticationChannelRequests;
|
this.authenticationChannelRequests = authenticationChannelRequests;
|
||||||
|
this.cibaClientNotifications = cibaClientNotifications;
|
||||||
}
|
}
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
|
@ -233,7 +237,7 @@ public class TestApplicationResourceProvider implements RealmResourceProvider {
|
||||||
|
|
||||||
@Path("/oidc-client-endpoints")
|
@Path("/oidc-client-endpoints")
|
||||||
public TestingOIDCEndpointsApplicationResource getTestingOIDCClientEndpoints() {
|
public TestingOIDCEndpointsApplicationResource getTestingOIDCClientEndpoints() {
|
||||||
return new TestingOIDCEndpointsApplicationResource(oidcClientData, authenticationChannelRequests);
|
return new TestingOIDCEndpointsApplicationResource(oidcClientData, authenticationChannelRequests, cibaClientNotifications);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -24,6 +24,7 @@ import org.keycloak.crypto.KeyType;
|
||||||
import org.keycloak.crypto.KeyUse;
|
import org.keycloak.crypto.KeyUse;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
import org.keycloak.protocol.oidc.grants.ciba.endpoints.ClientNotificationEndpointRequest;
|
||||||
import org.keycloak.representations.LogoutToken;
|
import org.keycloak.representations.LogoutToken;
|
||||||
import org.keycloak.representations.adapters.action.LogoutAction;
|
import org.keycloak.representations.adapters.action.LogoutAction;
|
||||||
import org.keycloak.representations.adapters.action.PushNotBeforeAction;
|
import org.keycloak.representations.adapters.action.PushNotBeforeAction;
|
||||||
|
@ -50,12 +51,13 @@ public class TestApplicationResourceProviderFactory implements RealmResourceProv
|
||||||
private BlockingQueue<TestAvailabilityAction> testAvailabilityActions = new LinkedBlockingDeque<>();
|
private BlockingQueue<TestAvailabilityAction> testAvailabilityActions = new LinkedBlockingDeque<>();
|
||||||
|
|
||||||
private final OIDCClientData oidcClientData = new OIDCClientData();
|
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
|
@Override
|
||||||
public RealmResourceProvider create(KeycloakSession session) {
|
public RealmResourceProvider create(KeycloakSession session) {
|
||||||
TestApplicationResourceProvider provider = new TestApplicationResourceProvider(session, adminLogoutActions,
|
TestApplicationResourceProvider provider = new TestApplicationResourceProvider(session, adminLogoutActions,
|
||||||
backChannelLogoutTokens, pushNotBeforeActions, testAvailabilityActions, oidcClientData, authenticationChannelRequests);
|
backChannelLogoutTokens, pushNotBeforeActions, testAvailabilityActions, oidcClientData, authenticationChannelRequests, cibaClientNotifications);
|
||||||
|
|
||||||
ResteasyProviderFactory.getInstance().injectProperties(provider);
|
ResteasyProviderFactory.getInstance().injectProperties(provider);
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ import javax.ws.rs.BadRequestException;
|
||||||
import javax.ws.rs.Consumes;
|
import javax.ws.rs.Consumes;
|
||||||
|
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
|
import org.keycloak.OAuthErrorException;
|
||||||
import org.keycloak.common.util.Base64;
|
import org.keycloak.common.util.Base64;
|
||||||
import org.keycloak.common.util.Base64Url;
|
import org.keycloak.common.util.Base64Url;
|
||||||
import org.keycloak.common.util.KeyUtils;
|
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.AuthenticationChannelRequest;
|
||||||
import org.keycloak.protocol.oidc.grants.ciba.channel.HttpAuthenticationChannelProvider;
|
import org.keycloak.protocol.oidc.grants.ciba.channel.HttpAuthenticationChannelProvider;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
|
import org.keycloak.protocol.oidc.grants.ciba.endpoints.ClientNotificationEndpointRequest;
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.representations.AccessToken;
|
||||||
import org.keycloak.representations.JsonWebToken;
|
import org.keycloak.representations.JsonWebToken;
|
||||||
|
import org.keycloak.services.ErrorResponseException;
|
||||||
import org.keycloak.services.managers.AppAuthManager;
|
import org.keycloak.services.managers.AppAuthManager;
|
||||||
import org.keycloak.testsuite.rest.TestApplicationResourceProviderFactory;
|
import org.keycloak.testsuite.rest.TestApplicationResourceProviderFactory;
|
||||||
import org.keycloak.testsuite.rest.representation.TestAuthenticationChannelRequest;
|
import org.keycloak.testsuite.rest.representation.TestAuthenticationChannelRequest;
|
||||||
|
@ -99,12 +102,14 @@ public class TestingOIDCEndpointsApplicationResource {
|
||||||
|
|
||||||
private final TestApplicationResourceProviderFactory.OIDCClientData clientData;
|
private final TestApplicationResourceProviderFactory.OIDCClientData clientData;
|
||||||
private final ConcurrentMap<String, TestAuthenticationChannelRequest> authenticationChannelRequests;
|
private final ConcurrentMap<String, TestAuthenticationChannelRequest> authenticationChannelRequests;
|
||||||
|
private final ConcurrentMap<String, ClientNotificationEndpointRequest> cibaClientNotifications;
|
||||||
|
|
||||||
|
|
||||||
public TestingOIDCEndpointsApplicationResource(TestApplicationResourceProviderFactory.OIDCClientData oidcClientData,
|
public TestingOIDCEndpointsApplicationResource(TestApplicationResourceProviderFactory.OIDCClientData oidcClientData,
|
||||||
ConcurrentMap<String, TestAuthenticationChannelRequest> authenticationChannelRequests) {
|
ConcurrentMap<String, TestAuthenticationChannelRequest> authenticationChannelRequests, ConcurrentMap<String, ClientNotificationEndpointRequest> cibaClientNotifications) {
|
||||||
this.clientData = oidcClientData;
|
this.clientData = oidcClientData;
|
||||||
this.authenticationChannelRequests = authenticationChannelRequests;
|
this.authenticationChannelRequests = authenticationChannelRequests;
|
||||||
|
this.cibaClientNotifications = cibaClientNotifications;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
|
@ -694,4 +699,33 @@ public class TestingOIDCEndpointsApplicationResource {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String ChannelRequestDummyKey = "channel_request_dummy_key";
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,4 +51,11 @@ public class TestApplicationResourceUrls {
|
||||||
.path(TestOIDCEndpointsApplicationResource.class, "getSectorIdentifierRedirectUris");
|
.path(TestOIDCEndpointsApplicationResource.class, "getSectorIdentifierRedirectUris");
|
||||||
return builder.build().toString();
|
return builder.build().toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String cibaClientNotificationEndpointUri() {
|
||||||
|
UriBuilder builder = oidcClientEndpoints()
|
||||||
|
.path(TestOIDCEndpointsApplicationResource.class, "cibaClientNotificationEndpoint");
|
||||||
|
|
||||||
|
return builder.build().toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ package org.keycloak.testsuite.client.resources;
|
||||||
|
|
||||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||||
import org.keycloak.jose.jwk.JSONWebKeySet;
|
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||||
|
import org.keycloak.protocol.oidc.grants.ciba.endpoints.ClientNotificationEndpointRequest;
|
||||||
import org.keycloak.testsuite.rest.representation.TestAuthenticationChannelRequest;
|
import org.keycloak.testsuite.rest.representation.TestAuthenticationChannelRequest;
|
||||||
|
|
||||||
import javax.ws.rs.Consumes;
|
import javax.ws.rs.Consumes;
|
||||||
|
@ -118,4 +119,30 @@ public interface TestOIDCEndpointsApplicationResource {
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
TestAuthenticationChannelRequest getAuthenticationChannel(@QueryParam("bindingMessage") String bindingMessage);
|
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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -715,10 +715,10 @@ public class OAuthClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
public AuthenticationRequestAcknowledgement doBackchannelAuthenticationRequest(String clientId, String clientSecret, String userid, String bindingMessage, String acrValues) throws Exception {
|
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()) {
|
try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
|
||||||
HttpPost post = new HttpPost(getBackchannelAuthenticationUrl());
|
HttpPost post = new HttpPost(getBackchannelAuthenticationUrl());
|
||||||
|
|
||||||
|
@ -729,6 +729,7 @@ public class OAuthClient {
|
||||||
if (userid != null) parameters.add(new BasicNameValuePair(LOGIN_HINT_PARAM, userid));
|
if (userid != null) parameters.add(new BasicNameValuePair(LOGIN_HINT_PARAM, userid));
|
||||||
if (bindingMessage != null) parameters.add(new BasicNameValuePair(BINDING_MESSAGE, bindingMessage));
|
if (bindingMessage != null) parameters.add(new BasicNameValuePair(BINDING_MESSAGE, bindingMessage));
|
||||||
if (acrValues != null) parameters.add(new BasicNameValuePair(OAuth2Constants.ACR_VALUES, acrValues));
|
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) {
|
if (scope != null) {
|
||||||
parameters.add(new BasicNameValuePair(OAuth2Constants.SCOPE, OAuth2Constants.SCOPE_OPENID + " " + scope));
|
parameters.add(new BasicNameValuePair(OAuth2Constants.SCOPE, OAuth2Constants.SCOPE_OPENID + " " + scope));
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -21,6 +21,7 @@ import static org.hamcrest.Matchers.equalTo;
|
||||||
import static org.hamcrest.Matchers.is;
|
import static org.hamcrest.Matchers.is;
|
||||||
import static org.hamcrest.Matchers.containsString;
|
import static org.hamcrest.Matchers.containsString;
|
||||||
|
|
||||||
|
import javax.ws.rs.BadRequestException;
|
||||||
import javax.ws.rs.core.Response.Status;
|
import javax.ws.rs.core.Response.Status;
|
||||||
|
|
||||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
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.SecureCibaSessionEnforceExecutorFactory;
|
||||||
import org.keycloak.protocol.oidc.grants.ciba.clientpolicy.executor.SecureCibaSignedAuthenticationRequestExecutor;
|
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.clientpolicy.executor.SecureCibaSignedAuthenticationRequestExecutorFactory;
|
||||||
|
import org.keycloak.protocol.oidc.grants.ciba.endpoints.ClientNotificationEndpointRequest;
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.representations.AccessToken;
|
||||||
import org.keycloak.representations.IDToken;
|
import org.keycloak.representations.IDToken;
|
||||||
import org.keycloak.representations.JsonWebToken;
|
import org.keycloak.representations.JsonWebToken;
|
||||||
|
@ -127,6 +129,8 @@ import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
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>
|
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
|
||||||
*/
|
*/
|
||||||
@AuthServerContainerExclude({REMOTE})
|
@AuthServerContainerExclude({REMOTE})
|
||||||
|
@ -702,6 +706,209 @@ public class CIBATest extends AbstractClientPoliciesTest {
|
||||||
testBackchannelAuthenticationFlow(true, null);
|
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
|
@Test
|
||||||
public void testMultipleUsersBackchannelAuthenticationFlows() throws Exception {
|
public void testMultipleUsersBackchannelAuthenticationFlows() throws Exception {
|
||||||
ClientResource clientResource = null;
|
ClientResource clientResource = null;
|
||||||
|
@ -1178,11 +1385,6 @@ public class CIBATest extends AbstractClientPoliciesTest {
|
||||||
testBackchannelAuthenticationFlowWithSignedAuthenticationRequest(true, Algorithm.ES256);
|
testBackchannelAuthenticationFlowWithSignedAuthenticationRequest(true, Algorithm.ES256);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testBackchannelAuthenticationFlowWithInvalidSignedAuthenticationRequestParam() throws Exception {
|
|
||||||
testBackchannelAuthenticationFlowWithInvalidSignedAuthenticationRequest(false, Algorithm.HS256, 400, "Signed algorithm is not allowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testBackchannelAuthenticationFlowWithInvalidSignedAuthenticationRequestUriParam() throws Exception {
|
public void testBackchannelAuthenticationFlowWithInvalidSignedAuthenticationRequestUriParam() throws Exception {
|
||||||
testBackchannelAuthenticationFlowWithInvalidSignedAuthenticationRequest(true, "none", 400, "None signed algorithm is not allowed");
|
testBackchannelAuthenticationFlowWithInvalidSignedAuthenticationRequest(true, "none", 400, "None signed algorithm is not allowed");
|
||||||
|
@ -1221,7 +1423,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
|
||||||
updatePolicies(json);
|
updatePolicies(json);
|
||||||
|
|
||||||
// user Backchannel Authentication Request
|
// 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.getStatusCode(), is(equalTo(400)));
|
||||||
assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST));
|
assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST));
|
||||||
assertThat(response.getErrorDescription(), is("Missing parameter: binding_message"));
|
assertThat(response.getErrorDescription(), is("Missing parameter: binding_message"));
|
||||||
|
@ -1276,7 +1478,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
|
||||||
registerSharedAuthenticationRequest(requestObject, TEST_CLIENT_NAME, sigAlg, useRequestUri);
|
registerSharedAuthenticationRequest(requestObject, TEST_CLIENT_NAME, sigAlg, useRequestUri);
|
||||||
|
|
||||||
// user Backchannel Authentication Request
|
// 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.getStatusCode(), is(equalTo(400)));
|
||||||
assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST));
|
assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST));
|
||||||
assertThat(response.getErrorDescription(), is("Missing parameter in the signed authentication request: exp"));
|
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);
|
registerSharedAuthenticationRequest(requestObject, TEST_CLIENT_NAME, sigAlg, useRequestUri);
|
||||||
|
|
||||||
// user Backchannel Authentication Request
|
// 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.getStatusCode(), is(equalTo(400)));
|
||||||
assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST));
|
assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST));
|
||||||
assertThat(response.getErrorDescription(), is("Missing parameter in the signed authentication request: nbf"));
|
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);
|
registerSharedAuthenticationRequest(requestObject, TEST_CLIENT_NAME, sigAlg, useRequestUri);
|
||||||
|
|
||||||
// user Backchannel Authentication Request
|
// 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.getStatusCode(), is(equalTo(400)));
|
||||||
assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST));
|
assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST));
|
||||||
assertThat(response.getErrorDescription(), is("signed authentication request's available period is long"));
|
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);
|
registerSharedAuthenticationRequest(requestObject, TEST_CLIENT_NAME, sigAlg, useRequestUri);
|
||||||
|
|
||||||
// user Backchannel Authentication Request
|
// 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.getStatusCode(), is(equalTo(400)));
|
||||||
assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST));
|
assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST));
|
||||||
assertThat(response.getErrorDescription(), is("Missing parameter in the 'request' object: aud"));
|
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);
|
registerSharedAuthenticationRequest(requestObject, TEST_CLIENT_NAME, sigAlg, useRequestUri);
|
||||||
|
|
||||||
// user Backchannel Authentication Request
|
// 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.getStatusCode(), is(equalTo(400)));
|
||||||
assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST));
|
assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST));
|
||||||
assertThat(response.getErrorDescription(), is("Invalid parameter in the 'request' object: aud"));
|
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);
|
registerSharedAuthenticationRequest(requestObject, TEST_CLIENT_NAME, sigAlg, useRequestUri);
|
||||||
|
|
||||||
// user Backchannel Authentication Request
|
// 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.getStatusCode(), is(equalTo(400)));
|
||||||
assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST));
|
assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST));
|
||||||
assertThat(response.getErrorDescription(), is("Missing parameter in the 'request' object: iss"));
|
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);
|
registerSharedAuthenticationRequest(requestObject, TEST_CLIENT_NAME, sigAlg, useRequestUri);
|
||||||
|
|
||||||
// user Backchannel Authentication Request
|
// 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.getStatusCode(), is(equalTo(400)));
|
||||||
assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST));
|
assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST));
|
||||||
assertThat(response.getErrorDescription(), is("Invalid parameter in the 'request' object: iss"));
|
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);
|
registerSharedAuthenticationRequest(requestObject, TEST_CLIENT_NAME, sigAlg, useRequestUri);
|
||||||
|
|
||||||
// user Backchannel Authentication Request
|
// 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.getStatusCode(), is(equalTo(400)));
|
||||||
assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST));
|
assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST));
|
||||||
assertThat(response.getErrorDescription(), is("Missing parameter in the signed authentication request: iat"));
|
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);
|
registerSharedAuthenticationRequest(requestObject, TEST_CLIENT_NAME, sigAlg, useRequestUri);
|
||||||
|
|
||||||
// user Backchannel Authentication Request
|
// 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.getStatusCode(), is(equalTo(400)));
|
||||||
assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST));
|
assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST));
|
||||||
assertThat(response.getErrorDescription(), is("Missing parameter in the signed authentication request: jti"));
|
assertThat(response.getErrorDescription(), is("Missing parameter in the signed authentication request: jti"));
|
||||||
|
@ -1484,7 +1686,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
|
||||||
updatePolicies(json);
|
updatePolicies(json);
|
||||||
|
|
||||||
// user Backchannel Authentication Request
|
// 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.getStatusCode(), is(equalTo(400)));
|
||||||
assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST));
|
assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST));
|
||||||
assertThat(response.getErrorDescription(), is("Missing parameter: binding_message"));
|
assertThat(response.getErrorDescription(), is("Missing parameter: binding_message"));
|
||||||
|
@ -1519,7 +1721,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testExtendedClientPolicyIntefacesForBackchannelAuthenticationRequest() throws Exception {
|
public void testExtendedClientPolicyInterfacesForBackchannelAuthenticationRequest() throws Exception {
|
||||||
String clientId = generateSuffixedName("confidential-app");
|
String clientId = generateSuffixedName("confidential-app");
|
||||||
String clientSecret = "app-secret";
|
String clientSecret = "app-secret";
|
||||||
createClientByAdmin(clientId, (ClientRepresentation clientRep) -> {
|
createClientByAdmin(clientId, (ClientRepresentation clientRep) -> {
|
||||||
|
@ -1557,14 +1759,14 @@ public class CIBATest extends AbstractClientPoliciesTest {
|
||||||
ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm(REALM_NAME), clientId);
|
ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm(REALM_NAME), clientId);
|
||||||
clientResource.roles().create(RoleBuilder.create().name(roleName).build());
|
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(400, response.getStatusCode());
|
||||||
assertEquals(ClientPolicyEvent.BACKCHANNEL_AUTHENTICATION_REQUEST.toString(), response.getError());
|
assertEquals(ClientPolicyEvent.BACKCHANNEL_AUTHENTICATION_REQUEST.toString(), response.getError());
|
||||||
assertEquals("Exception thrown intentionally", response.getErrorDescription());
|
assertEquals("Exception thrown intentionally", response.getErrorDescription());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testExtendedClientPolicyIntefacesForBackchannelTokenRequest() throws Exception {
|
public void testExtendedClientPolicyInterfacesForBackchannelTokenRequest() throws Exception {
|
||||||
String clientId = generateSuffixedName("confidential-app");
|
String clientId = generateSuffixedName("confidential-app");
|
||||||
String clientSecret = "app-secret";
|
String clientSecret = "app-secret";
|
||||||
createClientByAdmin(clientId, (ClientRepresentation clientRep) -> {
|
createClientByAdmin(clientId, (ClientRepresentation clientRep) -> {
|
||||||
|
@ -1584,7 +1786,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
|
||||||
additionalParameters.put("user_device", "mobile");
|
additionalParameters.put("user_device", "mobile");
|
||||||
|
|
||||||
// user Backchannel Authentication Request
|
// 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)));
|
assertThat(response.getStatusCode(), is(equalTo(200)));
|
||||||
Assert.assertNotNull(response.getAuthReqId());
|
Assert.assertNotNull(response.getAuthReqId());
|
||||||
|
|
||||||
|
@ -1819,7 +2021,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
|
||||||
prepareCIBASettings(clientResource, clientRep);
|
prepareCIBASettings(clientResource, clientRep);
|
||||||
|
|
||||||
// user Backchannel Authentication Request
|
// 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
|
// user Authentication Channel Request
|
||||||
TestAuthenticationChannelRequest testRequest = doAuthenticationChannelRequest(bindingMessage);
|
TestAuthenticationChannelRequest testRequest = doAuthenticationChannelRequest(bindingMessage);
|
||||||
|
@ -1904,7 +2106,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
|
||||||
updatePolicies(json);
|
updatePolicies(json);
|
||||||
|
|
||||||
// user Backchannel Authentication Request
|
// 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.getStatusCode(), is(equalTo(400)));
|
||||||
assertThat(response.getError(), is(OAuthErrorException.INVALID_CLIENT));
|
assertThat(response.getError(), is(OAuthErrorException.INVALID_CLIENT));
|
||||||
assertThat(response.getErrorDescription(), is("invalid client access type"));
|
assertThat(response.getErrorDescription(), is("invalid client access type"));
|
||||||
|
@ -1924,7 +2126,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
|
||||||
});
|
});
|
||||||
|
|
||||||
// user Backchannel Authentication Request
|
// user Backchannel Authentication Request
|
||||||
response = doBackchannelAuthenticationRequest(clientConfidentialId, clientConfidentialSecret, username, bindingMessage, additionalParameters);
|
response = doBackchannelAuthenticationRequest(clientConfidentialId, clientConfidentialSecret, username, bindingMessage, null, additionalParameters);
|
||||||
|
|
||||||
updateClientByAdmin(cidConfidential, (ClientRepresentation cRep) -> {
|
updateClientByAdmin(cidConfidential, (ClientRepresentation cRep) -> {
|
||||||
cRep.setPublicClient(Boolean.TRUE);
|
cRep.setPublicClient(Boolean.TRUE);
|
||||||
|
@ -1980,14 +2182,14 @@ public class CIBATest extends AbstractClientPoliciesTest {
|
||||||
updatePolicies(json);
|
updatePolicies(json);
|
||||||
|
|
||||||
// user Backchannel Authentication Request
|
// 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.getStatusCode(), is(equalTo(400)));
|
||||||
assertThat(response.getError(), is(ClientPolicyEvent.BACKCHANNEL_AUTHENTICATION_REQUEST.name()));
|
assertThat(response.getError(), is(ClientPolicyEvent.BACKCHANNEL_AUTHENTICATION_REQUEST.name()));
|
||||||
assertThat(response.getErrorDescription(), is("Exception thrown intentionally"));
|
assertThat(response.getErrorDescription(), is("Exception thrown intentionally"));
|
||||||
|
|
||||||
updatePolicies("{}");
|
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)));
|
assertThat(response.getStatusCode(), is(equalTo(200)));
|
||||||
Assert.assertNotNull(response.getAuthReqId());
|
Assert.assertNotNull(response.getAuthReqId());
|
||||||
|
|
||||||
|
@ -2258,8 +2460,13 @@ public class CIBATest extends AbstractClientPoliciesTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void prepareCIBASettings(ClientResource clientResource, ClientRepresentation clientRep) {
|
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<>());
|
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());
|
attributes.put(CibaConfig.OIDC_CIBA_GRANT_ENABLED, Boolean.TRUE.toString());
|
||||||
clientRep.setAttributes(attributes);
|
clientRep.setAttributes(attributes);
|
||||||
List<String> requestUris = new ArrayList<>(OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).getRequestUris());
|
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<>());
|
Map<String, String> attributes = Optional.ofNullable(clientRep.getAttributes()).orElse(new HashMap<>());
|
||||||
attributes.remove(CibaConfig.CIBA_BACKCHANNEL_TOKEN_DELIVERY_MODE_PER_CLIENT);
|
attributes.remove(CibaConfig.CIBA_BACKCHANNEL_TOKEN_DELIVERY_MODE_PER_CLIENT);
|
||||||
attributes.remove(CibaConfig.OIDC_CIBA_GRANT_ENABLED, Boolean.TRUE.toString());
|
attributes.remove(CibaConfig.OIDC_CIBA_GRANT_ENABLED, Boolean.TRUE.toString());
|
||||||
|
attributes.remove(CibaConfig.CIBA_BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT);
|
||||||
clientRep.setAttributes(attributes);
|
clientRep.setAttributes(attributes);
|
||||||
List<String> requestUris = new ArrayList<>(OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).getRequestUris());
|
List<String> requestUris = new ArrayList<>(OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).getRequestUris());
|
||||||
requestUris.remove(TestApplicationResourceUrls.clientRequestUri());
|
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 {
|
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 {
|
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, additionalParameters);
|
AuthenticationRequestAcknowledgement response = oauth.doBackchannelAuthenticationRequest(clientId, clientSecret, username, bindingMessage, null, clientNotificationToken, additionalParameters);
|
||||||
assertThat(response.getStatusCode(), is(equalTo(200)));
|
assertThat(response.getStatusCode(), is(equalTo(200)));
|
||||||
Assert.assertNotNull(response.getAuthReqId());
|
Assert.assertNotNull(response.getAuthReqId());
|
||||||
return response;
|
return response;
|
||||||
|
@ -2498,7 +2706,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
|
||||||
long startTime = Time.currentTime();
|
long startTime = Time.currentTime();
|
||||||
|
|
||||||
// user Backchannel Authentication Request
|
// 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
|
// user Authentication Channel Request
|
||||||
TestAuthenticationChannelRequest testRequest = doAuthenticationChannelRequest(bindingMessage);
|
TestAuthenticationChannelRequest testRequest = doAuthenticationChannelRequest(bindingMessage);
|
||||||
|
|
|
@ -477,7 +477,7 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
|
||||||
ClientRepresentation kcClient = getClient(response.getClientId());
|
ClientRepresentation kcClient = getClient(response.getClientId());
|
||||||
Assert.assertEquals("poll", kcClient.getAttributes().get(CibaConfig.CIBA_BACKCHANNEL_TOKEN_DELIVERY_MODE_PER_CLIENT));
|
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");
|
clientRep.setBackchannelTokenDeliveryMode("ping");
|
||||||
try {
|
try {
|
||||||
reg.oidc().create(clientRep);
|
reg.oidc().create(clientRep);
|
||||||
|
@ -485,6 +485,21 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
|
||||||
} catch (ClientRegistrationException e) {
|
} catch (ClientRegistrationException e) {
|
||||||
assertEquals(ERR_MSG_CLIENT_REG_FAIL, e.getMessage());
|
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
|
@Test
|
||||||
|
|
|
@ -179,7 +179,7 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest {
|
||||||
// CIBA
|
// CIBA
|
||||||
assertEquals(oidcConfig.getBackchannelAuthenticationEndpoint(), oauth.getBackchannelAuthenticationUrl());
|
assertEquals(oidcConfig.getBackchannelAuthenticationEndpoint(), oauth.getBackchannelAuthenticationUrl());
|
||||||
assertContains(oidcConfig.getGrantTypesSupported(), OAuth2Constants.CIBA_GRANT_TYPE);
|
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.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());
|
Assert.assertTrue(oidcConfig.getBackchannelLogoutSupported());
|
||||||
|
|
|
@ -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-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=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.
|
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=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=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.
|
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
|
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
|
public-key-credential-label=Public Key Credential Label
|
||||||
ciba-policy=CIBA Policy
|
ciba-policy=CIBA Policy
|
||||||
ciba-backchannel-tokendelivery-mode=Backchannel Token Delivery Mode
|
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=Expires In
|
||||||
ciba-expires-in.tooltip=The expiration time of the "auth_req_id" in seconds since the authentication request was received.
|
ciba-expires-in.tooltip=The expiration time of the "auth_req_id" in seconds since the authentication request was received.
|
||||||
ciba-interval=Interval
|
ciba-interval=Interval
|
||||||
|
|
|
@ -1320,6 +1320,9 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
|
||||||
var attrVal7 = $scope.client.attributes['request.object.encryption.enc'];
|
var attrVal7 = $scope.client.attributes['request.object.encryption.enc'];
|
||||||
$scope.requestObjectEncryptionEnc = attrVal7==null ? 'any' : attrVal7;
|
$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"]) {
|
||||||
if ($scope.client.attributes["exclude.session.state.from.auth.response"] == "true") {
|
if ($scope.client.attributes["exclude.session.state.from.auth.response"] == "true") {
|
||||||
$scope.excludeSessionStateFromAuthResponse = 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"]) {
|
||||||
if ($scope.client.attributes["use.refresh.tokens"] == "true") {
|
if ($scope.client.attributes["use.refresh.tokens"] == "true") {
|
||||||
$scope.useRefreshTokens = true;
|
$scope.useRefreshTokens = true;
|
||||||
|
@ -1554,13 +1559,17 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.changeCibaBackchannelAuthRequestSigningAlg = function() {
|
$scope.changeCibaBackchannelAuthRequestSigningAlg = function() {
|
||||||
if ($scope.cibaBackchannelAuthRequestSigningAlg === 'none') {
|
if ($scope.cibaBackchannelAuthRequestSigningAlg === 'any') {
|
||||||
$scope.clientEdit.attributes['ciba.backchannel.auth.request.signing.alg'] = null;
|
$scope.clientEdit.attributes['ciba.backchannel.auth.request.signing.alg'] = null;
|
||||||
} else {
|
} else {
|
||||||
$scope.clientEdit.attributes['ciba.backchannel.auth.request.signing.alg'] = $scope.cibaBackchannelAuthRequestSigningAlg;
|
$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.changeAuthorizationSignedResponseAlg = function() {
|
||||||
$scope.clientEdit.attributes['authorization.signed.response.alg'] = $scope.authorizationSignedResponseAlg;
|
$scope.clientEdit.attributes['authorization.signed.response.alg'] = $scope.authorizationSignedResponseAlg;
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,7 +9,8 @@
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
<div>
|
<div>
|
||||||
<select id="tokendelivery" ng-model="realm.attributes.cibaBackchannelTokenDeliveryMode" class="form-control">
|
<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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -600,14 +600,35 @@
|
||||||
</div>
|
</div>
|
||||||
<kc-tooltip>{{:: 'request-object-required.tooltip' | translate}}</kc-tooltip>
|
<kc-tooltip>{{:: 'request-object-required.tooltip' | translate}}</kc-tooltip>
|
||||||
</div>
|
</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>
|
<label class="col-md-2 control-label" for="cibaBackchannelAuthRequestSigningAlg">{{:: 'ciba-backchannel-auth-request-signing-alg' | translate}}</label>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div>
|
<div>
|
||||||
<select class="form-control" id="cibaBackchannelAuthRequestSigningAlg"
|
<select class="form-control" id="cibaBackchannelAuthRequestSigningAlg"
|
||||||
ng-change="changeCibaBackchannelAuthRequestSigningAlg()"
|
ng-change="changeCibaBackchannelAuthRequestSigningAlg()"
|
||||||
ng-model="cibaBackchannelAuthRequestSigningAlg">
|
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>
|
<option ng-repeat="provider in serverInfo.listProviderIds('clientSignature')" value="{{provider}}">{{provider}}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue