diff --git a/core/src/main/java/org/keycloak/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java index 1a8aaf6a1d..48317a4dc4 100755 --- a/core/src/main/java/org/keycloak/OAuth2Constants.java +++ b/core/src/main/java/org/keycloak/OAuth2Constants.java @@ -133,6 +133,9 @@ public interface OAuth2Constants { String DISPLAY_CONSOLE = "console"; String INTERVAL = "interval"; String USER_CODE = "user_code"; + + // https://openid.net/specs/openid-financial-api-jarm-ID1.html + String RESPONSE = "response"; } diff --git a/core/src/main/java/org/keycloak/TokenCategory.java b/core/src/main/java/org/keycloak/TokenCategory.java index fb83321ca4..45ab4b0d7d 100644 --- a/core/src/main/java/org/keycloak/TokenCategory.java +++ b/core/src/main/java/org/keycloak/TokenCategory.java @@ -22,5 +22,6 @@ public enum TokenCategory { ID, ADMIN, USERINFO, - LOGOUT + LOGOUT, + AUTHORIZATION_RESPONSE } diff --git a/core/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java b/core/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java index 06712f1e2d..4824bfc98a 100755 --- a/core/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java +++ b/core/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java @@ -97,6 +97,15 @@ public class OIDCConfigurationRepresentation { @JsonProperty("introspection_endpoint_auth_signing_alg_values_supported") private List introspectionEndpointAuthSigningAlgValuesSupported; + @JsonProperty("authorization_signing_alg_values_supported") + private List authorizationSigningAlgValuesSupported; + + @JsonProperty("authorization_encryption_alg_values_supported") + private List authorizationEncryptionAlgValuesSupported; + + @JsonProperty("authorization_encryption_enc_values_supported") + private List authorizationEncryptionEncValuesSupported; + @JsonProperty("claims_supported") private List claimsSupported; @@ -489,4 +498,28 @@ public class OIDCConfigurationRepresentation { public String getDeviceAuthorizationEndpoint() { return deviceAuthorizationEndpoint; } + + public List getAuthorizationSigningAlgValuesSupported() { + return authorizationSigningAlgValuesSupported; + } + + public void setAuthorizationSigningAlgValuesSupported(List authorizationSigningAlgValuesSupported) { + this.authorizationSigningAlgValuesSupported = authorizationSigningAlgValuesSupported; + } + + public List getAuthorizationEncryptionAlgValuesSupported() { + return authorizationEncryptionAlgValuesSupported; + } + + public void setAuthorizationEncryptionAlgValuesSupported(List authorizationEncryptionAlgValuesSupported) { + this.authorizationEncryptionAlgValuesSupported = authorizationEncryptionAlgValuesSupported; + } + + public List getAuthorizationEncryptionEncValuesSupported() { + return authorizationEncryptionEncValuesSupported; + } + + public void setAuthorizationEncryptionEncValuesSupported(List authorizationEncryptionEncValuesSupported) { + this.authorizationEncryptionEncValuesSupported = authorizationEncryptionEncValuesSupported; + } } diff --git a/core/src/main/java/org/keycloak/representations/AuthorizationResponseToken.java b/core/src/main/java/org/keycloak/representations/AuthorizationResponseToken.java new file mode 100644 index 0000000000..2356dc3700 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/AuthorizationResponseToken.java @@ -0,0 +1,11 @@ +package org.keycloak.representations; + +import org.keycloak.TokenCategory; + +public class AuthorizationResponseToken extends JsonWebToken{ + + @Override + public TokenCategory getCategory() { + return TokenCategory.AUTHORIZATION_RESPONSE; + } +} diff --git a/core/src/main/java/org/keycloak/representations/oidc/OIDCClientRepresentation.java b/core/src/main/java/org/keycloak/representations/oidc/OIDCClientRepresentation.java index 0ea0f9e50f..803ee3c7b5 100644 --- a/core/src/main/java/org/keycloak/representations/oidc/OIDCClientRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/oidc/OIDCClientRepresentation.java @@ -130,6 +130,13 @@ public class OIDCClientRepresentation { private String backchannel_authentication_request_signing_alg; + // FAPI JARM + private String authorization_signed_response_alg; + + private String authorization_encrypted_response_alg; + + private String authorization_encrypted_response_enc; + public List getRedirectUris() { return redirect_uris; } @@ -507,4 +514,28 @@ public class OIDCClientRepresentation { public void setBackchannelAuthenticationRequestSigningAlg(String backchannel_authentication_request_signing_alg) { this.backchannel_authentication_request_signing_alg = backchannel_authentication_request_signing_alg; } + + public String getAuthorizationSignedResponseAlg() { + return authorization_signed_response_alg; + } + + public void setAuthorizationSignedResponseAlg(String authorization_signed_response_alg) { + this.authorization_signed_response_alg = authorization_signed_response_alg; + } + + public String getAuthorizationEncryptedResponseAlg() { + return authorization_encrypted_response_alg; + } + + public void setAuthorizationEncryptedResponseAlg(String authorization_encrypted_response_alg) { + this.authorization_encrypted_response_alg = authorization_encrypted_response_alg; + } + + public String getAuthorizationEncryptedResponseEnc() { + return authorization_encrypted_response_enc; + } + + public void setAuthorizationEncryptedResponseEnc(String authorization_encrypted_response_enc) { + this.authorization_encrypted_response_enc = authorization_encrypted_response_enc; + } } diff --git a/services/src/main/java/org/keycloak/jose/jws/DefaultTokenManager.java b/services/src/main/java/org/keycloak/jose/jws/DefaultTokenManager.java index 354938a759..948c0ad5a0 100644 --- a/services/src/main/java/org/keycloak/jose/jws/DefaultTokenManager.java +++ b/services/src/main/java/org/keycloak/jose/jws/DefaultTokenManager.java @@ -139,6 +139,8 @@ public class DefaultTokenManager implements TokenManager { return getSignatureAlgorithm(OIDCConfigAttributes.ID_TOKEN_SIGNED_RESPONSE_ALG); case USERINFO: return getSignatureAlgorithm(OIDCConfigAttributes.USER_INFO_RESPONSE_SIGNATURE_ALG); + case AUTHORIZATION_RESPONSE: + return getSignatureAlgorithm(OIDCConfigAttributes.AUTHORIZATION_SIGNED_RESPONSE_ALG); default: throw new RuntimeException("Unknown token type"); } @@ -211,6 +213,8 @@ public class DefaultTokenManager implements TokenManager { case ID: case LOGOUT: return getCekManagementAlgorithm(OIDCConfigAttributes.ID_TOKEN_ENCRYPTED_RESPONSE_ALG); + case AUTHORIZATION_RESPONSE: + return getCekManagementAlgorithm(OIDCConfigAttributes.AUTHORIZATION_ENCRYPTED_RESPONSE_ALG); default: return null; } @@ -232,6 +236,8 @@ public class DefaultTokenManager implements TokenManager { case ID: case LOGOUT: return getEncryptAlgorithm(OIDCConfigAttributes.ID_TOKEN_ENCRYPTED_RESPONSE_ENC); + case AUTHORIZATION_RESPONSE: + return getEncryptAlgorithm(OIDCConfigAttributes.AUTHORIZATION_ENCRYPTED_RESPONSE_ENC); default: return null; } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java index 76884b69d3..3ed00d2266 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java @@ -195,6 +195,29 @@ public class OIDCAdvancedConfigWrapper { setAttribute(OIDCConfigAttributes.ID_TOKEN_ENCRYPTED_RESPONSE_ENC, encName); } + public String getAuthorizationSignedResponseAlg() { + return getAttribute(OIDCConfigAttributes.AUTHORIZATION_SIGNED_RESPONSE_ALG); + } + public void setAuthorizationSignedResponseAlg(String algName) { + setAttribute(OIDCConfigAttributes.AUTHORIZATION_SIGNED_RESPONSE_ALG, algName); + } + + public String getAuthorizationEncryptedResponseAlg() { + return getAttribute(OIDCConfigAttributes.AUTHORIZATION_ENCRYPTED_RESPONSE_ALG); + } + + public void setAuthorizationEncryptedResponseAlg(String algName) { + setAttribute(OIDCConfigAttributes.AUTHORIZATION_ENCRYPTED_RESPONSE_ALG, algName); + } + + public String getAuthorizationEncryptedResponseEnc() { + return getAttribute(OIDCConfigAttributes.AUTHORIZATION_ENCRYPTED_RESPONSE_ENC); + } + + public void setAuthorizationEncryptedResponseEnc(String encName) { + setAttribute(OIDCConfigAttributes.AUTHORIZATION_ENCRYPTED_RESPONSE_ENC, encName); + } + public String getTokenEndpointAuthSigningAlg() { return getAttribute(OIDCConfigAttributes.TOKEN_ENDPOINT_AUTH_SIGNING_ALG); } 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 3c525542f5..fe176e3224 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCConfigAttributes.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCConfigAttributes.java @@ -66,6 +66,10 @@ public final class OIDCConfigAttributes { public static final String ID_TOKEN_AS_DETACHED_SIGNATURE = "id.token.as.detached.signature"; + public static final String AUTHORIZATION_SIGNED_RESPONSE_ALG = "authorization.signed.response.alg"; + public static final String AUTHORIZATION_ENCRYPTED_RESPONSE_ALG = "authorization.encrypted.response.alg"; + public static final String AUTHORIZATION_ENCRYPTED_RESPONSE_ENC = "authorization.encrypted.response.enc"; + 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 0fa5f504d6..1f9e879670 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java @@ -202,7 +202,7 @@ public class OIDCLoginProtocol implements LoginProtocol { setupResponseTypeAndMode(responseTypeParam, responseModeParam); String redirect = authSession.getRedirectUri(); - OIDCRedirectUriBuilder redirectUri = OIDCRedirectUriBuilder.fromUri(redirect, responseMode); + OIDCRedirectUriBuilder redirectUri = OIDCRedirectUriBuilder.fromUri(redirect, responseMode, session, clientSession); String state = authSession.getClientNote(OIDCLoginProtocol.STATE_PARAM); logger.debugv("redirectAccessCode: state: {0}", state); if (state != null) @@ -287,14 +287,14 @@ public class OIDCLoginProtocol implements LoginProtocol { if (isOAuth2DeviceVerificationFlow(authSession)) { return denyOAuth2DeviceAuthorization(authSession, error, session); } - String responseTypeParam = authSession.getClientNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM); String responseModeParam = authSession.getClientNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM); setupResponseTypeAndMode(responseTypeParam, responseModeParam); String redirect = authSession.getRedirectUri(); String state = authSession.getClientNote(OIDCLoginProtocol.STATE_PARAM); - OIDCRedirectUriBuilder redirectUri = OIDCRedirectUriBuilder.fromUri(redirect, responseMode); + + OIDCRedirectUriBuilder redirectUri = OIDCRedirectUriBuilder.fromUri(redirect, responseMode, session, null); if (error != Error.CANCELLED_AIA_SILENT) { redirectUri.addParam(OAuth2Constants.ERROR, translateError(error)); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java index 2d846e9911..78a65a4c46 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java @@ -74,7 +74,7 @@ public class OIDCWellKnownProvider implements WellKnownProvider { public static final List DEFAULT_SUBJECT_TYPES_SUPPORTED = list("public", "pairwise"); - public static final List DEFAULT_RESPONSE_MODES_SUPPORTED = list("query", "fragment", "form_post"); + public static final List DEFAULT_RESPONSE_MODES_SUPPORTED = list("query", "fragment", "form_post", "query.jwt", "fragment.jwt", "form_post.jwt", "jwt"); public static final List DEFAULT_CLIENT_AUTH_SIGNING_ALG_VALUES_SUPPORTED = list(Algorithm.RS256.toString()); @@ -126,8 +126,8 @@ public class OIDCWellKnownProvider implements WellKnownProvider { config.setRegistrationEndpoint(RealmsResource.clientRegistrationUrl(backendUriInfo).path(ClientRegistrationService.class, "provider").build(realm.getName(), OIDCClientRegistrationProviderFactory.ID).toString()); config.setIdTokenSigningAlgValuesSupported(getSupportedSigningAlgorithms(false)); - config.setIdTokenEncryptionAlgValuesSupported(getSupportedIdTokenEncryptionAlg(false)); - config.setIdTokenEncryptionEncValuesSupported(getSupportedIdTokenEncryptionEnc(false)); + config.setIdTokenEncryptionAlgValuesSupported(getSupportedEncryptionAlg(false)); + config.setIdTokenEncryptionEncValuesSupported(getSupportedEncryptionEnc(false)); config.setUserInfoSigningAlgValuesSupported(getSupportedSigningAlgorithms(true)); config.setRequestObjectSigningAlgValuesSupported(getSupportedClientSigningAlgorithms(true)); config.setResponseTypesSupported(DEFAULT_RESPONSE_TYPES_SUPPORTED); @@ -140,6 +140,10 @@ public class OIDCWellKnownProvider implements WellKnownProvider { config.setIntrospectionEndpointAuthMethodsSupported(getClientAuthMethodsSupported()); config.setIntrospectionEndpointAuthSigningAlgValuesSupported(getSupportedClientSigningAlgorithms(false)); + config.setAuthorizationSigningAlgValuesSupported(getSupportedSigningAlgorithms(false)); + config.setAuthorizationEncryptionAlgValuesSupported(getSupportedEncryptionAlg(false)); + config.setAuthorizationEncryptionEncValuesSupported(getSupportedEncryptionEnc(false)); + config.setClaimsSupported(DEFAULT_CLAIMS_SUPPORTED); config.setClaimTypesSupported(DEFAULT_CLAIM_TYPES_SUPPORTED); config.setClaimsParameterSupported(true); @@ -228,11 +232,11 @@ public class OIDCWellKnownProvider implements WellKnownProvider { return getSupportedAsymmetricAlgorithms(); } - private List getSupportedIdTokenEncryptionAlg(boolean includeNone) { + private List getSupportedEncryptionAlg(boolean includeNone) { return getSupportedAlgorithms(CekManagementProvider.class, includeNone); } - private List getSupportedIdTokenEncryptionEnc(boolean includeNone) { + private List getSupportedEncryptionEnc(boolean includeNone) { return getSupportedAlgorithms(ContentEncryptionProvider.class, includeNone); } } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java index 0dd4296267..54cf97efac 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java @@ -34,6 +34,7 @@ import org.keycloak.models.Constants; import org.keycloak.models.RealmModel; import org.keycloak.protocol.AuthorizationEndpointBase; import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; +import org.keycloak.protocol.oidc.OIDCConfigAttributes; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest; @@ -53,6 +54,7 @@ import org.keycloak.services.resources.LoginActionsService; import org.keycloak.services.util.CacheControlUtil; import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.util.TokenUtil; +import org.keycloak.utils.StringUtil; import javax.ws.rs.Consumes; import javax.ws.rs.GET; @@ -61,7 +63,6 @@ import javax.ws.rs.Path; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; - import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -289,6 +290,14 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase { return redirectErrorToClient(OIDCResponseMode.QUERY, OAuthErrorException.INVALID_REQUEST, "Response_mode 'query' not allowed for implicit or hybrid flow"); } + if(parsedResponseType.isImplicitOrHybridFlow() && parsedResponseMode == OIDCResponseMode.QUERY_JWT && + (!StringUtil.isNotBlank(client.getAttribute(OIDCConfigAttributes.AUTHORIZATION_ENCRYPTED_RESPONSE_ALG)) || + !StringUtil.isNotBlank(client.getAttribute(OIDCConfigAttributes.AUTHORIZATION_ENCRYPTED_RESPONSE_ENC)))) { + ServicesLogger.LOGGER.responseModeQueryJwtNotAllowed(); + event.error(Errors.INVALID_REQUEST); + return redirectErrorToClient(OIDCResponseMode.QUERY_JWT, OAuthErrorException.INVALID_REQUEST, "Response_mode 'query.jwt' is allowed only when the authorization response token is encrypted"); + } + if ((parsedResponseType.hasResponseType(OIDCResponseType.CODE) || parsedResponseType.hasResponseType(OIDCResponseType.NONE)) && !client.isStandardFlowEnabled()) { ServicesLogger.LOGGER.flowNotAllowed("Standard"); event.error(Errors.NOT_ALLOWED); @@ -422,7 +431,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase { } private Response redirectErrorToClient(OIDCResponseMode responseMode, String error, String errorDescription) { - OIDCRedirectUriBuilder errorResponseBuilder = OIDCRedirectUriBuilder.fromUri(redirectUri, responseMode) + OIDCRedirectUriBuilder errorResponseBuilder = OIDCRedirectUriBuilder.fromUri(redirectUri, responseMode, session, null) .addParam(OAuth2Constants.ERROR, error); if (errorDescription != null) { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCRedirectUriBuilder.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCRedirectUriBuilder.java index 3b62fe13fc..2df5f18f96 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCRedirectUriBuilder.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCRedirectUriBuilder.java @@ -20,6 +20,11 @@ package org.keycloak.protocol.oidc.utils; import org.keycloak.common.util.Encode; import org.keycloak.common.util.HtmlUtils; import org.keycloak.common.util.KeycloakUriBuilder; +import org.keycloak.common.util.Time; +import org.keycloak.models.AuthenticatedClientSessionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.representations.AuthorizationResponseToken; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; @@ -43,13 +48,17 @@ public abstract class OIDCRedirectUriBuilder { public abstract Response build(); - public static OIDCRedirectUriBuilder fromUri(String baseUri, OIDCResponseMode responseMode) { + public static OIDCRedirectUriBuilder fromUri(String baseUri, OIDCResponseMode responseMode, KeycloakSession session, AuthenticatedClientSessionModel clientSession) { KeycloakUriBuilder uriBuilder = KeycloakUriBuilder.fromUri(baseUri); switch (responseMode) { case QUERY: return new QueryRedirectUriBuilder(uriBuilder); case FRAGMENT: return new FragmentRedirectUriBuilder(uriBuilder); case FORM_POST: return new FormPostRedirectUriBuilder(uriBuilder); + case QUERY_JWT: + case FRAGMENT_JWT: + case FORM_POST_JWT: + return new JWTRedirectUriBuilder(uriBuilder, responseMode, session, clientSession); } throw new IllegalStateException("Not possible to end here"); @@ -171,5 +180,88 @@ public abstract class OIDCRedirectUriBuilder { } + // https://openid.net/specs/openid-financial-api-jarm-ID1.html + private static class JWTRedirectUriBuilder extends OIDCRedirectUriBuilder { + private OIDCResponseMode responseMode; + private AuthorizationResponseToken responseJWT; + private KeycloakSession session; + private AuthenticatedClientSessionModel clientSession; + + public JWTRedirectUriBuilder(KeycloakUriBuilder uriBuilder, OIDCResponseMode responseMode, KeycloakSession session, AuthenticatedClientSessionModel clientSession) { + super(uriBuilder); + this.responseMode = responseMode; + this.session = session; + this.clientSession = clientSession; + responseJWT = new AuthorizationResponseToken(); + } + + @Override + public OIDCRedirectUriBuilder addParam(String paramName, String paramValue) { + responseJWT.getOtherClaims().put(paramName, paramValue); + return this; + } + + @Override + public Response build() { + if(clientSession != null) { + responseJWT.issuer(clientSession.getNote(OIDCLoginProtocol.ISSUER)); + responseJWT.audience(clientSession.getClient().getClientId()); + responseJWT.setOtherClaims("scope", clientSession.getNote(OIDCLoginProtocol.SCOPE_PARAM)); + responseJWT.exp((long) (Time.currentTime() + clientSession.getRealm().getAccessCodeLifespan())); + } + switch (responseMode) { + case QUERY_JWT: + return buildQueryResponse(); + case FRAGMENT_JWT: + return buildFragmentResponse(); + case FORM_POST_JWT: + return buildFormPostResponse(); + } + throw new IllegalStateException("Not possible to end here"); + } + + private Response buildQueryResponse() { + uriBuilder.queryParam("response", session.tokens().encodeAndEncrypt(responseJWT)); + URI redirectUri = uriBuilder.build(); + Response.ResponseBuilder location = Response.status(302).location(redirectUri); + return location.build(); + } + + private Response buildFragmentResponse() { + uriBuilder.encodedFragment("response=" + Encode.encodeQueryParamAsIs(session.tokens().encodeAndEncrypt(responseJWT))); + URI redirectUri = uriBuilder.build(); + Response.ResponseBuilder location = Response.status(302).location(redirectUri); + return location.build(); + } + + private Response buildFormPostResponse() { + StringBuilder builder = new StringBuilder(); + URI redirectUri = uriBuilder.build(); + + builder.append(""); + builder.append(" "); + builder.append(" OIDC Form_Post Response"); + builder.append(" "); + builder.append(" "); + + builder.append("
"); + + builder.append(" "); + + builder.append(" "); + builder.append("
"); + builder.append(" "); + builder.append(""); + + return Response.status(Response.Status.OK) + .type(MediaType.TEXT_HTML_TYPE) + .entity(builder.toString()).build(); + } + } } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCResponseMode.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCResponseMode.java index ca984f65bf..fb3624a733 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCResponseMode.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCResponseMode.java @@ -22,16 +22,42 @@ package org.keycloak.protocol.oidc.utils; */ public enum OIDCResponseMode { - QUERY, FRAGMENT, FORM_POST; + QUERY("query"), + FRAGMENT("fragment"), + FORM_POST("form_post"), + QUERY_JWT("query.jwt"), + FRAGMENT_JWT("fragment.jwt"), + FORM_POST_JWT("form_post.jwt"); + + private String value; + + OIDCResponseMode(String v) { + value = v; + } public static OIDCResponseMode parse(String responseMode, OIDCResponseType responseType) { if (responseMode == null) { return getDefaultResponseMode(responseType); + } else if(responseMode.equals("jwt")) { + return getDefaultJarmResponseMode(responseType); } else { - return Enum.valueOf(OIDCResponseMode.class, responseMode.toUpperCase()); + return fromValue(responseMode); } } + public String value() { + return value; + } + + private static OIDCResponseMode fromValue(String v) { + for (OIDCResponseMode c : OIDCResponseMode.values()) { + if (c.value.equals(v)) { + return c; + } + } + throw new IllegalArgumentException(v); + } + private static OIDCResponseMode getDefaultResponseMode(OIDCResponseType responseType) { if (responseType.isImplicitOrHybridFlow()) { return OIDCResponseMode.FRAGMENT; @@ -39,4 +65,12 @@ public enum OIDCResponseMode { return OIDCResponseMode.QUERY; } } + + private static OIDCResponseMode getDefaultJarmResponseMode(OIDCResponseType responseType) { + if (responseType.isImplicitOrHybridFlow()) { + return OIDCResponseMode.FRAGMENT_JWT; + } else { + return OIDCResponseMode.QUERY_JWT; + } + } } diff --git a/services/src/main/java/org/keycloak/services/ServicesLogger.java b/services/src/main/java/org/keycloak/services/ServicesLogger.java index 0cd06a5764..a67ba6efa4 100644 --- a/services/src/main/java/org/keycloak/services/ServicesLogger.java +++ b/services/src/main/java/org/keycloak/services/ServicesLogger.java @@ -459,4 +459,7 @@ public interface ServicesLogger extends BasicLogger { @Message(id=104, value="Not creating user %s. It already exists.") void notCreatingExistingUser(String userName); + @LogMessage(level = ERROR) + @Message(id=105, value="Response_mode 'query.jwt' is allowed only when the authorization response token is encrypted") + void responseModeQueryJwtNotAllowed(); } diff --git a/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java b/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java index 08f7c095be..a3cfc2f903 100755 --- a/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java +++ b/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java @@ -162,6 +162,10 @@ public class DescriptionConverter { configWrapper.setIdTokenEncryptedResponseEnc(clientOIDC.getIdTokenEncryptedResponseEnc()); } + configWrapper.setAuthorizationSignedResponseAlg(clientOIDC.getAuthorizationSignedResponseAlg()); + configWrapper.setAuthorizationEncryptedResponseAlg(clientOIDC.getAuthorizationEncryptedResponseAlg()); + configWrapper.setAuthorizationEncryptedResponseEnc(clientOIDC.getAuthorizationEncryptedResponseEnc()); + if (clientOIDC.getRequestUris() != null) { configWrapper.setRequestUris(clientOIDC.getRequestUris()); } @@ -330,6 +334,15 @@ public class DescriptionConverter { if (config.getIdTokenEncryptedResponseEnc() != null) { response.setIdTokenEncryptedResponseEnc(config.getIdTokenEncryptedResponseEnc()); } + if (config.getAuthorizationSignedResponseAlg() != null) { + response.setAuthorizationSignedResponseAlg(config.getAuthorizationSignedResponseAlg()); + } + if (config.getAuthorizationEncryptedResponseAlg() != null) { + response.setAuthorizationEncryptedResponseAlg(config.getAuthorizationEncryptedResponseAlg()); + } + if (config.getAuthorizationEncryptedResponseEnc() != null) { + response.setAuthorizationEncryptedResponseEnc(config.getAuthorizationEncryptedResponseEnc()); + } if (config.getRequestUris() != null) { response.setRequestUris(config.getRequestUris()); } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java index 1682914119..0a2a87bc0c 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java @@ -71,6 +71,7 @@ import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation; import org.keycloak.protocol.oidc.utils.OIDCResponseType; import org.keycloak.representations.AccessToken; +import org.keycloak.representations.AuthorizationResponseToken; import org.keycloak.representations.IDToken; import org.keycloak.representations.JsonWebToken; import org.keycloak.representations.RefreshToken; @@ -1053,6 +1054,10 @@ public class OAuthClient { return verifyToken(token, IDToken.class); } + public AuthorizationResponseToken verifyAuthorizationResponseToken(String token) { + return verifyToken(token, AuthorizationResponseToken.class); + } + public RefreshToken parseRefreshToken(String refreshToken) { try { return new JWSInput(refreshToken).readJsonContent(RefreshToken.class); @@ -1467,18 +1472,20 @@ public class OAuthClient { private String tokenType; private String expiresIn; + // Just during FAPI JARM response mode JWT + private String response; + public AuthorizationEndpointResponse(OAuthClient client) { boolean fragment; - try { - fragment = client.responseType != null && OIDCResponseType.parse(client.responseType).isImplicitOrHybridFlow(); - } catch (IllegalArgumentException iae) { - fragment = false; + if (client.responseMode == null || "jwt".equals(client.responseMode)) { + try { + fragment = client.responseType != null && OIDCResponseType.parse(client.responseType).isImplicitOrHybridFlow(); + } catch (IllegalArgumentException iae) { + fragment = false; + } + } else { + fragment = "fragment".equals(client.responseMode) || "fragment.jwt".equals(client.responseMode); } - - if ("fragment".equals(client.responseMode)) { - fragment = true; - } - init (client, fragment); } @@ -1499,6 +1506,7 @@ public class OAuthClient { idToken = params.get(OAuth2Constants.ID_TOKEN); tokenType = params.get(OAuth2Constants.TOKEN_TYPE); expiresIn = params.get(OAuth2Constants.EXPIRES_IN); + response = params.get(OAuth2Constants.RESPONSE); } public boolean isRedirected() { @@ -1540,6 +1548,10 @@ public class OAuthClient { public String getExpiresIn() { return expiresIn; } + + public String getResponse() { + return response; + } } public static class AuthenticationRequestAcknowledgement { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java index 405e1e5679..5bfcdc7d15 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java @@ -389,6 +389,80 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest { } } + @Test + public void testAuthorizationResponseSigningAlg() throws Exception { + OIDCClientRepresentation response = null; + OIDCClientRepresentation updated = null; + try { + OIDCClientRepresentation clientRep = createRep(); + clientRep.setAuthorizationSignedResponseAlg(Algorithm.PS256.toString()); + + response = reg.oidc().create(clientRep); + Assert.assertEquals(Algorithm.PS256.toString(), response.getAuthorizationSignedResponseAlg()); + + ClientRepresentation kcClient = getClient(response.getClientId()); + OIDCAdvancedConfigWrapper config = OIDCAdvancedConfigWrapper.fromClientRepresentation(kcClient); + Assert.assertEquals(Algorithm.PS256.toString(), config.getAuthorizationSignedResponseAlg()); + + reg.auth(Auth.token(response)); + response.setAuthorizationSignedResponseAlg(null); + updated = reg.oidc().update(response); + Assert.assertEquals(null, response.getAuthorizationSignedResponseAlg()); + + kcClient = getClient(updated.getClientId()); + config = OIDCAdvancedConfigWrapper.fromClientRepresentation(kcClient); + Assert.assertEquals(null, config.getAuthorizationSignedResponseAlg()); + } finally { + // revert + reg.auth(Auth.token(updated)); + updated.setAuthorizationSignedResponseAlg(null); + reg.oidc().update(updated); + } + } + + @Test + public void testAuthorizationEncryptedResponse() throws Exception { + OIDCClientRepresentation response = null; + OIDCClientRepresentation updated = null; + try { + OIDCClientRepresentation clientRep = createRep(); + clientRep.setAuthorizationEncryptedResponseAlg(JWEConstants.RSA1_5); + clientRep.setAuthorizationEncryptedResponseEnc(JWEConstants.A128CBC_HS256); + + // create + response = reg.oidc().create(clientRep); + Assert.assertEquals(JWEConstants.RSA1_5, response.getAuthorizationEncryptedResponseAlg()); + Assert.assertEquals(JWEConstants.A128CBC_HS256, response.getAuthorizationEncryptedResponseEnc()); + + // Test Keycloak representation + ClientRepresentation kcClient = getClient(response.getClientId()); + OIDCAdvancedConfigWrapper config = OIDCAdvancedConfigWrapper.fromClientRepresentation(kcClient); + Assert.assertEquals(JWEConstants.RSA1_5, config.getAuthorizationEncryptedResponseAlg()); + Assert.assertEquals(JWEConstants.A128CBC_HS256, config.getAuthorizationEncryptedResponseEnc()); + + // update + reg.auth(Auth.token(response)); + response.setAuthorizationEncryptedResponseAlg(null); + response.setAuthorizationEncryptedResponseEnc(null); + updated = reg.oidc().update(response); + Assert.assertNull(updated.getAuthorizationEncryptedResponseAlg()); + Assert.assertNull(updated.getAuthorizationEncryptedResponseEnc()); + + // Test Keycloak representation + kcClient = getClient(updated.getClientId()); + config = OIDCAdvancedConfigWrapper.fromClientRepresentation(kcClient); + Assert.assertNull(config.getAuthorizationEncryptedResponseAlg()); + Assert.assertNull(config.getAuthorizationEncryptedResponseEnc()); + + } finally { + // revert + reg.auth(Auth.token(updated)); + updated.setAuthorizationEncryptedResponseAlg(null); + updated.setAuthorizationEncryptedResponseEnc(null); + reg.oidc().update(updated); + } + } + @Test public void testCIBASettings() throws Exception { OIDCClientRepresentation clientRep = null; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java index ec2c0be42f..976140dab2 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java @@ -161,7 +161,7 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest { // KEYCLOAK-3281 @Test public void authorizationRequestFormPostResponseMode() throws IOException { - oauth.responseMode(OIDCResponseMode.FORM_POST.toString().toLowerCase()); + oauth.responseMode(OIDCResponseMode.FORM_POST.value()); oauth.stateParamHardcoded("OpenIdConnect.AuthenticationProperties=2302984sdlk"); oauth.doLoginGrant("test-user@localhost", "password"); @@ -179,7 +179,7 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest { @Test public void authorizationRequestFormPostResponseModeWithCustomState() throws IOException { - oauth.responseMode(OIDCResponseMode.FORM_POST.toString().toLowerCase()); + oauth.responseMode(OIDCResponseMode.FORM_POST.value()); oauth.stateParamHardcoded("\">bar_baz(2)far"); oauth.doLoginGrant("test-user@localhost", "password"); @@ -198,7 +198,7 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest { @Test public void authorizationRequestFragmentResponseModeNotKept() throws Exception { // Set response_mode=fragment and login - oauth.responseMode(OIDCResponseMode.FRAGMENT.toString().toLowerCase()); + oauth.responseMode(OIDCResponseMode.FRAGMENT.value()); OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password"); Assert.assertNotNull(response.getCode()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/AuthorizationTokenEncryptionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/AuthorizationTokenEncryptionTest.java new file mode 100644 index 0000000000..df366a756f --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/AuthorizationTokenEncryptionTest.java @@ -0,0 +1,293 @@ +/* + * 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; + +import org.jboss.arquillian.graphene.page.Page; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.common.util.PemUtils; +import org.keycloak.crypto.AesCbcHmacShaContentEncryptionProvider; +import org.keycloak.crypto.AesGcmContentEncryptionProvider; +import org.keycloak.crypto.Algorithm; +import org.keycloak.crypto.RsaCekManagementProvider; +import org.keycloak.jose.jwe.JWEConstants; +import org.keycloak.jose.jwe.JWEException; +import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider; +import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; +import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; +import org.keycloak.representations.AuthorizationResponseToken; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; +import org.keycloak.testsuite.Assert; +import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.arquillian.annotation.UncaughtServerErrorExpected; +import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls; +import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResource; +import org.keycloak.testsuite.pages.*; +import org.keycloak.testsuite.util.OAuthClient; +import org.keycloak.testsuite.util.TokenSignatureUtil; +import org.keycloak.util.TokenUtil; + +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.security.PrivateKey; +import java.util.Map; + +public class AuthorizationTokenEncryptionTest extends AbstractTestRealmKeycloakTest { + + @Rule + public AssertEvents events = new AssertEvents(this); + + @Page + protected AppPage appPage; + + @Page + protected LoginPage loginPage; + + @Page + protected AccountUpdateProfilePage profilePage; + + @Page + protected OAuthGrantPage grantPage; + + @Page + protected ErrorPage errorPage; + + @Override + public void configureTestRealm(RealmRepresentation testRealm) { + } + + @Test + public void testAuthorizationEncryptionAlgRSA1_5EncA128CBC_HS256() { + // add key provider explicitly though DefaultKeyManager create fallback key provider if not exist + TokenSignatureUtil.registerKeyProvider("P-256", adminClient, testContext); + testAuthorizationTokenSignatureAndEncryption(Algorithm.ES256, JWEConstants.RSA1_5, JWEConstants.A128CBC_HS256); + } + + @Test + public void testAuthorizationEncryptionAlgRSA1_5EncA192CBC_HS384() { + testAuthorizationTokenSignatureAndEncryption(Algorithm.PS256, JWEConstants.RSA1_5, JWEConstants.A192CBC_HS384); + } + + @Test + public void testAuthorizationEncryptionAlgRSA1_5EncA256CBC_HS512() { + testAuthorizationTokenSignatureAndEncryption(Algorithm.PS384, JWEConstants.RSA1_5, JWEConstants.A256CBC_HS512); + } + + @Test + public void testAuthorizationEncryptionAlgRSA1_5EncA128GCM() { + testAuthorizationTokenSignatureAndEncryption(Algorithm.RS384, JWEConstants.RSA1_5, JWEConstants.A128GCM); + } + + @Test + public void testAuthorizationEncryptionAlgRSA1_5EncA192GCM() { + testAuthorizationTokenSignatureAndEncryption(Algorithm.RS512, JWEConstants.RSA1_5, JWEConstants.A192GCM); + } + + @Test + public void testAuthorizationEncryptionAlgRSA1_5EncA256GCM() { + testAuthorizationTokenSignatureAndEncryption(Algorithm.RS256, JWEConstants.RSA1_5, JWEConstants.A256GCM); + } + + @Test + public void testAuthorizationEncryptionAlgRSA_OAEPEncA128CBC_HS256() { + // add key provider explicitly though DefaultKeyManager create fallback key provider if not exist + TokenSignatureUtil.registerKeyProvider("P-521", adminClient, testContext); + testAuthorizationTokenSignatureAndEncryption(Algorithm.ES512, JWEConstants.RSA_OAEP, JWEConstants.A128CBC_HS256); + } + + @Test + public void testAuthorizationEncryptionAlgRSA_OAEPEncA192CBC_HS384() { + testAuthorizationTokenSignatureAndEncryption(Algorithm.PS256, JWEConstants.RSA_OAEP, JWEConstants.A192CBC_HS384); + } + + @Test + public void testAuthorizationEncryptionAlgRSA_OAEPEncA256CBC_HS512() { + testAuthorizationTokenSignatureAndEncryption(Algorithm.PS512, JWEConstants.RSA_OAEP, JWEConstants.A256CBC_HS512); + } + + @Test + public void testAuthorizationEncryptionAlgRSA_OAEP256EncA128CBC_HS256() { + // add key provider explicitly though DefaultKeyManager create fallback key provider if not exist + TokenSignatureUtil.registerKeyProvider("P-521", adminClient, testContext); + testAuthorizationTokenSignatureAndEncryption(Algorithm.ES512, JWEConstants.RSA_OAEP_256, JWEConstants.A128CBC_HS256); + } + + @Test + public void testAuthorizationEncryptionAlgRSA_OAEP256EncA192CBC_HS384() { + testAuthorizationTokenSignatureAndEncryption(Algorithm.PS256, JWEConstants.RSA_OAEP_256, JWEConstants.A192CBC_HS384); + } + + @Test + public void testAuthorizationEncryptionAlgRSA_OAEP256EncA256CBC_HS512() { + testAuthorizationTokenSignatureAndEncryption(Algorithm.PS512, JWEConstants.RSA_OAEP_256, JWEConstants.A256CBC_HS512); + } + + @Test + public void testAuthorizationEncryptionAlgRSA_OAEPEncA128GCM() { + // add key provider explicitly though DefaultKeyManager create fallback key provider if not exist + TokenSignatureUtil.registerKeyProvider("P-256", adminClient, testContext); + testAuthorizationTokenSignatureAndEncryption(Algorithm.ES256, JWEConstants.RSA_OAEP, JWEConstants.A128GCM); + } + + @Test + public void testAuthorizationEncryptionAlgRSA_OAEPEncA192GCM() { + testAuthorizationTokenSignatureAndEncryption(Algorithm.PS384, JWEConstants.RSA_OAEP, JWEConstants.A192GCM); + } + + @Test + public void testAuthorizationEncryptionAlgRSA_OAEPEncA256GCM() { + testAuthorizationTokenSignatureAndEncryption(Algorithm.PS512, JWEConstants.RSA_OAEP, JWEConstants.A256GCM); + } + + private void testAuthorizationTokenSignatureAndEncryption(String sigAlgorithm, String algAlgorithm, String encAlgorithm) { + ClientResource clientResource; + ClientRepresentation clientRep; + try { + // generate and register encryption key onto client + TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); + oidcClientEndpointsResource.generateKeys(algAlgorithm); + + clientResource = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"); + clientRep = clientResource.toRepresentation(); + // set authorization response signature algorithm and encryption algorithms + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setAuthorizationSignedResponseAlg(sigAlgorithm); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setAuthorizationEncryptedResponseAlg(algAlgorithm); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setAuthorizationEncryptedResponseEnc(encAlgorithm); + // use and set jwks_url + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setUseJwksUrl(true); + String jwksUrl = TestApplicationResourceUrls.clientJwksUri(); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setJwksUrl(jwksUrl); + clientResource.update(clientRep); + + // get authorization response + oauth.responseMode("jwt"); + oauth.stateParamHardcoded("OpenIdConnect.AuthenticationProperties=2302984sdlk"); + OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password"); + + // parse JWE and JOSE Header + String jweStr = response.getResponse(); + String[] parts = jweStr.split("\\."); + Assert.assertEquals(parts.length, 5); + + // get decryption key + // not publickey , use privateKey + Map keyPair = oidcClientEndpointsResource.getKeysAsPem(); + PrivateKey decryptionKEK = PemUtils.decodePrivateKey(keyPair.get("privateKey")); + + // verify and decrypt JWE + JWEAlgorithmProvider algorithmProvider = getJweAlgorithmProvider(algAlgorithm); + JWEEncryptionProvider encryptionProvider = getJweEncryptionProvider(encAlgorithm); + byte[] decodedString = TokenUtil.jweKeyEncryptionVerifyAndDecode(decryptionKEK, jweStr, algorithmProvider, encryptionProvider); + String authorizationTokenString = new String(decodedString, "UTF-8"); + + // verify JWS + AuthorizationResponseToken authorizationToken = oauth.verifyAuthorizationResponseToken(authorizationTokenString); + Assert.assertEquals("test-app", authorizationToken.getAudience()[0]); + Assert.assertEquals("OpenIdConnect.AuthenticationProperties=2302984sdlk", authorizationToken.getOtherClaims().get("state")); + Assert.assertNotNull(authorizationToken.getOtherClaims().get("code")); + } catch (JWEException | UnsupportedEncodingException e) { + Assert.fail(); + } finally { + clientResource = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"); + clientRep = clientResource.toRepresentation(); + // revert id token signature algorithm and encryption algorithms + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setAuthorizationSignedResponseAlg(Algorithm.RS256); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setAuthorizationEncryptedResponseAlg(null); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setAuthorizationEncryptedResponseEnc(null); + // revert jwks_url settings + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setUseJwksUrl(false); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setJwksUrl(null); + clientResource.update(clientRep); + } + } + + private JWEAlgorithmProvider getJweAlgorithmProvider(String algAlgorithm) { + JWEAlgorithmProvider jweAlgorithmProvider = null; + if (JWEConstants.RSA1_5.equals(algAlgorithm) || JWEConstants.RSA_OAEP.equals(algAlgorithm) || + JWEConstants.RSA_OAEP_256.equals(algAlgorithm)) { + jweAlgorithmProvider = new RsaCekManagementProvider(null, algAlgorithm).jweAlgorithmProvider(); + } + return jweAlgorithmProvider; + } + private JWEEncryptionProvider getJweEncryptionProvider(String encAlgorithm) { + JWEEncryptionProvider jweEncryptionProvider = null; + switch(encAlgorithm) { + case JWEConstants.A128GCM: + case JWEConstants.A192GCM: + case JWEConstants.A256GCM: + jweEncryptionProvider = new AesGcmContentEncryptionProvider(null, encAlgorithm).jweEncryptionProvider(); + break; + case JWEConstants.A128CBC_HS256: + case JWEConstants.A192CBC_HS384: + case JWEConstants.A256CBC_HS512: + jweEncryptionProvider = new AesCbcHmacShaContentEncryptionProvider(null, encAlgorithm).jweEncryptionProvider(); + break; + } + return jweEncryptionProvider; + } + + @Test + @UncaughtServerErrorExpected + public void testAuthorizationEncryptionWithoutEncryptionKEK() throws MalformedURLException, URISyntaxException { + ClientResource clientResource = null; + ClientRepresentation clientRep = null; + try { + // generate and register signing/verifying key onto client, not encryption key + TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); + oidcClientEndpointsResource.generateKeys(Algorithm.RS256); + + clientResource = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"); + clientRep = clientResource.toRepresentation(); + // set id token signature algorithm and encryption algorithms + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setAuthorizationSignedResponseAlg(Algorithm.RS256); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setAuthorizationEncryptedResponseAlg(JWEConstants.RSA1_5); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setAuthorizationEncryptedResponseEnc(JWEConstants.A128CBC_HS256); + // use and set jwks_url + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setUseJwksUrl(true); + String jwksUrl = TestApplicationResourceUrls.clientJwksUri(); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setJwksUrl(jwksUrl); + clientResource.update(clientRep); + + // get authorization response but failed + oauth.responseMode("jwt"); + oauth.stateParamHardcoded("OpenIdConnect.AuthenticationProperties=2302984sdlk"); + + OAuthClient.AuthorizationEndpointResponse errorResponse = oauth.doLogin("test-user@localhost", "password"); + + System.out.println(driver.getPageSource().contains("Unexpected error when handling authentication request to identity provider.")); + + } finally { + // Revert + clientResource = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"); + clientRep = clientResource.toRepresentation(); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setAuthorizationSignedResponseAlg(Algorithm.RS256); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setAuthorizationEncryptedResponseAlg(null); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setAuthorizationEncryptedResponseEnc(null); + // Revert jwks_url settings + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setUseJwksUrl(false); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setJwksUrl(null); + clientResource.update(clientRep); + } + } + +} + diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/AuthorizationTokenResponseModeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/AuthorizationTokenResponseModeTest.java new file mode 100644 index 0000000000..eb8619621e --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/AuthorizationTokenResponseModeTest.java @@ -0,0 +1,218 @@ +/* + * 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; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.OAuthErrorException; +import org.keycloak.events.Details; +import org.keycloak.events.Errors; +import org.keycloak.protocol.oidc.utils.OIDCResponseMode; +import org.keycloak.representations.AccessToken; +import org.keycloak.representations.AuthorizationResponseToken; +import org.keycloak.representations.IDToken; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; +import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.util.ClientManager; +import org.keycloak.testsuite.util.OAuthClient; +import org.openqa.selenium.By; + +import javax.ws.rs.core.UriBuilder; +import java.io.IOException; +import java.net.URI; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class AuthorizationTokenResponseModeTest extends AbstractTestRealmKeycloakTest { + + @Rule + public AssertEvents events = new AssertEvents(this); + + @Test + public void authorizationRequestQueryJWTResponseMode() throws Exception { + oauth.responseMode(OIDCResponseMode.QUERY_JWT.value()); + oauth.stateParamHardcoded("OpenIdConnect.AuthenticationProperties=2302984sdlk"); + + OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password"); + + assertTrue(response.isRedirected()); + AuthorizationResponseToken responseToken = oauth.verifyAuthorizationResponseToken(response.getResponse()); + + assertEquals("test-app", responseToken.getAudience()[0]); + Assert.assertNotNull(responseToken.getOtherClaims().get("code")); + assertEquals("OpenIdConnect.AuthenticationProperties=2302984sdlk", responseToken.getOtherClaims().get("state")); + Assert.assertNull(responseToken.getOtherClaims().get("error")); + + String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID); + } + + @Test + public void authorizationRequestJWTResponseMode() throws Exception { + // jwt response_mode. It should fallback to query.jwt + oauth.responseMode("jwt"); + oauth.stateParamHardcoded("OpenIdConnect.AuthenticationProperties=2302984sdlk"); + + OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password"); + + assertTrue(response.isRedirected()); + AuthorizationResponseToken responseToken = oauth.verifyAuthorizationResponseToken(response.getResponse()); + + assertEquals("test-app", responseToken.getAudience()[0]); + Assert.assertNotNull(responseToken.getOtherClaims().get("code")); + assertEquals("OpenIdConnect.AuthenticationProperties=2302984sdlk", responseToken.getOtherClaims().get("state")); + Assert.assertNull(responseToken.getOtherClaims().get("error")); + + URI currentUri = new URI(driver.getCurrentUrl()); + Assert.assertNotNull(currentUri.getRawQuery()); + Assert.assertNull(currentUri.getRawFragment()); + + String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID); + } + + @Test + public void authorizationRequestFragmentJWTResponseMode() throws Exception { + oauth.responseMode(OIDCResponseMode.FRAGMENT_JWT.value()); + oauth.stateParamHardcoded("OpenIdConnect.AuthenticationProperties=2302984sdlk"); + + OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password"); + + assertTrue(response.isRedirected()); + AuthorizationResponseToken responseToken = oauth.verifyAuthorizationResponseToken(response.getResponse()); + + assertEquals("test-app", responseToken.getAudience()[0]); + Assert.assertNotNull(responseToken.getOtherClaims().get("code")); + assertEquals("OpenIdConnect.AuthenticationProperties=2302984sdlk", responseToken.getOtherClaims().get("state")); + Assert.assertNull(responseToken.getOtherClaims().get("error")); + + URI currentUri = new URI(driver.getCurrentUrl()); + Assert.assertNull(currentUri.getRawQuery()); + Assert.assertNotNull(currentUri.getRawFragment()); + + String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID); + } + + @Test + public void authorizationRequestFormPostJWTResponseMode() throws IOException { + oauth.responseMode(OIDCResponseMode.FORM_POST_JWT.value()); + oauth.stateParamHardcoded("OpenIdConnect.AuthenticationProperties=2302984sdlk"); + oauth.doLoginGrant("test-user@localhost", "password"); + + String sources = driver.getPageSource(); + System.out.println(sources); + + String responseTokenEncoded = driver.findElement(By.id("response")).getText(); + + AuthorizationResponseToken responseToken = oauth.verifyAuthorizationResponseToken(responseTokenEncoded); + + assertEquals("test-app", responseToken.getAudience()[0]); + Assert.assertNotNull(responseToken.getOtherClaims().get("code")); + assertEquals("OpenIdConnect.AuthenticationProperties=2302984sdlk", responseToken.getOtherClaims().get("state")); + Assert.assertNull(responseToken.getOtherClaims().get("error")); + + String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID); + } + + @Test + public void authorizationRequestJWTResponseModeIdTokenResponseType() throws Exception { + ClientManager.realm(adminClient.realm("test")).clientId("test-app").implicitFlow(true); + // jwt response_mode. It should fallback to fragment.jwt when its hybrid flow + oauth.responseMode("jwt"); + oauth.responseType("code id_token"); + oauth.stateParamHardcoded("OpenIdConnect.AuthenticationProperties=2302984sdlk"); + oauth.nonce("123456"); + + OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password"); + + assertTrue(response.isRedirected()); + AuthorizationResponseToken responseToken = oauth.verifyAuthorizationResponseToken(response.getResponse()); + + assertEquals("test-app", responseToken.getAudience()[0]); + Assert.assertNotNull(responseToken.getOtherClaims().get("code")); + assertEquals("OpenIdConnect.AuthenticationProperties=2302984sdlk", responseToken.getOtherClaims().get("state")); + Assert.assertNull(responseToken.getOtherClaims().get("error")); + + Assert.assertNotNull(responseToken.getOtherClaims().get("id_token")); + String idTokenEncoded = (String) responseToken.getOtherClaims().get("id_token"); + IDToken idToken = oauth.verifyIDToken(idTokenEncoded); + assertEquals("123456", idToken.getNonce()); + + URI currentUri = new URI(driver.getCurrentUrl()); + Assert.assertNull(currentUri.getRawQuery()); + Assert.assertNotNull(currentUri.getRawFragment()); + + String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID); + } + + @Test + public void authorizationRequestJWTResponseModeAccessTokenResponseType() throws Exception { + ClientManager.realm(adminClient.realm("test")).clientId("test-app").implicitFlow(true); + // jwt response_mode. It should fallback to fragment.jwt when its hybrid flow + oauth.responseMode("jwt"); + oauth.responseType("token id_token"); + oauth.stateParamHardcoded("OpenIdConnect.AuthenticationProperties=2302984sdlk"); + oauth.nonce("123456"); + + OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password"); + + assertTrue(response.isRedirected()); + AuthorizationResponseToken responseToken = oauth.verifyAuthorizationResponseToken(response.getResponse()); + + assertEquals("test-app", responseToken.getAudience()[0]); + Assert.assertNull(responseToken.getOtherClaims().get("code")); + assertEquals("OpenIdConnect.AuthenticationProperties=2302984sdlk", responseToken.getOtherClaims().get("state")); + Assert.assertNull(responseToken.getOtherClaims().get("error")); + + Assert.assertNotNull(responseToken.getOtherClaims().get("id_token")); + String idTokenEncoded = (String) responseToken.getOtherClaims().get("id_token"); + IDToken idToken = oauth.verifyIDToken(idTokenEncoded); + assertEquals("123456", idToken.getNonce()); + + Assert.assertNotNull(responseToken.getOtherClaims().get("access_token")); + String accessTokenEncoded = (String) responseToken.getOtherClaims().get("access_token"); + AccessToken accessToken = oauth.verifyToken(accessTokenEncoded); + assertEquals("123456", accessToken.getNonce()); + + URI currentUri = new URI(driver.getCurrentUrl()); + Assert.assertNull(currentUri.getRawQuery()); + Assert.assertNotNull(currentUri.getRawFragment()); + } + + @Test + public void authorizationRequestFailInvalidResponseModeQueryJWT() throws Exception { + ClientManager.realm(adminClient.realm("test")).clientId("test-app").implicitFlow(true); + oauth.responseMode("query.jwt"); + oauth.responseType("code id_token"); + oauth.stateParamHardcoded("OpenIdConnect.AuthenticationProperties=2302984sdlk"); + oauth.nonce("123456"); + UriBuilder b = UriBuilder.fromUri(oauth.getLoginFormUrl()); + driver.navigate().to(b.build().toURL()); + + OAuthClient.AuthorizationEndpointResponse errorResponse = new OAuthClient.AuthorizationEndpointResponse(oauth); + AuthorizationResponseToken responseToken = oauth.verifyAuthorizationResponseToken(errorResponse.getResponse()); + Assert.assertEquals(OAuthErrorException.INVALID_REQUEST, responseToken.getOtherClaims().get("error")); + Assert.assertEquals("Response_mode 'query.jwt' is allowed only when the authorization response token is encrypted", responseToken.getOtherClaims().get("error_description")); + + events.expectLogin().error(Errors.INVALID_REQUEST).user((String) null).session((String) null).clearDetails().assertEvent(); + } + + @Override + public void configureTestRealm(RealmRepresentation testRealm) { + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java index 94a24014e5..5b7b595995 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java @@ -124,7 +124,7 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest { assertContains(oidcConfig.getResponseTypesSupported(), OAuth2Constants.CODE, OIDCResponseType.ID_TOKEN, "id_token token", "code id_token", "code token", "code id_token token"); assertContains(oidcConfig.getGrantTypesSupported(), OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.IMPLICIT, OAuth2Constants.DEVICE_CODE_GRANT_TYPE); - assertContains(oidcConfig.getResponseModesSupported(), "query", "fragment"); + assertContains(oidcConfig.getResponseModesSupported(), "query", "fragment", "form_post", "jwt", "query.jwt", "fragment.jwt", "form_post.jwt"); Assert.assertNames(oidcConfig.getSubjectTypesSupported(), "pairwise", "public"); @@ -132,10 +132,13 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest { Assert.assertNames(oidcConfig.getIdTokenSigningAlgValuesSupported(), Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512); Assert.assertNames(oidcConfig.getUserInfoSigningAlgValuesSupported(), "none", Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512); Assert.assertNames(oidcConfig.getRequestObjectSigningAlgValuesSupported(), "none", Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512); + Assert.assertNames(oidcConfig.getAuthorizationSigningAlgValuesSupported(), Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512); // Encryption algorithms Assert.assertNames(oidcConfig.getIdTokenEncryptionAlgValuesSupported(), JWEConstants.RSA1_5, JWEConstants.RSA_OAEP, JWEConstants.RSA_OAEP_256); Assert.assertNames(oidcConfig.getIdTokenEncryptionEncValuesSupported(), JWEConstants.A128CBC_HS256, JWEConstants.A128GCM, JWEConstants.A192CBC_HS384, JWEConstants.A192GCM, JWEConstants.A256CBC_HS512, JWEConstants.A256GCM); + Assert.assertNames(oidcConfig.getAuthorizationEncryptionAlgValuesSupported(), JWEConstants.RSA1_5, JWEConstants.RSA_OAEP, JWEConstants.RSA_OAEP_256); + Assert.assertNames(oidcConfig.getAuthorizationEncryptionEncValuesSupported(), JWEConstants.A128CBC_HS256, JWEConstants.A128GCM, JWEConstants.A192CBC_HS384, JWEConstants.A192GCM, JWEConstants.A256CBC_HS512, JWEConstants.A256GCM); // Client authentication Assert.assertNames(oidcConfig.getTokenEndpointAuthMethodsSupported(), "client_secret_basic", "client_secret_post", "private_key_jwt", "client_secret_jwt", "tls_client_auth"); 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 ccc94bf721..81a90ea30a 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 @@ -433,6 +433,12 @@ use-refresh-tokens=Use Refresh Tokens use-refresh-tokens.tooltip=If this is on, a refresh_token will be created and added to the token response. If this is off then no refresh_token will be generated. use-refresh-token-for-client-credentials-grant=Use Refresh Tokens For Client Credentials Grant use-refresh-token-for-client-credentials-grant.tooltip=If this is on, a refresh_token will be created and added to the token response if the client_credentials grant is used. The OAuth 2.0 RFC6749 Section 4.4.3 states that a refresh_token should not be generated when client_credentials grant is used. If this is off then no refresh_token will be generated and the associated user session will be removed. +authorization-signed-response-alg=Authorization Response Signature Algorithm +authorization-signed-response-alg.tooltip=JWA algorithm used for signing authorization response tokens when the response mode is jwt. +authorization-encrypted-response-alg=Authorization Response Encryption Key Management Algorithm +authorization-encrypted-response-alg.tooltip=JWA Algorithm used for key management in encrypting the authorization response when the response mode is jwt. This option is needed if you want encrypted authorization response. If left empty, the authorization response is just signed, but not encrypted. +authorization-encrypted-response-enc=Authorization Response Encryption Content Encryption Algorithm +authorization-encrypted-response-enc.tooltip=JWA Algorithm used for content encryption in encrypting the authorization response when the response mode is jwt. This option is needed if you want encrypted authorization response. If left empty, the authorization response is just signed, but not encrypted. # client import import-client=Import Client 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 ff10275de5..ba6176806c 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 @@ -1292,6 +1292,9 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro $scope.idTokenSignedResponseAlg = $scope.client.attributes['id.token.signed.response.alg']; $scope.idTokenEncryptedResponseAlg = $scope.client.attributes['id.token.encrypted.response.alg']; $scope.idTokenEncryptedResponseEnc = $scope.client.attributes['id.token.encrypted.response.enc']; + $scope.authorizationSignedResponseAlg = $scope.client.attributes['authorization.signed.response.alg']; + $scope.authorizationEncryptedResponseAlg = $scope.client.attributes['authorization.encrypted.response.alg']; + $scope.authorizationEncryptedResponseEnc = $scope.client.attributes['authorization.encrypted.response.enc']; var attrVal1 = $scope.client.attributes['user.info.response.signature.alg']; $scope.userInfoSignedResponseAlg = attrVal1==null ? 'unsigned' : attrVal1; @@ -1524,6 +1527,18 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro } }; + $scope.changeAuthorizationSignedResponseAlg = function() { + $scope.clientEdit.attributes['authorization.signed.response.alg'] = $scope.authorizationSignedResponseAlg; + }; + + $scope.changeAuthorizationEncryptedResponseAlg = function() { + $scope.clientEdit.attributes['authorization.encrypted.response.alg'] = $scope.authorizationEncryptedResponseAlg; + }; + + $scope.changeAuthorizationEncryptedResponseEnc = function() { + $scope.clientEdit.attributes['authorization.encrypted.response.enc'] = $scope.authorizationEncryptedResponseEnc; + }; + $scope.$watch(function() { return $location.path(); }, function() { 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 1984e16df0..0d14a47c4a 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 @@ -605,6 +605,48 @@ {{:: 'request-uris.tooltip' | translate}} +
+ +
+
+ +
+
+ {{:: 'authorization-signed-response-alg.tooltip' | translate}} +
+
+ +
+
+ +
+
+ {{:: 'authorization-encrypted-response-alg.tooltip' | translate}} +
+
+ +
+
+ +
+
+ {{:: 'authorization-encrypted-response-enc.tooltip' | translate}} +