KEYCLOAK-18127 Option for skip return user's claims in the ID Token for hybrid flow
This commit is contained in:
parent
5d578f0c90
commit
6532baa9a7
8 changed files with 278 additions and 2 deletions
|
@ -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() {
|
||||
}
|
||||
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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">
|
||||
|
|
Loading…
Reference in a new issue