KEYCLOAK-17936 FAPI-CIBA : support Signed Authentication Request

Co-authored-by: Pritish Joshi <pritish@banfico.com>
Co-authored-by: mposolda <mposolda@gmail.com>
This commit is contained in:
Takashi Norimatsu 2021-05-04 14:07:06 +09:00 committed by Marek Posolda
parent 948f453e2d
commit 57c80483bb
24 changed files with 1005 additions and 30 deletions

View file

@ -151,6 +151,9 @@ public class OIDCConfigurationRepresentation {
@JsonProperty("backchannel_authentication_endpoint")
private String backchannelAuthenticationEndpoint;
@JsonProperty("backchannel_authentication_request_signing_alg_values_supported")
private List<String> backchannelAuthenticationRequestSigningAlgValuesSupported;
protected Map<String, Object> otherClaims = new HashMap<String, Object>();
public String getIssuer() {
@ -461,6 +464,14 @@ public class OIDCConfigurationRepresentation {
this.backchannelAuthenticationEndpoint = backchannelAuthenticationEndpoint;
}
public List<String> getBackchannelAuthenticationRequestSigningAlgValuesSupported() {
return backchannelAuthenticationRequestSigningAlgValuesSupported;
}
public void setBackchannelAuthenticationRequestSigningAlgValuesSupported(List<String> backchannelAuthenticationRequestSigningAlgValuesSupported) {
this.backchannelAuthenticationRequestSigningAlgValuesSupported = backchannelAuthenticationRequestSigningAlgValuesSupported;
}
@JsonAnyGetter
public Map<String, Object> getOtherClaims() {
return otherClaims;

View file

@ -128,6 +128,8 @@ public class OIDCClientRepresentation {
// OIDC CIBA
private String backchannel_token_delivery_mode;
private String backchannel_authentication_request_signing_alg;
public List<String> 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;
}
}

View file

@ -25,6 +25,8 @@ public interface SignatureProvider extends Provider {
SignatureVerifierContext verifier(String kid) throws VerificationException;
boolean isAsymmetricAlgorithm();
@Override
default void close() {
}

View file

@ -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<RealmModel> 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) {

View file

@ -39,4 +39,8 @@ public class AsymmetricSignatureProvider implements SignatureProvider {
return new ServerAsymmetricSignatureVerifierContext(session, kid, algorithm);
}
@Override
public boolean isAsymmetricAlgorithm() {
return true;
}
}

View file

@ -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;

View file

@ -39,4 +39,8 @@ public class MacSecretSignatureProvider implements SignatureProvider {
return new ServerMacSignatureVerifierContext(session, kid, algorithm);
}
@Override
public boolean isAsymmetricAlgorithm() {
return false;
}
}

View file

@ -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<String> list(String... values) {
return Arrays.asList(values);
}
@ -204,6 +217,15 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
return supportedAlgorithms.collect(Collectors.toList());
}
private List<String> 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<String> getSupportedSigningAlgorithms(boolean includeNone) {
return getSupportedAlgorithms(SignatureProvider.class, includeNone);
}
@ -212,6 +234,10 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
return getSupportedAlgorithms(ClientSignatureVerifierProvider.class, includeNone);
}
private List<String> getSupportedBackchannelAuthenticationRequestSigningAlgorithms() {
return getSupportedAsymmetricAlgorithms();
}
private List<String> getSupportedIdTokenEncryptionAlg(boolean includeNone) {
return getSupportedAlgorithms(CekManagementProvider.class, includeNone);
}

View file

@ -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);

View file

@ -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<String, String> 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<String, String> 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);

View file

@ -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 <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
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<String, String> 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<String, String> getAdditionalReqParams() {
return additionalReqParams;
}
public String getInvalidRequestMessage() {
return invalidRequestMessage;
}
}

View file

@ -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 <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
class BackchannelAuthenticationEndpointRequestBodyParser extends BackchannelAuthenticationEndpointRequestParser {
private final MultivaluedMap<String, String> requestParams;
private String invalidRequestMessage = null;
public BackchannelAuthenticationEndpointRequestBodyParser(MultivaluedMap<String, String> 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<String> keySet() {
return requestParams.keySet();
}
private void checkDuplicated(MultivaluedMap<String, String> requestParams, String paramName) {
if (invalidRequestMessage == null) {
if (requestParams.get(paramName) != null && requestParams.get(paramName).size() != 1) {
invalidRequestMessage = "duplicated parameter";
}
}
}
}

View file

@ -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 <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
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<String> 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<String, String> 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> 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<String> keySet();
}

