KEYCLOAK-18452 FAPI JARM: JWT Secured Authorization Response Mode for OAuth 2.0

This commit is contained in:
lbortoli 2021-06-06 19:34:08 -03:00 committed by Marek Posolda
parent 04ff2c327b
commit e5ae113453
24 changed files with 957 additions and 27 deletions

View file

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

View file

@ -22,5 +22,6 @@ public enum TokenCategory {
ID,
ADMIN,
USERINFO,
LOGOUT
LOGOUT,
AUTHORIZATION_RESPONSE
}

View file

@ -97,6 +97,15 @@ public class OIDCConfigurationRepresentation {
@JsonProperty("introspection_endpoint_auth_signing_alg_values_supported")
private List<String> introspectionEndpointAuthSigningAlgValuesSupported;
@JsonProperty("authorization_signing_alg_values_supported")
private List<String> authorizationSigningAlgValuesSupported;
@JsonProperty("authorization_encryption_alg_values_supported")
private List<String> authorizationEncryptionAlgValuesSupported;
@JsonProperty("authorization_encryption_enc_values_supported")
private List<String> authorizationEncryptionEncValuesSupported;
@JsonProperty("claims_supported")
private List<String> claimsSupported;
@ -489,4 +498,28 @@ public class OIDCConfigurationRepresentation {
public String getDeviceAuthorizationEndpoint() {
return deviceAuthorizationEndpoint;
}
public List<String> getAuthorizationSigningAlgValuesSupported() {
return authorizationSigningAlgValuesSupported;
}
public void setAuthorizationSigningAlgValuesSupported(List<String> authorizationSigningAlgValuesSupported) {
this.authorizationSigningAlgValuesSupported = authorizationSigningAlgValuesSupported;
}
public List<String> getAuthorizationEncryptionAlgValuesSupported() {
return authorizationEncryptionAlgValuesSupported;
}
public void setAuthorizationEncryptionAlgValuesSupported(List<String> authorizationEncryptionAlgValuesSupported) {
this.authorizationEncryptionAlgValuesSupported = authorizationEncryptionAlgValuesSupported;
}
public List<String> getAuthorizationEncryptionEncValuesSupported() {
return authorizationEncryptionEncValuesSupported;
}
public void setAuthorizationEncryptionEncValuesSupported(List<String> authorizationEncryptionEncValuesSupported) {
this.authorizationEncryptionEncValuesSupported = authorizationEncryptionEncValuesSupported;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -74,7 +74,7 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
public static final List<String> DEFAULT_SUBJECT_TYPES_SUPPORTED = list("public", "pairwise");
public static final List<String> DEFAULT_RESPONSE_MODES_SUPPORTED = list("query", "fragment", "form_post");
public static final List<String> DEFAULT_RESPONSE_MODES_SUPPORTED = list("query", "fragment", "form_post", "query.jwt", "fragment.jwt", "form_post.jwt", "jwt");
public static final List<String> 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<String> getSupportedIdTokenEncryptionAlg(boolean includeNone) {
private List<String> getSupportedEncryptionAlg(boolean includeNone) {
return getSupportedAlgorithms(CekManagementProvider.class, includeNone);
}
private List<String> getSupportedIdTokenEncryptionEnc(boolean includeNone) {
private List<String> getSupportedEncryptionEnc(boolean includeNone) {
return getSupportedAlgorithms(ContentEncryptionProvider.class, includeNone);
}
}

View file

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

View file

@ -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("<HTML>");
builder.append(" <HEAD>");
builder.append(" <TITLE>OIDC Form_Post Response</TITLE>");
builder.append(" </HEAD>");
builder.append(" <BODY Onload=\"document.forms[0].submit()\">");
builder.append(" <FORM METHOD=\"POST\" ACTION=\"" + redirectUri.toString() + "\">");
builder.append(" <INPUT TYPE=\"HIDDEN\" NAME=\"response\" VALUE=\"")
.append(HtmlUtils.escapeAttribute(session.tokens().encodeAndEncrypt(responseJWT)))
.append("\" />");
builder.append(" <NOSCRIPT>");
builder.append(" <P>JavaScript is disabled. We strongly recommend to enable it. Click the button below to continue .</P>");
builder.append(" <INPUT name=\"continue\" TYPE=\"SUBMIT\" VALUE=\"CONTINUE\" />");
builder.append(" </NOSCRIPT>");
builder.append(" </FORM>");
builder.append(" </BODY>");
builder.append("</HTML>");
return Response.status(Response.Status.OK)
.type(MediaType.TEXT_HTML_TYPE)
.entity(builder.toString()).build();
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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("\"><foo>bar_baz(2)far</foo>");
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());

View file

@ -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<String, String> 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);
}
}
}

View file

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

View file

@ -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");

View file

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

View file

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

View file

@ -605,6 +605,48 @@
</div>
<kc-tooltip>{{:: 'request-uris.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="authorizationSignedResponseAlg">{{:: 'authorization-signed-response-alg' | translate}}</label>
<div class="col-sm-6">
<div>
<select class="form-control" id="authorizationSignedResponseAlg"
ng-change="changeAuthorizationSignedResponseAlg()"
ng-model="authorizationSignedResponseAlg">
<option value=""></option>
<option ng-repeat="provider in serverInfo.listProviderIds('signature')" value="{{provider}}">{{provider}}</option>
</select>
</div>
</div>
<kc-tooltip>{{:: 'authorization-signed-response-alg.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix block">
<label class="col-md-2 control-label" for="authorizationEncryptedResponseAlg">{{:: 'authorization-encrypted-response-alg' | translate}}</label>
<div class="col-sm-6">
<div>
<select class="form-control" id="authorizationEncryptedResponseAlg"
ng-change="changeAuthorizationEncryptedResponseAlg()"
ng-model="authorizationEncryptedResponseAlg">
<option value=""></option>
<option ng-repeat="provider in serverInfo.listProviderIds('cekmanagement')" value="{{provider}}">{{provider}}</option>
</select>
</div>
</div>
<kc-tooltip>{{:: 'authorization-encrypted-response-alg.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix block">
<label class="col-md-2 control-label" for="authorizationEncryptedResponseEnc">{{:: 'authorization-encrypted-response-enc' | translate}}</label>
<div class="col-sm-6">
<div>
<select class="form-control" id="authorizationEncryptedResponseEnc"
ng-change="changeAuthorizationEncryptedResponseEnc()"
ng-model="authorizationEncryptedResponseEnc">
<option value=""></option>
<option ng-repeat="provider in serverInfo.listProviderIds('contentencryption')" value="{{provider}}">{{provider}}</option>
</select>
</div>
</div>
<kc-tooltip>{{:: 'authorization-encrypted-response-enc.tooltip' | translate}}</kc-tooltip>
</div>
</fieldset>
<fieldset data-ng-show="protocol == 'openid-connect'">