KEYCLOAK-18127 Option for skip return user's claims in the ID Token for hybrid flow

This commit is contained in:
Takashi Norimatsu 2021-05-21 16:59:31 +09:00 committed by Marek Posolda
parent 5d578f0c90
commit 6532baa9a7
8 changed files with 278 additions and 2 deletions

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

@ -762,6 +762,14 @@
<kc-tooltip>{{:: 'tls-client-certificate-bound-access-tokens.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix block" data-ng-show="protocol == 'openid-connect' && clientEdit.standardFlowEnabled && clientEdit.implicitFlowEnabled">
<label class="col-md-2 control-label" for="useIdTokenAsDetachedSignature">{{:: 'use-idtoken-as-detached-signature' | translate}}</label>
<div class="col-sm-6">
<input ng-model="useIdTokenAsDetachedSignature" ng-click="switchChange()" name="useIdTokenAsDetachedSignature" id="useIdTokenAsDetachedSignature" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
</div>
<kc-tooltip>{{:: 'use-idtoken-as-detached-signature.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="changePkceCodeChallengeMethod">{{:: 'pkce-code-challenge-method' | translate}}</label>
<div class="col-sm-6">