View file

@ -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 <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
public class BackchannelAuthenticationEndpointRequestParserProcessor {
public static BackchannelAuthenticationEndpointRequest parseRequest(EventBuilder event, KeycloakSession session, ClientModel client, MultivaluedMap<String, String> 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<String> 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);
}
}
}

View file

@ -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 <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
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<String> keySet() {
HashSet<String> keys = new HashSet<>();
requestParams.fieldNames().forEachRemaining(keys::add);
return keys;
}
static class TypedHashMap extends HashMap<String, Object> {
}
}

View file

@ -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<String, String> attr = Optional.ofNullable(client.getAttributes()).orElse(new HashMap<>());
attr.put(CibaConfig.CIBA_BACKCHANNEL_AUTH_REQUEST_SIGNING_ALG, backchannelAuthenticationRequestSigningAlg);
client.setAttributes(attr);
} else {
throw new ClientRegistrationException("Unsupported requested backchannel_authentication_request_signing_alg");
}
}
return client;
}
@ -202,6 +218,23 @@ public class DescriptionConverter {
return false;
}
private static boolean isSupportedBackchannelAuthenticationRequestSigningAlg(KeycloakSession session, String alg) {
Stream<String> supportedAlgorithms = session.getKeycloakSessionFactory().getProviderFactoriesStream(ClientSignatureVerifierProvider.class)
.map(ProviderFactory::getId);
supportedAlgorithms = Streams.concat(supportedAlgorithms, Stream.of("none"));
return supportedAlgorithms.collect(Collectors.toList()).contains(alg);
}
private static List<String> getSupportedAlgorithms(KeycloakSession session, Class<? extends Provider> clazz, boolean includeNone) {
Stream<String> supportedAlgorithms = session.getKeycloakSessionFactory().getProviderFactoriesStream(clazz)
.map(ProviderFactory::getId);
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<ProtocolMapperRepresentation> foundPairwiseMappers = PairwiseSubMapperUtils.getPairwiseSubMappers(client);

View file

@ -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,6 +248,20 @@ 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 {
@ -249,8 +269,7 @@ public class TestingOIDCEndpointsApplicationResource {
} catch (IOException e) {
throw new BadRequestException("deserialize request object failed : " + e.getMessage());
}
setOidcRequest(oidcRequest, jwaAlgorithm);
return oidcRequest;
}
private void setOidcRequest(Object oidcRequest, String jwaAlgorithm) {
@ -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

View file

@ -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)

View file

@ -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 {

View file

@ -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<String, String> 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<String> 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<String, String> 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<String, String> 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<OIDCClientRepresentation> 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<String> 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<String, String> attributes = Optional.ofNullable(clientRep.getAttributes()).orElse(new HashMap<>());
attributes.remove(CibaConfig.CIBA_BACKCHANNEL_TOKEN_DELIVERY_MODE_PER_CLIENT);
attributes.remove(CibaConfig.OIDC_CIBA_GRANT_ENABLED, Boolean.TRUE.toString());
clientRep.setAttributes(attributes);
List<String> requestUris = new ArrayList<>(OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).getRequestUris());
requestUris.remove(TestApplicationResourceUrls.clientRequestUri());
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestUris(requestUris);
clientResource.update(clientRep);
}

View file

@ -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());

View file

@ -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

View file

@ -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() {

View file

@ -572,6 +572,20 @@
</div>
<kc-tooltip>{{:: 'request-object-required.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix block" data-ng-show="protocol == 'openid-connect' && !clientEdit.publicClient && !clientEdit.bearerOnly && serverInfo.featureEnabled('CIBA') && oidcCibaGrantEnabled == true"">
<label class="col-md-2 control-label" for="cibaBackchannelAuthRequestSigningAlg">{{:: 'ciba-backchannel-auth-request-signing-alg' | translate}}</label>
<div class="col-sm-6">
<div>
<select class="form-control" id="cibaBackchannelAuthRequestSigningAlg"
ng-change="changeCibaBackchannelAuthRequestSigningAlg()"
ng-model="cibaBackchannelAuthRequestSigningAlg">
<option value="none">none</option>
<option ng-repeat="provider in serverInfo.listProviderIds('clientSignature')" value="{{provider}}">{{provider}}</option>
</select>
</div>
</div>
<kc-tooltip>{{:: 'ciba-backchannel-auth-request-signing-alg.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix block" data-ng-show="protocol == 'openid-connect'">
<label class="col-md-2 control-label" for="newRequestUri">{{:: 'request-uris' | translate}}</label>
<div class="col-sm-6">