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:
parent
948f453e2d
commit
57c80483bb
24 changed files with 1005 additions and 30 deletions
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,8 @@ public interface SignatureProvider extends Provider {
|
|||
|
||||
SignatureVerifierContext verifier(String kid) throws VerificationException;
|
||||
|
||||
boolean isAsymmetricAlgorithm();
|
||||
|
||||
@Override
|
||||
default void close() {
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -39,4 +39,8 @@ public class AsymmetricSignatureProvider implements SignatureProvider {
|
|||
return new ServerAsymmetricSignatureVerifierContext(session, kid, algorithm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAsymmetricAlgorithm() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -39,4 +39,8 @@ public class MacSecretSignatureProvider implements SignatureProvider {
|
|||
return new ServerMacSignatureVerifierContext(session, kid, algorithm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAsymmetricAlgorithm() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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> {
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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">
|
||||
|
|
Loading…
Reference in a new issue