diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCConfigAttributes.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCConfigAttributes.java
index f6eacf0849..3c525542f5 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCConfigAttributes.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCConfigAttributes.java
@@ -64,6 +64,8 @@ public final class OIDCConfigAttributes {
public static final String USE_REFRESH_TOKEN = "use.refresh.tokens";
+ public static final String ID_TOKEN_AS_DETACHED_SIGNATURE = "id.token.as.detached.signature";
+
private OIDCConfigAttributes() {
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
index 8466bd67fa..e10c9770ba 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
@@ -57,6 +57,7 @@ import org.keycloak.util.TokenUtil;
import java.io.IOException;
import java.net.URI;
+import java.util.Optional;
import java.util.UUID;
import javax.ws.rs.core.HttpHeaders;
@@ -243,7 +244,7 @@ public class OIDCLoginProtocol implements LoginProtocol {
if (responseType.hasResponseType(OIDCResponseType.ID_TOKEN)) {
- responseBuilder.generateIDToken();
+ responseBuilder.generateIDToken(isIdTokenAsDetachedSignature(clientSession.getClient()));
if (responseType.hasResponseType(OIDCResponseType.TOKEN)) {
responseBuilder.generateAccessTokenHash();
@@ -275,6 +276,12 @@ public class OIDCLoginProtocol implements LoginProtocol {
return redirectUri.build();
}
+ // For FAPI 1.0 Advanced
+ private boolean isIdTokenAsDetachedSignature(ClientModel client) {
+ if (client == null) return false;
+ return Boolean.valueOf(Optional.ofNullable(client.getAttribute(OIDCConfigAttributes.ID_TOKEN_AS_DETACHED_SIGNATURE)).orElse(Boolean.FALSE.toString())).booleanValue();
+ }
+
@Override
public Response sendError(AuthenticationSessionModel authSession, Error error) {
if (isOAuth2DeviceVerificationFlow(authSession)) {
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
index 34e20985a8..83713f2412 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
@@ -967,6 +967,10 @@ public class TokenManager {
}
public AccessTokenResponseBuilder generateIDToken() {
+ return generateIDToken(false);
+ }
+
+ public AccessTokenResponseBuilder generateIDToken(boolean isIdTokenAsDetachedSignature) {
if (accessToken == null) {
throw new IllegalStateException("accessToken not set");
}
@@ -983,7 +987,9 @@ public class TokenManager {
idToken.setSessionState(accessToken.getSessionState());
idToken.expiration(accessToken.getExpiration());
idToken.setAcr(accessToken.getAcr());
- transformIDToken(session, idToken, userSession, clientSessionCtx);
+ if (isIdTokenAsDetachedSignature == false) {
+ transformIDToken(session, idToken, userSession, clientSessionCtx);
+ }
return this;
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCHybridResponseTypeCodeIDTokenAsDetachedSigTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCHybridResponseTypeCodeIDTokenAsDetachedSigTest.java
new file mode 100644
index 0000000000..8da72edeb2
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCHybridResponseTypeCodeIDTokenAsDetachedSigTest.java
@@ -0,0 +1,118 @@
+/*
+ * 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.testsuite.oidc.flows;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.events.Details;
+import org.keycloak.jose.jws.crypto.HashUtils;
+import org.keycloak.protocol.oidc.OIDCConfigAttributes;
+import org.keycloak.protocol.oidc.utils.OIDCResponseType;
+import org.keycloak.representations.IDToken;
+import org.keycloak.representations.idm.EventRepresentation;
+import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.util.OAuthClient;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Tests with response_type=code id_token as detached signature
+ *
+ * @author Takashi Norimatsu
+ */
+public class OIDCHybridResponseTypeCodeIDTokenAsDetachedSigTest extends AbstractOIDCResponseTypeTest {
+
+ @Before
+ public void clientConfiguration() {
+ clientManagerBuilder().standardFlow(true).implicitFlow(true).updateAttribute(OIDCConfigAttributes.ID_TOKEN_AS_DETACHED_SIGNATURE, Boolean.TRUE.toString());
+
+ oauth.clientId("test-app");
+ oauth.responseType(OIDCResponseType.CODE + " " + OIDCResponseType.ID_TOKEN);
+ }
+
+
+ @Override
+ protected boolean isFragment() {
+ return true;
+ }
+
+
+ protected List testAuthzResponseAndRetrieveIDTokens(OAuthClient.AuthorizationEndpointResponse authzResponse, EventRepresentation loginEvent) {
+ Assert.assertEquals(OIDCResponseType.CODE + " " + OIDCResponseType.ID_TOKEN, loginEvent.getDetails().get(Details.RESPONSE_TYPE));
+
+ // IDToken from the authorization response
+ Assert.assertNull(authzResponse.getAccessToken());
+ String idTokenStr = authzResponse.getIdToken();
+ IDToken idToken = oauth.verifyIDToken(idTokenStr);
+ // confirm ID token as detached signature does not include authenticated user's claims
+ Assert.assertNull(idToken.getEmailVerified());
+ Assert.assertNull(idToken.getName());
+ Assert.assertNull(idToken.getPreferredUsername());
+ Assert.assertNull(idToken.getGivenName());
+ Assert.assertNull(idToken.getFamilyName());
+ Assert.assertNull(idToken.getEmail());
+
+ // Validate "at_hash"
+ Assert.assertNull(idToken.getAccessTokenHash());
+
+ // Validate "c_hash"
+ assertValidCodeHash(idToken.getCodeHash(), authzResponse.getCode());
+
+ // Financial API - Part 2: Read and Write API Security Profile
+ // http://openid.net/specs/openid-financial-api-part-2.html#authorization-server
+ // Validate "s_hash"
+ Assert.assertNotNull(idToken.getStateHash());
+
+ Assert.assertEquals(idToken.getStateHash(), HashUtils.oidcHash(getIdTokenSignatureAlgorithm(), authzResponse.getState()));
+
+ // Validate if token_type is null
+ Assert.assertNull(authzResponse.getTokenType());
+
+ // Validate if expires_in is null
+ Assert.assertNull(authzResponse.getExpiresIn());
+
+ // IDToken exchanged for the code
+ IDToken idToken2 = sendTokenRequestAndGetIDToken(loginEvent);
+ // confirm ordinal ID token includes authenticated user's claims
+ Assert.assertNotNull(idToken2.getEmailVerified());
+ Assert.assertNotNull(idToken2.getName());
+ Assert.assertNotNull(idToken2.getPreferredUsername());
+ Assert.assertNotNull(idToken2.getGivenName());
+ Assert.assertNotNull(idToken2.getFamilyName());
+ Assert.assertNotNull(idToken2.getEmail());
+
+ return Arrays.asList(idToken, idToken2);
+ }
+
+
+ @Test
+ public void nonceNotUsedErrorExpected() {
+ super.validateNonceNotUsedErrorExpected();
+ }
+
+ @Test
+ public void errorStandardFlowNotAllowed() throws Exception {
+ super.validateErrorStandardFlowNotAllowed();
+ }
+
+ @Test
+ public void errorImplicitFlowNotAllowed() throws Exception {
+ super.validateErrorImplicitFlowNotAllowed();
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCHybridResponseTypeCodeIDTokenAsDetachedSigTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCHybridResponseTypeCodeIDTokenAsDetachedSigTokenTest.java
new file mode 100644
index 0000000000..63d1d616b1
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCHybridResponseTypeCodeIDTokenAsDetachedSigTokenTest.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2021 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.oidc.flows;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.events.Details;
+import org.keycloak.jose.jws.crypto.HashUtils;
+import org.keycloak.protocol.oidc.OIDCConfigAttributes;
+import org.keycloak.protocol.oidc.utils.OIDCResponseType;
+import org.keycloak.representations.IDToken;
+import org.keycloak.representations.idm.EventRepresentation;
+import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.util.OAuthClient;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Tests with response_type=code id_token token as detached signature
+ *
+ * @author Takashi Norimatsu
+ */
+public class OIDCHybridResponseTypeCodeIDTokenAsDetachedSigTokenTest extends AbstractOIDCResponseTypeTest {
+
+ @Before
+ public void clientConfiguration() {
+ clientManagerBuilder().standardFlow(true).implicitFlow(true).updateAttribute(OIDCConfigAttributes.ID_TOKEN_AS_DETACHED_SIGNATURE, Boolean.TRUE.toString());
+
+ oauth.clientId("test-app");
+ oauth.responseType(OIDCResponseType.CODE + " " + OIDCResponseType.ID_TOKEN + " " + OIDCResponseType.TOKEN);
+ }
+
+
+ @Override
+ protected boolean isFragment() {
+ return true;
+ }
+
+
+ protected List testAuthzResponseAndRetrieveIDTokens(OAuthClient.AuthorizationEndpointResponse authzResponse, EventRepresentation loginEvent) {
+ Assert.assertEquals(OIDCResponseType.CODE + " " + OIDCResponseType.ID_TOKEN + " " + OIDCResponseType.TOKEN, loginEvent.getDetails().get(Details.RESPONSE_TYPE));
+
+ // IDToken from the authorization response
+ Assert.assertNotNull(authzResponse.getAccessToken());
+ String idTokenStr = authzResponse.getIdToken();
+ IDToken idToken = oauth.verifyIDToken(idTokenStr);
+ // confirm ID token as detached signature does not include authenticated user's claims
+ Assert.assertNull(idToken.getEmailVerified());
+ Assert.assertNull(idToken.getName());
+ Assert.assertNull(idToken.getPreferredUsername());
+ Assert.assertNull(idToken.getGivenName());
+ Assert.assertNull(idToken.getFamilyName());
+ Assert.assertNull(idToken.getEmail());
+
+ // Validate "at_hash"
+ assertValidAccessTokenHash(idToken.getAccessTokenHash(), authzResponse.getAccessToken());
+
+ // Validate "c_hash"
+ assertValidCodeHash(idToken.getCodeHash(), authzResponse.getCode());
+
+ // Financial API - Part 2: Read and Write API Security Profile
+ // http://openid.net/specs/openid-financial-api-part-2.html#authorization-server
+ // Validate "s_hash"
+ Assert.assertNotNull(idToken.getStateHash());
+
+ Assert.assertEquals(idToken.getStateHash(), HashUtils.oidcHash(getIdTokenSignatureAlgorithm(), authzResponse.getState()));
+
+ // Validate if token_type is present
+ Assert.assertNotNull(authzResponse.getTokenType());
+
+ // Validate if expires_in is present
+ Assert.assertNotNull(authzResponse.getExpiresIn());
+
+ // IDToken exchanged for the code
+ IDToken idToken2 = sendTokenRequestAndGetIDToken(loginEvent);
+ // confirm ordinal ID token includes authenticated user's claims
+ Assert.assertNotNull(idToken2.getEmailVerified());
+ Assert.assertNotNull(idToken2.getName());
+ Assert.assertNotNull(idToken2.getPreferredUsername());
+ Assert.assertNotNull(idToken2.getGivenName());
+ Assert.assertNotNull(idToken2.getFamilyName());
+ Assert.assertNotNull(idToken2.getEmail());
+
+ return Arrays.asList(idToken, idToken2);
+ }
+
+ @Test
+ public void nonceNotUsedErrorExpected() {
+ super.validateNonceNotUsedErrorExpected();
+ }
+
+ @Test
+ public void errorStandardFlowNotAllowed() throws Exception {
+ super.validateErrorStandardFlowNotAllowed();
+ }
+
+ @Test
+ public void errorImplicitFlowNotAllowed() throws Exception {
+ super.validateErrorImplicitFlowNotAllowed();
+ }
+}
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index 41731bd1b2..7d3ee79914 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -1862,6 +1862,9 @@ subjectdn-tooltip=A regular expression for validating Subject DN in the Client C
pkce-code-challenge-method=Proof Key for Code Exchange Code Challenge Method
pkce-code-challenge-method.tooltip=Choose which code challenge method for PKCE is used. If not specified, keycloak does not applies PKCE to a client unless the client sends an authorization request with appropriate code challenge and code exchange method.
+use-idtoken-as-detached-signature=Use ID Token as a Detached Signature
+use-idtoken-as-detached-signature.tooltip=This makes ID token returned from Authorization Endpoint in OIDC Hybrid flow use as a detached signature defined in FAPI 1.0 Advanced Security Profile. Therefore, this ID token does not include an authenticated user's information.
+
key-not-allowed-here=Key '{{character}}' is not allowed here.
# KEYCLOAK-10927 Implement LDAPv3 Password Modify Extended Operation
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
index b2259478c8..56839490db 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
@@ -1115,6 +1115,7 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-3
$scope.tlsClientCertificateBoundAccessTokens = false;
$scope.useRefreshTokens = true;
+ $scope.useIdTokenAsDetachedSignature = false;
$scope.accessTokenLifespan = TimeUnit2.asUnit(client.attributes['access.token.lifespan']);
$scope.samlAssertionLifespan = TimeUnit2.asUnit(client.attributes['saml.assertion.lifespan']);
@@ -1319,6 +1320,14 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
}
}
+ if ($scope.client.attributes["id.token.as.detached.signature"]) {
+ if ($scope.client.attributes["id.token.as.detached.signature"] == "true") {
+ $scope.useIdTokenAsDetachedSignature = true;
+ } else {
+ $scope.useIdTokenAsDetachedSignature = false;
+ }
+ }
+
// KEYCLOAK-6771 Certificate Bound Token
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-3
if ($scope.client.attributes["tls.client.certificate.bound.access.tokens"]) {
@@ -1748,6 +1757,12 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
$scope.clientEdit.attributes["use.refresh.tokens"] = "false";
}
+ if ($scope.useIdTokenAsDetachedSignature == true) {
+ $scope.clientEdit.attributes["id.token.as.detached.signature"] = "true";
+ } else {
+ $scope.clientEdit.attributes["id.token.as.detached.signature"] = "false";
+ }
+
// KEYCLOAK-6771 Certificate Bound Token
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-3
if ($scope.tlsClientCertificateBoundAccessTokens == true) {
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
index 073c170148..5245a0b25a 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
@@ -762,6 +762,14 @@
{{:: 'tls-client-certificate-bound-access-tokens.tooltip' | translate}}
+
+