diff --git a/core/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java b/core/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java index 5a3e0040f4..06712f1e2d 100755 --- a/core/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java +++ b/core/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java @@ -151,6 +151,9 @@ public class OIDCConfigurationRepresentation { @JsonProperty("backchannel_authentication_endpoint") private String backchannelAuthenticationEndpoint; + @JsonProperty("backchannel_authentication_request_signing_alg_values_supported") + private List backchannelAuthenticationRequestSigningAlgValuesSupported; + protected Map otherClaims = new HashMap(); public String getIssuer() { @@ -461,6 +464,14 @@ public class OIDCConfigurationRepresentation { this.backchannelAuthenticationEndpoint = backchannelAuthenticationEndpoint; } + public List getBackchannelAuthenticationRequestSigningAlgValuesSupported() { + return backchannelAuthenticationRequestSigningAlgValuesSupported; + } + + public void setBackchannelAuthenticationRequestSigningAlgValuesSupported(List backchannelAuthenticationRequestSigningAlgValuesSupported) { + this.backchannelAuthenticationRequestSigningAlgValuesSupported = backchannelAuthenticationRequestSigningAlgValuesSupported; + } + @JsonAnyGetter public Map getOtherClaims() { return otherClaims; diff --git a/core/src/main/java/org/keycloak/representations/oidc/OIDCClientRepresentation.java b/core/src/main/java/org/keycloak/representations/oidc/OIDCClientRepresentation.java index 4435362e9f..0ea0f9e50f 100644 --- a/core/src/main/java/org/keycloak/representations/oidc/OIDCClientRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/oidc/OIDCClientRepresentation.java @@ -128,6 +128,8 @@ public class OIDCClientRepresentation { // OIDC CIBA private String backchannel_token_delivery_mode; + private String backchannel_authentication_request_signing_alg; + public List getRedirectUris() { return redirect_uris; } @@ -497,4 +499,12 @@ public class OIDCClientRepresentation { public void setBackchannelTokenDeliveryMode(String backchannel_token_delivery_mode) { this.backchannel_token_delivery_mode = backchannel_token_delivery_mode; } + + public String getBackchannelAuthenticationRequestSigningAlg() { + return backchannel_authentication_request_signing_alg; + } + + public void setBackchannelAuthenticationRequestSigningAlg(String backchannel_authentication_request_signing_alg) { + this.backchannel_authentication_request_signing_alg = backchannel_authentication_request_signing_alg; + } } diff --git a/server-spi-private/src/main/java/org/keycloak/crypto/SignatureProvider.java b/server-spi-private/src/main/java/org/keycloak/crypto/SignatureProvider.java index edb6262b6f..494992a2ef 100644 --- a/server-spi-private/src/main/java/org/keycloak/crypto/SignatureProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/crypto/SignatureProvider.java @@ -25,6 +25,8 @@ public interface SignatureProvider extends Provider { SignatureVerifierContext verifier(String kid) throws VerificationException; + boolean isAsymmetricAlgorithm(); + @Override default void close() { } diff --git a/server-spi/src/main/java/org/keycloak/models/CibaConfig.java b/server-spi/src/main/java/org/keycloak/models/CibaConfig.java index 406ba70957..e3b00f82ec 100644 --- a/server-spi/src/main/java/org/keycloak/models/CibaConfig.java +++ b/server-spi/src/main/java/org/keycloak/models/CibaConfig.java @@ -19,13 +19,13 @@ package org.keycloak.models; import java.io.Serializable; import java.util.function.Supplier; +import org.keycloak.jose.jws.Algorithm; import org.keycloak.utils.StringUtil; public class CibaConfig implements Serializable { // realm attribute names public static final String CIBA_BACKCHANNEL_TOKEN_DELIVERY_MODE = "cibaBackchannelTokenDeliveryMode"; - public static final String CIBA_BACKCHANNEL_TOKEN_DELIVERY_MODE_PER_CLIENT = "ciba.backchannel.token.delivery.mode"; public static final String CIBA_EXPIRES_IN = "cibaExpiresIn"; public static final String CIBA_INTERVAL = "cibaInterval"; public static final String CIBA_AUTH_REQUESTED_USER_HINT = "cibaAuthRequestedUserHint"; @@ -43,6 +43,8 @@ public class CibaConfig implements Serializable { // client attribute names public static final String OIDC_CIBA_GRANT_ENABLED = "oidc.ciba.grant.enabled"; + public static final String CIBA_BACKCHANNEL_TOKEN_DELIVERY_MODE_PER_CLIENT = "ciba.backchannel.token.delivery.mode"; + public static final String CIBA_BACKCHANNEL_AUTH_REQUEST_SIGNING_ALG = "ciba.backchannel.auth.request.signing.alg"; private transient Supplier realm; @@ -148,6 +150,11 @@ public class CibaConfig implements Serializable { return Boolean.parseBoolean(enabled); } + public Algorithm getBackchannelAuthRequestSigningAlg(ClientModel client) { + String alg = client.getAttribute(CIBA_BACKCHANNEL_AUTH_REQUEST_SIGNING_ALG); + return alg==null ? null : Enum.valueOf(Algorithm.class, alg); + } + private void persistRealmAttribute(String name, String value) { RealmModel realm = realmForWrite == null ? null : this.realmForWrite.get(); if (realm != null) { diff --git a/services/src/main/java/org/keycloak/crypto/AsymmetricSignatureProvider.java b/services/src/main/java/org/keycloak/crypto/AsymmetricSignatureProvider.java index cf024c4d2c..82a05cca57 100644 --- a/services/src/main/java/org/keycloak/crypto/AsymmetricSignatureProvider.java +++ b/services/src/main/java/org/keycloak/crypto/AsymmetricSignatureProvider.java @@ -39,4 +39,8 @@ public class AsymmetricSignatureProvider implements SignatureProvider { return new ServerAsymmetricSignatureVerifierContext(session, kid, algorithm); } + @Override + public boolean isAsymmetricAlgorithm() { + return true; + } } diff --git a/services/src/main/java/org/keycloak/crypto/ECDSASignatureProvider.java b/services/src/main/java/org/keycloak/crypto/ECDSASignatureProvider.java index f9cf09bdb4..a3d641bb4d 100644 --- a/services/src/main/java/org/keycloak/crypto/ECDSASignatureProvider.java +++ b/services/src/main/java/org/keycloak/crypto/ECDSASignatureProvider.java @@ -33,6 +33,11 @@ public class ECDSASignatureProvider implements SignatureProvider { return new ServerECDSASignatureVerifierContext(session, kid, algorithm); } + @Override + public boolean isAsymmetricAlgorithm() { + return true; + } + public static byte[] concatenatedRSToASN1DER(final byte[] signature, int signLength) throws IOException { int len = signLength / 2; int arraySize = len + 1; diff --git a/services/src/main/java/org/keycloak/crypto/MacSecretSignatureProvider.java b/services/src/main/java/org/keycloak/crypto/MacSecretSignatureProvider.java index 2664edbe06..035b7969e8 100644 --- a/services/src/main/java/org/keycloak/crypto/MacSecretSignatureProvider.java +++ b/services/src/main/java/org/keycloak/crypto/MacSecretSignatureProvider.java @@ -39,4 +39,8 @@ public class MacSecretSignatureProvider implements SignatureProvider { return new ServerMacSignatureVerifierContext(session, kid, algorithm); } + @Override + public boolean isAsymmetricAlgorithm() { + return false; + } } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java index 3974811213..f5630f56db 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java @@ -24,6 +24,9 @@ import org.keycloak.authentication.ClientAuthenticatorFactory; import org.keycloak.crypto.CekManagementProvider; import org.keycloak.crypto.ClientSignatureVerifierProvider; import org.keycloak.crypto.ContentEncryptionProvider; +import org.keycloak.crypto.HS256ClientSignatureVerifierProviderFactory; +import org.keycloak.crypto.HS384ClientSignatureVerifierProviderFactory; +import org.keycloak.crypto.HS512ClientSignatureVerifierProviderFactory; import org.keycloak.crypto.SignatureProvider; import org.keycloak.jose.jws.Algorithm; import org.keycloak.models.CibaConfig; @@ -50,10 +53,13 @@ import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; import java.net.URI; +import java.util.AbstractMap; import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -164,7 +170,6 @@ public class OIDCWellKnownProvider implements WellKnownProvider { // NOTE: Don't hardcode HTTPS checks here. JWKS URI is exposed just in the development/testing environment. For the production environment, the OIDCWellKnownProvider // is not exposed over "http" at all. - //if (isHttps(jwksUri)) { config.setRevocationEndpoint(revocationEndpoint.toString()); config.setRevocationEndpointAuthMethodsSupported(getClientAuthMethodsSupported()); config.setRevocationEndpointAuthSigningAlgValuesSupported(getSupportedClientSigningAlgorithms(false)); @@ -174,6 +179,7 @@ public class OIDCWellKnownProvider implements WellKnownProvider { config.setBackchannelTokenDeliveryModesSupported(DEFAULT_BACKCHANNEL_TOKEN_DELIVERY_MODES_SUPPORTED); config.setBackchannelAuthenticationEndpoint(CibaGrantType.authorizationUrl(backendUriInfo.getBaseUriBuilder()).build(realm.getName()).toString()); + config.setBackchannelAuthenticationRequestSigningAlgValuesSupported(getSupportedBackchannelAuthenticationRequestSigningAlgorithms()); return config; } @@ -182,6 +188,13 @@ public class OIDCWellKnownProvider implements WellKnownProvider { public void close() { } + public static boolean isAsymmetricAlgorithm(String alg) { + if (HS256ClientSignatureVerifierProviderFactory.ID.equals(alg)) return false; + if (HS384ClientSignatureVerifierProviderFactory.ID.equals(alg)) return false; + if (HS512ClientSignatureVerifierProviderFactory.ID.equals(alg)) return false; + return true; + } + private static List list(String... values) { return Arrays.asList(values); } @@ -204,6 +217,15 @@ public class OIDCWellKnownProvider implements WellKnownProvider { return supportedAlgorithms.collect(Collectors.toList()); } + private List getSupportedAsymmetricAlgorithms() { + return getSupportedAlgorithms(SignatureProvider.class, false).stream() + .map(algorithm -> new AbstractMap.SimpleEntry<>(algorithm, session.getProvider(SignatureProvider.class, algorithm))) + .filter(entry -> entry.getValue() != null) + .filter(entry -> entry.getValue().isAsymmetricAlgorithm()) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + } + private List getSupportedSigningAlgorithms(boolean includeNone) { return getSupportedAlgorithms(SignatureProvider.class, includeNone); } @@ -212,6 +234,10 @@ public class OIDCWellKnownProvider implements WellKnownProvider { return getSupportedAlgorithms(ClientSignatureVerifierProvider.class, includeNone); } + private List getSupportedBackchannelAuthenticationRequestSigningAlgorithms() { + return getSupportedAsymmetricAlgorithms(); + } + private List getSupportedIdTokenEncryptionAlg(boolean includeNone) { return getSupportedAlgorithms(CekManagementProvider.class, includeNone); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/CibaGrantType.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/CibaGrantType.java index 3062fb0afe..47638c634b 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/CibaGrantType.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/CibaGrantType.java @@ -71,6 +71,10 @@ public class CibaGrantType { public static final String AUTH_REQ_ID = "auth_req_id"; public static final String CLIENT_NOTIFICATION_TOKEN = "client_notification_token"; public static final String REQUESTED_EXPIRY = "requested_expiry"; + public static final String USER_CODE = "user_code"; + + public static final String REQUEST = OIDCLoginProtocol.REQUEST_PARAM; + public static final String REQUEST_URI = OIDCLoginProtocol.REQUEST_URI_PARAM; public static UriBuilder authorizationUrl(UriBuilder baseUriBuilder) { UriBuilder uriBuilder = OIDCLoginProtocolService.tokenServiceBaseUrl(baseUriBuilder); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/BackchannelAuthenticationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/BackchannelAuthenticationEndpoint.java index e99da15f32..ab59d6a003 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/BackchannelAuthenticationEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/BackchannelAuthenticationEndpoint.java @@ -47,10 +47,11 @@ import org.keycloak.models.UserModel; import org.keycloak.protocol.oidc.grants.ciba.CibaGrantType; import org.keycloak.protocol.oidc.grants.ciba.channel.AuthenticationChannelProvider; import org.keycloak.protocol.oidc.grants.ciba.channel.CIBAAuthenticationRequest; +import org.keycloak.protocol.oidc.grants.ciba.endpoints.request.BackchannelAuthenticationEndpointRequest; +import org.keycloak.protocol.oidc.grants.ciba.endpoints.request.BackchannelAuthenticationEndpointRequestParserProcessor; import org.keycloak.protocol.oidc.grants.ciba.resolvers.CIBALoginUserResolver; import org.keycloak.services.ErrorResponseException; import org.keycloak.util.JsonSerialization; -import org.keycloak.utils.ProfileHelper; public class BackchannelAuthenticationEndpoint extends AbstractCibaEndpoint { @@ -139,33 +140,28 @@ public class BackchannelAuthenticationEndpoint extends AbstractCibaEndpoint { private CIBAAuthenticationRequest authorizeClient(MultivaluedMap params) { ClientModel client = authenticateClient(); - UserModel user = resolveUser(params, realm.getCibaPolicy().getAuthRequestedUserHint()); + BackchannelAuthenticationEndpointRequest endpointRequest = BackchannelAuthenticationEndpointRequestParserProcessor.parseRequest(event, session, client, params, realm.getCibaPolicy()); + UserModel user = resolveUser(endpointRequest, realm.getCibaPolicy().getAuthRequestedUserHint()); CIBAAuthenticationRequest request = new CIBAAuthenticationRequest(session, user, client); request.setClient(client); - String scope = params.getFirst(OAuth2Constants.SCOPE); - - if (scope == null) + String scope = endpointRequest.getScope(); + if (scope == null) { throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "missing parameter : scope", Response.Status.BAD_REQUEST); - + } request.setScope(scope); // optional parameters - if (params.getFirst(CibaGrantType.BINDING_MESSAGE) != null) request.setBindingMessage(params.getFirst(CibaGrantType.BINDING_MESSAGE)); - if (params.getFirst(OAuth2Constants.ACR_VALUES) != null) request.setAcrValues(params.getFirst(OAuth2Constants.ACR_VALUES)); + if (endpointRequest.getBindingMessage() != null) request.setBindingMessage(endpointRequest.getBindingMessage()); + if (endpointRequest.getAcr() != null) request.setAcrValues(endpointRequest.getAcr()); CibaConfig policy = realm.getCibaPolicy(); // create JWE encoded auth_req_id from Auth Req ID. - Integer expiresIn = policy.getExpiresIn(); - String requestedExpiry = params.getFirst(CibaGrantType.REQUESTED_EXPIRY); - - if (requestedExpiry != null) { - expiresIn = Integer.valueOf(requestedExpiry); - } + Integer expiresIn = Optional.ofNullable(endpointRequest.getRequestedExpiry()).orElse(policy.getExpiresIn()); request.exp(request.getIat() + expiresIn.longValue()); @@ -177,16 +173,12 @@ public class BackchannelAuthenticationEndpoint extends AbstractCibaEndpoint { }); request.setScope(scopes.toString()); - String clientNotificationToken = params.getFirst(CibaGrantType.CLIENT_NOTIFICATION_TOKEN); - - if (clientNotificationToken != null) { + if (endpointRequest.getClientNotificationToken() != null) { throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Ping and push modes not supported. Use poll mode instead.", Response.Status.BAD_REQUEST); } - String userCode = params.getFirst(OAuth2Constants.USER_CODE); - - if (userCode != null) { + if (endpointRequest.getUserCode() != null) { throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "User code not supported", Response.Status.BAD_REQUEST); } @@ -194,7 +186,7 @@ public class BackchannelAuthenticationEndpoint extends AbstractCibaEndpoint { return request; } - private UserModel resolveUser(MultivaluedMap params, String authRequestedUserHint) { + private UserModel resolveUser(BackchannelAuthenticationEndpointRequest endpointRequest, String authRequestedUserHint) { CIBALoginUserResolver resolver = session.getProvider(CIBALoginUserResolver.class); if (resolver == null) { @@ -205,19 +197,19 @@ public class BackchannelAuthenticationEndpoint extends AbstractCibaEndpoint { UserModel user; if (authRequestedUserHint.equals(LOGIN_HINT_PARAM)) { - userHint = params.getFirst(LOGIN_HINT_PARAM); + userHint = endpointRequest.getLoginHint(); if (userHint == null) throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "missing parameter : login_hint", Response.Status.BAD_REQUEST); user = resolver.getUserFromLoginHint(userHint); } else if (authRequestedUserHint.equals(ID_TOKEN_HINT)) { - userHint = params.getFirst(ID_TOKEN_HINT); + userHint = endpointRequest.getIdTokenHint(); if (userHint == null) throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "missing parameter : id_token_hint", Response.Status.BAD_REQUEST); user = resolver.getUserFromIdTokenHint(userHint); } else if (authRequestedUserHint.equals(CibaGrantType.LOGIN_HINT_TOKEN)) { - userHint = params.getFirst(CibaGrantType.LOGIN_HINT_TOKEN); + userHint = endpointRequest.getLoginHintToken(); if (userHint == null) throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "missing parameter : login_hint_token", Response.Status.BAD_REQUEST); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/request/BackchannelAuthenticationEndpointRequest.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/request/BackchannelAuthenticationEndpointRequest.java new file mode 100644 index 0000000000..8077e9406c --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/request/BackchannelAuthenticationEndpointRequest.java @@ -0,0 +1,117 @@ +/* + * 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.request; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Takashi Norimatsu + */ +public class BackchannelAuthenticationEndpointRequest { + + String scope; + String clientNotificationToken; + String acr; + String loginHintToken; + String idTokenHint; + String loginHint; + String bindingMessage; + String userCode; + Integer requestedExpiry; + + String prompt; + String nonce; + Integer maxAge; + String display; + String uiLocales; + String claims; + + Map additionalReqParams = new HashMap<>(); + + String invalidRequestMessage; + + public String getScope() { + return scope; + } + + public String getClientNotificationToken() { + return clientNotificationToken; + } + + public String getAcr() { + return acr; + } + + public String getLoginHintToken() { + return loginHintToken; + } + + public String getIdTokenHint() { + return idTokenHint; + } + + public String getLoginHint() { + return loginHint; + } + + public String getBindingMessage() { + return bindingMessage; + } + + public String getUserCode() { + return userCode; + } + + public Integer getRequestedExpiry() { + return requestedExpiry; + } + + public String getPrompt() { + return prompt; + } + + public String getNonce() { + return nonce; + } + + public Integer getMaxAge() { + return maxAge; + } + + public String getDisplay() { + return display; + } + + public String getUiLocales() { + return uiLocales; + } + + public String getClaims() { + return claims; + } + + public Map getAdditionalReqParams() { + return additionalReqParams; + } + + + public String getInvalidRequestMessage() { + return invalidRequestMessage; + } +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/request/BackchannelAuthenticationEndpointRequestBodyParser.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/request/BackchannelAuthenticationEndpointRequestBodyParser.java new file mode 100644 index 0000000000..ee0673850d --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/request/BackchannelAuthenticationEndpointRequestBodyParser.java @@ -0,0 +1,69 @@ +/* + * 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.request; + +import javax.ws.rs.core.MultivaluedMap; + +import java.util.Set; + +/** + * Parse the parameters from request body + * + * @author Takashi Norimatsu + */ +class BackchannelAuthenticationEndpointRequestBodyParser extends BackchannelAuthenticationEndpointRequestParser { + + private final MultivaluedMap requestParams; + + private String invalidRequestMessage = null; + + public BackchannelAuthenticationEndpointRequestBodyParser(MultivaluedMap requestParams) { + this.requestParams = requestParams; + } + + @Override + protected String getParameter(String paramName) { + checkDuplicated(requestParams, paramName); + return requestParams.getFirst(paramName); + } + + @Override + protected Integer getIntParameter(String paramName) { + checkDuplicated(requestParams, paramName); + String paramVal = requestParams.getFirst(paramName); + return paramVal==null ? null : Integer.parseInt(paramVal); + } + + public String getInvalidRequestMessage() { + return invalidRequestMessage; + } + + @Override + protected Set keySet() { + return requestParams.keySet(); + } + + private void checkDuplicated(MultivaluedMap requestParams, String paramName) { + if (invalidRequestMessage == null) { + if (requestParams.get(paramName) != null && requestParams.get(paramName).size() != 1) { + invalidRequestMessage = "duplicated parameter"; + } + } + } + +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/request/BackchannelAuthenticationEndpointRequestParser.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/request/BackchannelAuthenticationEndpointRequestParser.java new file mode 100644 index 0000000000..e9b83d919d --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/request/BackchannelAuthenticationEndpointRequestParser.java @@ -0,0 +1,128 @@ +/* + * 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.request; + +import org.jboss.logging.Logger; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.protocol.oidc.grants.ciba.CibaGrantType; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * @author Takashi Norimatsu + */ +public abstract class BackchannelAuthenticationEndpointRequestParser { + + private static final Logger logger = Logger.getLogger(BackchannelAuthenticationEndpointRequestParser.class); + + /** + * Max number of additional req params copied into client session note to prevent DoS attacks + * + */ + public static final int ADDITIONAL_REQ_PARAMS_MAX_MUMBER = 5; + + /** + * Max size of additional req param value copied into client session note to prevent DoS attacks - params with longer value are ignored + * + */ + public static final int ADDITIONAL_REQ_PARAMS_MAX_SIZE = 200; + + public static final String CIBA_SIGNED_AUTHENTICATION_REQUEST = "ParsedSignedAuthenticationRequest"; + + /** Set of known protocol POST params not to be stored into additionalReqParams} */ + public static final Set KNOWN_REQ_PARAMS = new HashSet<>(); + static { + KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.REQUEST_PARAM); + KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.REQUEST_URI_PARAM); + + KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.SCOPE_PARAM); + + // CIBA + KNOWN_REQ_PARAMS.add(CibaGrantType.CLIENT_NOTIFICATION_TOKEN); + KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.ACR_PARAM); + KNOWN_REQ_PARAMS.add(CibaGrantType.LOGIN_HINT_TOKEN); + KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.ID_TOKEN_HINT); + KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.LOGIN_HINT_PARAM); + KNOWN_REQ_PARAMS.add(CibaGrantType.BINDING_MESSAGE); + KNOWN_REQ_PARAMS.add(CibaGrantType.USER_CODE); + KNOWN_REQ_PARAMS.add(CibaGrantType.REQUESTED_EXPIRY); + + // OIDC + KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.PROMPT_PARAM); + KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.NONCE_PARAM); + KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.MAX_AGE_PARAM); + KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.UI_LOCALES_PARAM); + KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.CLAIMS_PARAM); + } + + public void parseRequest(BackchannelAuthenticationEndpointRequest request) { + request.scope = replaceIfNotNull(request.scope, getParameter(OIDCLoginProtocol.SCOPE_PARAM)); + + request.clientNotificationToken = replaceIfNotNull(request.clientNotificationToken, getParameter(CibaGrantType.CLIENT_NOTIFICATION_TOKEN)); + request.acr = replaceIfNotNull(request.acr, getParameter(OIDCLoginProtocol.ACR_PARAM)); + request.loginHintToken = replaceIfNotNull(request.loginHintToken, getParameter(CibaGrantType.LOGIN_HINT_TOKEN)); + request.idTokenHint = replaceIfNotNull(request.idTokenHint, getParameter(OIDCLoginProtocol.ID_TOKEN_HINT)); + request.loginHint = replaceIfNotNull(request.loginHint, getParameter(OIDCLoginProtocol.LOGIN_HINT_PARAM)); + request.bindingMessage = replaceIfNotNull(request.bindingMessage, getParameter(CibaGrantType.BINDING_MESSAGE)); + request.userCode = replaceIfNotNull(request.userCode, getParameter(CibaGrantType.USER_CODE)); + request.requestedExpiry = replaceIfNotNull(request.requestedExpiry, getIntParameter(CibaGrantType.REQUESTED_EXPIRY)); + + request.prompt = replaceIfNotNull(request.prompt, getParameter(OIDCLoginProtocol.PROMPT_PARAM)); + request.nonce = replaceIfNotNull(request.nonce, getParameter(OIDCLoginProtocol.NONCE_PARAM)); + request.maxAge = replaceIfNotNull(request.maxAge, getIntParameter(OIDCLoginProtocol.MAX_AGE_PARAM)); + request.uiLocales = replaceIfNotNull(request.uiLocales, getParameter(OIDCLoginProtocol.UI_LOCALES_PARAM)); + request.claims = replaceIfNotNull(request.claims, getParameter(OIDCLoginProtocol.CLAIMS_PARAM)); + + extractAdditionalReqParams(request.additionalReqParams); + } + + protected void extractAdditionalReqParams(Map additionalReqParams) { + for (String paramName : keySet()) { + if (!KNOWN_REQ_PARAMS.contains(paramName)) { + String value = getParameter(paramName); + if (value != null && value.trim().isEmpty()) { + value = null; + } + if (value != null && value.length() <= ADDITIONAL_REQ_PARAMS_MAX_SIZE) { + if (additionalReqParams.size() >= ADDITIONAL_REQ_PARAMS_MAX_MUMBER) { + logger.debug("Maximal number of additional OIDC CIBA params (" + ADDITIONAL_REQ_PARAMS_MAX_MUMBER + ") exceeded, ignoring rest of them!"); + break; + } + additionalReqParams.put(paramName, value); + } else { + logger.debug("OIDC CIBA Additional param " + paramName + " ignored because value is empty or longer than " + ADDITIONAL_REQ_PARAMS_MAX_SIZE); + } + } + + } + } + + protected T replaceIfNotNull(T previousVal, T newVal) { + return newVal==null ? previousVal : newVal; + } + + protected abstract String getParameter(String paramName); + + protected abstract Integer getIntParameter(String paramName); + + protected abstract Set keySet(); + +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/request/BackchannelAuthenticationEndpointRequestParserProcessor.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/request/BackchannelAuthenticationEndpointRequestParserProcessor.java new file mode 100644 index 0000000000..3636471d6d --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/request/BackchannelAuthenticationEndpointRequestParserProcessor.java @@ -0,0 +1,85 @@ +/* + * 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.request; + +import org.keycloak.OAuthErrorException; +import org.keycloak.common.util.StreamUtil; +import org.keycloak.connections.httpclient.HttpClientProvider; +import org.keycloak.events.EventBuilder; +import org.keycloak.models.CibaConfig; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.protocol.oidc.utils.RedirectUtils; +import org.keycloak.services.ErrorResponseException; + +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import java.io.InputStream; +import java.util.HashSet; +import java.util.List; + +/** + * @author Takashi Norimatsu + */ +public class BackchannelAuthenticationEndpointRequestParserProcessor { + + public static BackchannelAuthenticationEndpointRequest parseRequest(EventBuilder event, KeycloakSession session, ClientModel client, MultivaluedMap requestParams, CibaConfig config) { + try { + BackchannelAuthenticationEndpointRequest request = new BackchannelAuthenticationEndpointRequest(); + + BackchannelAuthenticationEndpointRequestBodyParser parser = new BackchannelAuthenticationEndpointRequestBodyParser(requestParams); + parser.parseRequest(request); + + if (parser.getInvalidRequestMessage() != null) { + request.invalidRequestMessage = parser.getInvalidRequestMessage(); + return request; + } + + String requestParam = requestParams.getFirst(OIDCLoginProtocol.REQUEST_PARAM); + String requestUriParam = requestParams.getFirst(OIDCLoginProtocol.REQUEST_URI_PARAM); + + if (requestParam != null && requestUriParam != null) { + throw new RuntimeException("Illegal to use both 'request' and 'request_uri' parameters together"); + } + + if (requestParam != null) { + new BackchannelAuthenticationEndpointSignedRequestParser(session, requestParam, client, config).parseRequest(request); + } else if (requestUriParam != null) { + // Validate "requestUriParam" with allowed requestUris + List requestUris = OIDCAdvancedConfigWrapper.fromClientModel(client).getRequestUris(); + String requestUri = RedirectUtils.verifyRedirectUri(session, client.getRootUrl(), requestUriParam, new HashSet<>(requestUris), false); + if (requestUri == null) { + throw new RuntimeException("Specified 'request_uri' not allowed for this client."); + } + + try (InputStream is = session.getProvider(HttpClientProvider.class).get(requestUri)) { + String retrievedRequest = StreamUtil.readString(is); + new BackchannelAuthenticationEndpointSignedRequestParser(session, retrievedRequest, client, config).parseRequest(request); + } + } + + return request; + + } catch (Exception e) { + throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, e.getMessage(), Response.Status.BAD_REQUEST); + } + } +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/request/BackchannelAuthenticationEndpointSignedRequestParser.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/request/BackchannelAuthenticationEndpointSignedRequestParser.java new file mode 100644 index 0000000000..e9edb4d93b --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/request/BackchannelAuthenticationEndpointSignedRequestParser.java @@ -0,0 +1,102 @@ +/* + * 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.request; + +import com.fasterxml.jackson.databind.JsonNode; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; + +import org.keycloak.crypto.SignatureProvider; +import org.keycloak.jose.jws.Algorithm; +import org.keycloak.jose.jws.JWSHeader; +import org.keycloak.jose.jws.JWSInput; +import org.keycloak.models.CibaConfig; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; + +/** + * Parse the parameters from OIDC "request" object + * + * @author Takashi Norimatsu + */ +class BackchannelAuthenticationEndpointSignedRequestParser extends BackchannelAuthenticationEndpointRequestParser { + + private final JsonNode requestParams; + + public BackchannelAuthenticationEndpointSignedRequestParser(KeycloakSession session, String signedAuthReq, ClientModel client, CibaConfig config) throws Exception { + JWSInput input = new JWSInput(signedAuthReq); + JWSHeader header = input.getHeader(); + Algorithm headerAlgorithm = header.getAlgorithm(); + + Algorithm requestedSignatureAlgorithm = config.getBackchannelAuthRequestSigningAlg(client); + + if (headerAlgorithm == null) { + throw new RuntimeException("Signed algorithm not specified"); + } + if (header.getAlgorithm() == Algorithm.none) { + throw new RuntimeException("None signed algorithm is not allowed"); + } + SignatureProvider signatureProvider = session.getProvider(SignatureProvider.class, headerAlgorithm.name()); + if (signatureProvider == null) { + throw new RuntimeException("Not found provider for the algorithm " + headerAlgorithm.name()); + } + if (!signatureProvider.isAsymmetricAlgorithm()) { + throw new RuntimeException("Signed algorithm is not allowed"); + } + if (requestedSignatureAlgorithm != null && requestedSignatureAlgorithm != headerAlgorithm) { + throw new RuntimeException("Signed with different algorithm than client requested algorithm"); + } + + this.requestParams = session.tokens().decodeClientJWT(signedAuthReq, client, JsonNode.class); + if (this.requestParams == null) { + throw new RuntimeException("Failed to verify signature"); + } + + session.setAttribute(BackchannelAuthenticationEndpointRequestParser.CIBA_SIGNED_AUTHENTICATION_REQUEST, requestParams); + } + + @Override + protected String getParameter(String paramName) { + JsonNode val = this.requestParams.get(paramName); + if (val == null) { + return null; + } else if (val.isValueNode()) { + return val.asText(); + } else { + return val.toString(); + } + } + + @Override + protected Integer getIntParameter(String paramName) { + Object val = this.requestParams.get(paramName); + return val==null ? null : Integer.parseInt(getParameter(paramName)); + } + + @Override + protected Set keySet() { + HashSet keys = new HashSet<>(); + requestParams.fieldNames().forEachRemaining(keys::add); + return keys; + } + + static class TypedHashMap extends HashMap { + } +} diff --git a/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java b/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java index b713453e8e..08f7c095be 100755 --- a/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java +++ b/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java @@ -22,6 +22,7 @@ import org.keycloak.authentication.ClientAuthenticator; import org.keycloak.authentication.ClientAuthenticatorFactory; import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator; import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator; +import org.keycloak.crypto.ClientSignatureVerifierProvider; import org.keycloak.jose.jwk.JSONWebKeySet; import org.keycloak.jose.jwk.JWK; import org.keycloak.jose.jwk.JWKParser; @@ -36,6 +37,8 @@ import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil; import org.keycloak.protocol.oidc.utils.OIDCResponseType; import org.keycloak.protocol.oidc.utils.PairwiseSubMapperUtils; import org.keycloak.protocol.oidc.utils.SubjectType; +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; import org.keycloak.representations.idm.CertificateRepresentation; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation; @@ -45,6 +48,8 @@ import org.keycloak.services.util.CertificateInfoHelper; import org.keycloak.util.JWKSUtils; import org.keycloak.utils.StringUtil; +import com.google.common.collect.Streams; + import java.net.URI; import java.security.PublicKey; import java.util.ArrayList; @@ -56,6 +61,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.keycloak.models.OAuth2DeviceConfig.OAUTH2_DEVICE_AUTHORIZATION_GRANT_ENABLED; import static org.keycloak.models.CibaConfig.OIDC_CIBA_GRANT_ENABLED; @@ -186,6 +192,16 @@ public class DescriptionConverter { throw new ClientRegistrationException("Unsupported requested backchannel_token_delivery_mode"); } } + String backchannelAuthenticationRequestSigningAlg = clientOIDC.getBackchannelAuthenticationRequestSigningAlg(); + if (backchannelAuthenticationRequestSigningAlg != null) { + if(isSupportedBackchannelAuthenticationRequestSigningAlg(session, backchannelAuthenticationRequestSigningAlg)) { + Map attr = Optional.ofNullable(client.getAttributes()).orElse(new HashMap<>()); + attr.put(CibaConfig.CIBA_BACKCHANNEL_AUTH_REQUEST_SIGNING_ALG, backchannelAuthenticationRequestSigningAlg); + client.setAttributes(attr); + } else { + throw new ClientRegistrationException("Unsupported requested backchannel_authentication_request_signing_alg"); + } + } return client; } @@ -202,6 +218,23 @@ public class DescriptionConverter { return false; } + private static boolean isSupportedBackchannelAuthenticationRequestSigningAlg(KeycloakSession session, String alg) { + Stream 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 getSupportedAlgorithms(KeycloakSession session, Class clazz, boolean includeNone) { + Stream supportedAlgorithms = session.getKeycloakSessionFactory().getProviderFactoriesStream(clazz) + .map(ProviderFactory::getId); + + if (includeNone) { + supportedAlgorithms = Streams.concat(supportedAlgorithms, Stream.of("none")); + } + return supportedAlgorithms.collect(Collectors.toList()); + } + private static boolean setPublicKey(OIDCClientRepresentation clientOIDC, ClientRepresentation clientRep) { if (clientOIDC.getJwksUri() == null && clientOIDC.getJwks() == null) { return false; @@ -312,6 +345,10 @@ public class DescriptionConverter { if (StringUtil.isNotBlank(mode)) { response.setBackchannelTokenDeliveryMode(mode); } + String alg = client.getAttributes().get(CibaConfig.CIBA_BACKCHANNEL_AUTH_REQUEST_SIGNING_ALG); + if (StringUtil.isNotBlank(alg)) { + response.setBackchannelAuthenticationRequestSigningAlg(alg); + } } List foundPairwiseMappers = PairwiseSubMapperUtils.getPairwiseSubMappers(client); diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java index 07727dd127..82ce2d4a24 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java @@ -18,6 +18,9 @@ package org.keycloak.testsuite.rest.resource; import org.jboss.resteasy.annotations.cache.NoCache; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; import javax.ws.rs.BadRequestException; import javax.ws.rs.Consumes; @@ -29,9 +32,11 @@ import org.keycloak.common.util.PemUtils; import org.keycloak.constants.AdapterConstants; import org.keycloak.crypto.Algorithm; import org.keycloak.crypto.AsymmetricSignatureSignerContext; +import org.keycloak.crypto.JavaAlgorithm; import org.keycloak.crypto.KeyType; import org.keycloak.crypto.KeyUse; import org.keycloak.crypto.KeyWrapper; +import org.keycloak.crypto.MacSignatureSignerContext; import org.keycloak.crypto.ServerECDSASignatureSignerContext; import org.keycloak.crypto.SignatureSignerContext; import org.keycloak.jose.jwe.JWEConstants; @@ -67,6 +72,7 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.security.InvalidAlgorithmParameterException; import java.security.KeyPair; import java.security.KeyPairGenerator; @@ -242,15 +248,28 @@ public class TestingOIDCEndpointsApplicationResource { @Produces(org.keycloak.utils.MediaType.APPLICATION_JWT) @NoCache public void registerOIDCRequest(@QueryParam("requestObject") String encodedRequestObject, @QueryParam("jwaAlgorithm") String jwaAlgorithm) { + AuthorizationEndpointRequestObject oidcRequest = deserializeOidcRequest(encodedRequestObject); + setOidcRequest(oidcRequest, jwaAlgorithm); + } + + @GET + @Path("/register-oidc-request-symmetric-sig") + @Produces(org.keycloak.utils.MediaType.APPLICATION_JWT) + @NoCache + public void registerOIDCRequestSymmetricSig(@QueryParam("requestObject") String encodedRequestObject, @QueryParam("jwaAlgorithm") String jwaAlgorithm, @QueryParam("clientSecret") String clientSecret) { + AuthorizationEndpointRequestObject oidcRequest = deserializeOidcRequest(encodedRequestObject); + setOidcRequest(oidcRequest, jwaAlgorithm, clientSecret); + } + + private AuthorizationEndpointRequestObject deserializeOidcRequest(String encodedRequestObject) { byte[] serializedRequestObject = Base64Url.decode(encodedRequestObject); AuthorizationEndpointRequestObject oidcRequest = null; try { - oidcRequest = JsonSerialization.readValue(serializedRequestObject, AuthorizationEndpointRequestObject.class); + oidcRequest = JsonSerialization.readValue(serializedRequestObject, AuthorizationEndpointRequestObject.class); } catch (IOException e) { throw new BadRequestException("deserialize request object failed : " + e.getMessage()); } - - setOidcRequest(oidcRequest, jwaAlgorithm); + return oidcRequest; } private void setOidcRequest(Object oidcRequest, String jwaAlgorithm) { @@ -258,7 +277,7 @@ public class TestingOIDCEndpointsApplicationResource { if ("none".equals(jwaAlgorithm)) { clientData.setOidcRequest(new JWSBuilder().jsonContent(oidcRequest).none()); - } else if (clientData.getSigningKeyPair() == null) { + } else if (clientData.getSigningKeyPair() == null) { throw new BadRequestException("signing key not set"); } else { PrivateKey privateKey = clientData.getSigningKeyPair().getPrivate(); @@ -281,6 +300,33 @@ public class TestingOIDCEndpointsApplicationResource { } } + private void setOidcRequest(Object oidcRequest, String jwaAlgorithm, String clientSecret) { + if (!isSupportedAlgorithm(jwaAlgorithm)) throw new BadRequestException("Unknown argument: " + jwaAlgorithm); + if ("none".equals(jwaAlgorithm)) { + clientData.setOidcRequest(new JWSBuilder().jsonContent(oidcRequest).none()); + } else { + SignatureSignerContext signer; + switch (jwaAlgorithm) { + case Algorithm.HS256: + case Algorithm.HS384: + case Algorithm.HS512: + KeyWrapper keyWrapper = new KeyWrapper(); + SecretKey secretKey = new SecretKeySpec(clientSecret.getBytes(StandardCharsets.UTF_8), JavaAlgorithm.getJavaAlgorithm(jwaAlgorithm)); + keyWrapper.setSecretKey(secretKey); + String kid = KeyUtils.createKeyId(secretKey); + keyWrapper.setKid(kid); + keyWrapper.setAlgorithm(jwaAlgorithm); + keyWrapper.setUse(KeyUse.SIG); + keyWrapper.setType(KeyType.OCT); + signer = new MacSignatureSignerContext(keyWrapper); + clientData.setOidcRequest(new JWSBuilder().kid(kid).jsonContent(oidcRequest).sign(signer)); + break; + default: + throw new BadRequestException("Unknown jwaAlgorithm: " + jwaAlgorithm); + } + } + } + private boolean isSupportedAlgorithm(String signingAlgorithm) { if (signingAlgorithm == null) return false; boolean ret = false; @@ -295,6 +341,9 @@ public class TestingOIDCEndpointsApplicationResource { case Algorithm.ES256: case Algorithm.ES384: case Algorithm.ES512: + case Algorithm.HS256: + case Algorithm.HS384: + case Algorithm.HS512: case JWEConstants.RSA1_5: case JWEConstants.RSA_OAEP: case JWEConstants.RSA_OAEP_256: @@ -378,6 +427,25 @@ public class TestingOIDCEndpointsApplicationResource { @JsonProperty(Constants.KC_ACTION) String action; + // CIBA + + @JsonProperty(CibaGrantType.CLIENT_NOTIFICATION_TOKEN) + String clientNotificationToken; + + @JsonProperty(CibaGrantType.LOGIN_HINT_TOKEN) + String loginHintToken; + + @JsonProperty(OIDCLoginProtocol.ID_TOKEN_HINT) + String idTokenHint; + + @JsonProperty(CibaGrantType.USER_CODE) + String userCode; + + @JsonProperty(CibaGrantType.BINDING_MESSAGE) + String bindingMessage; + + Integer requested_expiry; + public String getClientId() { return clientId; } @@ -513,6 +581,55 @@ public class TestingOIDCEndpointsApplicationResource { public void setAction(String action) { this.action = action; } + + public String getClientNotificationToken() { + return clientNotificationToken; + } + + public void setClientNotificationToken(String clientNotificationToken) { + this.clientNotificationToken = clientNotificationToken; + } + + public String getLoginHintToken() { + return loginHintToken; + } + + public void setLoginHintToken(String loginHintToken) { + this.loginHintToken = loginHintToken; + } + + public String getIdTokenHint() { + return idTokenHint; + } + + public void setIdTokenHint(String idTokenHint) { + this.idTokenHint = idTokenHint; + } + + public String getBindingMessage() { + return bindingMessage; + } + + public void setBindingMessage(String bindingMessage) { + this.bindingMessage = bindingMessage; + } + + public String getUserCode() { + return userCode; + } + + public void setUserCode(String userCode) { + this.userCode = userCode; + } + + public Integer getRequested_expiry() { + return requested_expiry; + } + + public void setRequested_expiry(Integer requested_expiry) { + this.requested_expiry = requested_expiry; + } + } @POST diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestOIDCEndpointsApplicationResource.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestOIDCEndpointsApplicationResource.java index 9e50678c62..d0f934717e 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestOIDCEndpointsApplicationResource.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestOIDCEndpointsApplicationResource.java @@ -72,6 +72,11 @@ public interface TestOIDCEndpointsApplicationResource { @Produces(org.keycloak.utils.MediaType.APPLICATION_JWT) void registerOIDCRequest(@QueryParam("requestObject") String encodedRequestObject, @QueryParam("jwaAlgorithm") String jwaAlgorithm); + @GET + @Path("/register-oidc-request-symmetric-sig") + @Produces(org.keycloak.utils.MediaType.APPLICATION_JWT) + void registerOIDCRequestSymmetricSig(@QueryParam("requestObject") String encodedRequestObject, @QueryParam("jwaAlgorithm") String jwaAlgorithm, @QueryParam("clientSecret") String clientSecret); + @GET @Path("/get-oidc-request") @Produces(org.keycloak.utils.MediaType.APPLICATION_JWT) diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java index 83f1b57427..6d5e8f8ff8 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java @@ -727,6 +727,12 @@ public class OAuthClient { } else { parameters.add(new BasicNameValuePair(OAuth2Constants.SCOPE, OAuth2Constants.SCOPE_OPENID)); } + if (requestUri != null) { + parameters.add(new BasicNameValuePair(OIDCLoginProtocol.REQUEST_URI_PARAM, requestUri)); + } + if (request != null) { + parameters.add(new BasicNameValuePair(OIDCLoginProtocol.REQUEST_PARAM, request)); + } UrlEncodedFormEntity formEntity; try { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/CIBATest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/CIBATest.java index d5c4af54ed..d18fbd2c53 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/CIBATest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/CIBATest.java @@ -34,6 +34,8 @@ import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerEx import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE; import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -56,10 +58,14 @@ import org.keycloak.client.registration.Auth; import org.keycloak.client.registration.ClientRegistration; import org.keycloak.client.registration.ClientRegistrationException; import org.keycloak.common.Profile; +import org.keycloak.common.util.Base64Url; +import org.keycloak.common.util.Time; +import org.keycloak.crypto.Algorithm; import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.events.EventType; import org.keycloak.models.CibaConfig; +import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; 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.AuthenticationChannelResponse; @@ -74,13 +80,16 @@ import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.oidc.OIDCClientRepresentation; import org.keycloak.representations.oidc.TokenMetadataRepresentation; +import org.keycloak.services.Urls; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; import org.keycloak.testsuite.arquillian.annotation.EnableFeature; +import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls; import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResource; import org.keycloak.testsuite.rest.representation.TestAuthenticationChannelRequest; +import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource.AuthorizationEndpointRequestObject; import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule; import org.keycloak.testsuite.util.KeycloakModelUtils; import org.keycloak.testsuite.util.Matchers; @@ -88,6 +97,7 @@ import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.ServerURLs; import org.keycloak.testsuite.util.UserBuilder; import org.keycloak.testsuite.util.OAuthClient.AuthenticationRequestAcknowledgement; +import org.keycloak.util.JsonSerialization; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -1040,8 +1050,13 @@ public class CIBATest extends AbstractTestRealmKeycloakTest { Map attributes = Optional.ofNullable(clientRep.getAttributes()).orElse(new HashMap<>()); attributes.put(CibaConfig.CIBA_BACKCHANNEL_TOKEN_DELIVERY_MODE_PER_CLIENT, "poll"); attributes.put(CibaConfig.OIDC_CIBA_GRANT_ENABLED, null); + attributes.put(CibaConfig.CIBA_BACKCHANNEL_AUTH_REQUEST_SIGNING_ALG, Algorithm.RS256); clientRep.setAttributes(attributes); clientResource.update(clientRep); + //clientResource = ApiUtil.findClientByClientId(adminClient.realm(TEST_REALM_NAME), TEST_CLIENT_NAME); + clientRep = clientResource.toRepresentation(); + Assert.assertNull(clientRep.getAttributes().get(CibaConfig.OIDC_CIBA_GRANT_ENABLED)); + Assert.assertThat(clientRep.getAttributes().get(CibaConfig.CIBA_BACKCHANNEL_AUTH_REQUEST_SIGNING_ALG), is(Algorithm.RS256)); // user Backchannel Authentication Request AuthenticationRequestAcknowledgement response = oauth.doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, username, "gilwekDe3", "acr2"); @@ -1054,8 +1069,12 @@ public class CIBATest extends AbstractTestRealmKeycloakTest { clientRep = clientResource.toRepresentation(); attributes = clientRep.getAttributes(); attributes.put(CibaConfig.OIDC_CIBA_GRANT_ENABLED, Boolean.TRUE.toString()); + attributes.put(CibaConfig.CIBA_BACKCHANNEL_AUTH_REQUEST_SIGNING_ALG, Algorithm.ES256); clientRep.setAttributes(attributes); clientResource.update(clientRep); + clientRep = clientResource.toRepresentation(); + Assert.assertThat(clientRep.getAttributes().get(CibaConfig.OIDC_CIBA_GRANT_ENABLED), is(Boolean.TRUE.toString())); + Assert.assertThat(clientRep.getAttributes().get(CibaConfig.CIBA_BACKCHANNEL_AUTH_REQUEST_SIGNING_ALG), is(Algorithm.ES256)); // user Backchannel Authentication Request response = doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, username, "Fkb4T3s"); @@ -1071,8 +1090,12 @@ public class CIBATest extends AbstractTestRealmKeycloakTest { clientRep = clientResource.toRepresentation(); attributes = clientRep.getAttributes(); attributes.put(CibaConfig.OIDC_CIBA_GRANT_ENABLED, Boolean.FALSE.toString()); + attributes.put(CibaConfig.CIBA_BACKCHANNEL_AUTH_REQUEST_SIGNING_ALG, "none"); clientRep.setAttributes(attributes); clientResource.update(clientRep); + clientRep = clientResource.toRepresentation(); + Assert.assertThat(clientRep.getAttributes().get(CibaConfig.OIDC_CIBA_GRANT_ENABLED), is(Boolean.FALSE.toString())); + Assert.assertThat(clientRep.getAttributes().get(CibaConfig.CIBA_BACKCHANNEL_AUTH_REQUEST_SIGNING_ALG), is("none")); // user Token Request OAuthClient.AccessTokenResponse tokenRes = oauth.doBackchannelAuthenticationTokenRequest(SECOND_TEST_CLIENT_NAME, SECOND_TEST_CLIENT_SECRET, response.getAuthReqId()); @@ -1091,15 +1114,201 @@ public class CIBATest extends AbstractTestRealmKeycloakTest { OIDCClientRepresentation rep = getClientDynamically(clientId); Assert.assertTrue(!rep.getGrantTypes().contains(OAuth2Constants.CIBA_GRANT_TYPE)); + Assert.assertNull(rep.getBackchannelAuthenticationRequestSigningAlg()); updateClientDynamically(clientId, (OIDCClientRepresentation clientRep) -> { List grantTypes = Optional.ofNullable(clientRep.getGrantTypes()).orElse(new ArrayList<>()); grantTypes.add(OAuth2Constants.CIBA_GRANT_TYPE); clientRep.setGrantTypes(grantTypes); + clientRep.setBackchannelAuthenticationRequestSigningAlg(Algorithm.PS256); }); rep = getClientDynamically(clientId); Assert.assertTrue(rep.getGrantTypes().contains(OAuth2Constants.CIBA_GRANT_TYPE)); + Assert.assertThat(rep.getBackchannelAuthenticationRequestSigningAlg(), is(Algorithm.PS256)); + } + + @Test + public void testBackchannelAuthenticationFlowWithSignedAuthenticationRequestParam() throws Exception { + testBackchannelAuthenticationFlowWithSignedAuthenticationRequest(false, Algorithm.PS256); + } + + @Test + public void testBackchannelAuthenticationFlowWithSignedAuthenticationRequestUriParam() throws Exception { + testBackchannelAuthenticationFlowWithSignedAuthenticationRequest(true, Algorithm.ES256); + } + + @Test + public void testBackchannelAuthenticationFlowWithInvalidSignedAuthenticationRequestParam() throws Exception { + testBackchannelAuthenticationFlowWithInvalidSignedAuthenticationRequest(false, Algorithm.HS256, "Signed algorithm is not allowed"); + } + + @Test + public void testBackchannelAuthenticationFlowWithInvalidSignedAuthenticationRequestUriParam() throws Exception { + testBackchannelAuthenticationFlowWithInvalidSignedAuthenticationRequest(true, "none", "None signed algorithm is not allowed"); + } + + private void testBackchannelAuthenticationFlowWithInvalidSignedAuthenticationRequest(boolean useRequestUri, String sigAlg, String errorDescription) throws Exception { + ClientResource clientResource = null; + ClientRepresentation clientRep = null; + try { + final String username = "nutzername-rot"; + final String bindingMessage = "BASTION"; + + // prepare CIBA settings + clientResource = ApiUtil.findClientByClientId(adminClient.realm(TEST_REALM_NAME), TEST_CLIENT_NAME); + clientRep = clientResource.toRepresentation(); + prepareCIBASettings(clientResource, clientRep); + + AuthorizationEndpointRequestObject sharedAuthenticationRequest = createValidSharedAuthenticationRequest(); + sharedAuthenticationRequest.setLoginHint(username); + sharedAuthenticationRequest.setBindingMessage(bindingMessage); + registerSharedAuthenticationRequest(sharedAuthenticationRequest, TEST_CLIENT_NAME, sigAlg, useRequestUri, TEST_CLIENT_PASSWORD); + + // user Backchannel Authentication Request + AuthenticationRequestAcknowledgement response = oauth.doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, null, null, null); + Assert.assertThat(response.getStatusCode(), is(equalTo(400))); + Assert.assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST)); + Assert.assertThat(response.getErrorDescription(), is(errorDescription)); + } finally { + revertCIBASettings(clientResource, clientRep); + } + } + + protected void registerSharedInvalidAuthenticationRequest(AuthorizationEndpointRequestObject requestObject, String clientId, String sigAlg, boolean isUseRequestUri) throws URISyntaxException, IOException { + TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); + + // Set required signature for request_uri + // use and set jwks_url + ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm(TEST_REALM_NAME), clientId); + ClientRepresentation clientRep = clientResource.toRepresentation(); + Map attr = Optional.ofNullable(clientRep.getAttributes()).orElse(new HashMap<>()); + attr.put(CibaConfig.CIBA_BACKCHANNEL_AUTH_REQUEST_SIGNING_ALG, sigAlg); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setUseJwksUrl(true); + String jwksUrl = TestApplicationResourceUrls.clientJwksUri(); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setJwksUrl(jwksUrl); + clientResource.update(clientRep); + + oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); + + // register request object + byte[] contentBytes = JsonSerialization.writeValueAsBytes(requestObject); + String encodedRequestObject = Base64Url.encode(contentBytes); + oidcClientEndpointsResource.registerOIDCRequest(encodedRequestObject, sigAlg); + + if (isUseRequestUri) { + oauth.request(null); + oauth.requestUri(TestApplicationResourceUrls.clientRequestUri()); + } else { + oauth.requestUri(null); + oauth.request(oidcClientEndpointsResource.getOIDCRequest()); + } + } + + private void testBackchannelAuthenticationFlowWithSignedAuthenticationRequest(boolean useRequestUri, String sigAlg) throws Exception { + ClientResource clientResource = null; + ClientRepresentation clientRep = null; + try { + final String username = "nutzername-rot"; + final String bindingMessage = "BASTION"; + + // prepare CIBA settings + clientResource = ApiUtil.findClientByClientId(adminClient.realm(TEST_REALM_NAME), TEST_CLIENT_NAME); + clientRep = clientResource.toRepresentation(); + prepareCIBASettings(clientResource, clientRep); + + AuthorizationEndpointRequestObject sharedAuthenticationRequest = createValidSharedAuthenticationRequest(); + sharedAuthenticationRequest.setLoginHint(username); + sharedAuthenticationRequest.setBindingMessage(bindingMessage); + registerSharedAuthenticationRequest(sharedAuthenticationRequest, TEST_CLIENT_NAME, sigAlg, useRequestUri); + + // user Backchannel Authentication Request + AuthenticationRequestAcknowledgement response = doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, null, null); + + // user Authentication Channel Request + TestAuthenticationChannelRequest testRequest = doAuthenticationChannelRequest(bindingMessage); + AuthenticationChannelRequest authenticationChannelReq = testRequest.getRequest(); + Assert.assertThat(authenticationChannelReq.getBindingMessage(), is(equalTo(bindingMessage))); + Assert.assertThat(authenticationChannelReq.getScope(), is(containsString(OAuth2Constants.SCOPE_OPENID))); + + // user Authentication Channel completed + EventRepresentation loginEvent = doAuthenticationChannelCallback(testRequest); + String sessionId = loginEvent.getSessionId(); + String codeId = loginEvent.getDetails().get(Details.CODE_ID); + String userId = loginEvent.getUserId(); + + // user Token Request + OAuthClient.AccessTokenResponse tokenRes = doBackchannelAuthenticationTokenRequest(username, response.getAuthReqId()); + + // 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); + } + } + + private AuthorizationEndpointRequestObject createValidSharedAuthenticationRequest() throws URISyntaxException { + AuthorizationEndpointRequestObject requestObject = new AuthorizationEndpointRequestObject(); + requestObject.id(org.keycloak.models.utils.KeycloakModelUtils.generateId()); + requestObject.iat(Long.valueOf(Time.currentTime())); + requestObject.exp(requestObject.getIat() + Long.valueOf(300)); + requestObject.nbf(requestObject.getIat()); + requestObject.setScope("openid"); + requestObject.setMax_age(Integer.valueOf(600)); + requestObject.setOtherClaims("custom_claim_zwei", "gelb"); + requestObject.audience(Urls.realmIssuer(new URI(suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth"), TEST_REALM_NAME), "https://example.com"); + return requestObject; + } + + protected void registerSharedAuthenticationRequest(AuthorizationEndpointRequestObject requestObject, String clientId, String sigAlg, boolean isUseRequestUri) throws URISyntaxException, IOException { + registerSharedAuthenticationRequest(requestObject, clientId, sigAlg, isUseRequestUri, null); + } + + protected void registerSharedAuthenticationRequest(AuthorizationEndpointRequestObject requestObject, String clientId, String sigAlg, boolean isUseRequestUri, String clientSecret) throws URISyntaxException, IOException { + TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); + + // Set required signature for request_uri + // use and set jwks_url + ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm(TEST_REALM_NAME), clientId); + ClientRepresentation clientRep = clientResource.toRepresentation(); + Map attr = Optional.ofNullable(clientRep.getAttributes()).orElse(new HashMap<>()); + attr.put(CibaConfig.CIBA_BACKCHANNEL_AUTH_REQUEST_SIGNING_ALG, sigAlg); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setUseJwksUrl(true); + String jwksUrl = TestApplicationResourceUrls.clientJwksUri(); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setJwksUrl(jwksUrl); + clientResource.update(clientRep); + + oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); + + // register request object + byte[] contentBytes = JsonSerialization.writeValueAsBytes(requestObject); + String encodedRequestObject = Base64Url.encode(contentBytes); + if (clientSecret != null) { + oidcClientEndpointsResource.registerOIDCRequestSymmetricSig(encodedRequestObject, sigAlg, clientSecret); + } else { + // generate and register client keypair + oidcClientEndpointsResource.generateKeys(sigAlg); + + oidcClientEndpointsResource.registerOIDCRequest(encodedRequestObject, sigAlg); + } + + if (isUseRequestUri) { + oauth.request(null); + oauth.requestUri(TestApplicationResourceUrls.clientRequestUri()); + } else { + oauth.requestUri(null); + oauth.request(oidcClientEndpointsResource.getOIDCRequest()); + } } private String createClientDynamically(String clientName, Consumer op) throws ClientRegistrationException { @@ -1162,13 +1371,20 @@ public class CIBATest extends AbstractTestRealmKeycloakTest { attributes.put(CibaConfig.CIBA_BACKCHANNEL_TOKEN_DELIVERY_MODE_PER_CLIENT, "poll"); attributes.put(CibaConfig.OIDC_CIBA_GRANT_ENABLED, Boolean.TRUE.toString()); clientRep.setAttributes(attributes); + List requestUris = new ArrayList<>(OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).getRequestUris()); + requestUris.add(TestApplicationResourceUrls.clientRequestUri()); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestUris(requestUris); clientResource.update(clientRep); } private void revertCIBASettings(ClientResource clientResource, ClientRepresentation clientRep) { Map attributes = Optional.ofNullable(clientRep.getAttributes()).orElse(new HashMap<>()); attributes.remove(CibaConfig.CIBA_BACKCHANNEL_TOKEN_DELIVERY_MODE_PER_CLIENT); + attributes.remove(CibaConfig.OIDC_CIBA_GRANT_ENABLED, Boolean.TRUE.toString()); clientRep.setAttributes(attributes); + List requestUris = new ArrayList<>(OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).getRequestUris()); + requestUris.remove(TestApplicationResourceUrls.clientRequestUri()); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestUris(requestUris); clientResource.update(clientRep); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java index 535cfa2694..94a24014e5 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java @@ -173,6 +173,7 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest { assertEquals(oidcConfig.getBackchannelAuthenticationEndpoint(), oauth.getBackchannelAuthenticationUrl()); assertContains(oidcConfig.getGrantTypesSupported(), OAuth2Constants.CIBA_GRANT_TYPE); Assert.assertNames(oidcConfig.getBackchannelTokenDeliveryModesSupported(), "poll"); + 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.getBackchannelLogoutSessionSupported()); diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties index 50125310a1..bb391adf2e 100644 --- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties +++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties @@ -403,6 +403,8 @@ request-object-signature-alg=Request Object Signature Algorithm request-object-signature-alg.tooltip=JWA algorithm, which client needs to use when sending OIDC request object specified by 'request' or 'request_uri' parameters. If set to 'any', Request object can be signed by any algorithm (including 'none' ). request-object-required=Request Object Required request-object-required.tooltip=Specifies if the client needs to provide a request object with their authorization requests, and what method they can use for this. If set to "not required", providing a request object is optional. In all other cases, providing a request object is mandatory. If set to "request", the request object must be provided by value. If set to "request_uri", the request object must be provided by reference. If set to "request or request_uri", either method can be used. +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. request-uris=Valid Request URIs request-uris.tooltip=List of valid URIs, which can be used as values of 'request_uri' parameter during OpenID Connect authentication request. There is support for the same capabilities like for Valid Redirect URIs. For example wildcards or relative paths. fine-saml-endpoint-conf=Fine Grain SAML Endpoint Configuration diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js index 9e2c5601af..ff10275de5 100755 --- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js @@ -1305,6 +1305,9 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro var attrVal4 = $scope.client.attributes['pkce.code.challenge.method']; $scope.pkceCodeChallengeMethod = attrVal4==null ? 'none' : attrVal4; + var attrVal5 = $scope.client.attributes['ciba.backchannel.auth.request.signing.alg']; + $scope.cibaBackchannelAuthRequestSigningAlg = attrVal5==null ? 'none' : attrVal5; + if ($scope.client.attributes["exclude.session.state.from.auth.response"]) { if ($scope.client.attributes["exclude.session.state.from.auth.response"] == "true") { $scope.excludeSessionStateFromAuthResponse = true; @@ -1513,6 +1516,14 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro $scope.clientEdit.attributes['pkce.code.challenge.method'] = $scope.pkceCodeChallengeMethod; }; + $scope.changeCibaBackchannelAuthRequestSigningAlg = function() { + if ($scope.cibaBackchannelAuthRequestSigningAlg === 'none') { + $scope.clientEdit.attributes['ciba.backchannel.auth.request.signing.alg'] = null; + } else { + $scope.clientEdit.attributes['ciba.backchannel.auth.request.signing.alg'] = $scope.cibaBackchannelAuthRequestSigningAlg; + } + }; + $scope.$watch(function() { return $location.path(); }, function() { diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html index abae942ea4..1984e16df0 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html @@ -572,6 +572,20 @@ {{:: 'request-object-required.tooltip' | translate}} +
+ +
+
+ +
+
+ {{:: 'ciba-backchannel-auth-request-signing-alg.tooltip' | translate}} +