[KEYCLOAK-10949] - Proper error messages when failing to authenticate the request
This commit is contained in:
parent
967d21dbb5
commit
8b203d48ce
2 changed files with 98 additions and 22 deletions
|
@ -38,6 +38,7 @@ import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import javax.ws.rs.core.Response.Status;
|
import javax.ws.rs.core.Response.Status;
|
||||||
|
|
||||||
|
import org.apache.http.HttpStatus;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.jboss.resteasy.spi.HttpRequest;
|
import org.jboss.resteasy.spi.HttpRequest;
|
||||||
import org.keycloak.OAuthErrorException;
|
import org.keycloak.OAuthErrorException;
|
||||||
|
@ -99,39 +100,64 @@ public class AuthorizationTokenService {
|
||||||
private static final String RESPONSE_MODE_DECISION = "decision";
|
private static final String RESPONSE_MODE_DECISION = "decision";
|
||||||
private static final String RESPONSE_MODE_PERMISSIONS = "permissions";
|
private static final String RESPONSE_MODE_PERMISSIONS = "permissions";
|
||||||
private static final String RESPONSE_MODE_DECISION_RESULT = "result";
|
private static final String RESPONSE_MODE_DECISION_RESULT = "result";
|
||||||
private static Map<String, BiFunction<AuthorizationRequest, AuthorizationProvider, EvaluationContext>> SUPPORTED_CLAIM_TOKEN_FORMATS;
|
private static Map<String, BiFunction<KeycloakAuthorizationRequest, AuthorizationProvider, EvaluationContext>> SUPPORTED_CLAIM_TOKEN_FORMATS;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
SUPPORTED_CLAIM_TOKEN_FORMATS = new HashMap<>();
|
SUPPORTED_CLAIM_TOKEN_FORMATS = new HashMap<>();
|
||||||
SUPPORTED_CLAIM_TOKEN_FORMATS.put("urn:ietf:params:oauth:token-type:jwt", (authorizationRequest, authorization) -> {
|
SUPPORTED_CLAIM_TOKEN_FORMATS.put("urn:ietf:params:oauth:token-type:jwt", (request, authorization) -> {
|
||||||
String claimToken = authorizationRequest.getClaimToken();
|
String claimToken = request.getClaimToken();
|
||||||
|
|
||||||
if (claimToken != null) {
|
if (claimToken != null) {
|
||||||
|
Map claims;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Map claims = JsonSerialization.readValue(Base64Url.decode(authorizationRequest.getClaimToken()), Map.class);
|
claims = JsonSerialization.readValue(Base64Url.decode(request.getClaimToken()), Map.class);
|
||||||
authorizationRequest.setClaims(claims);
|
request.setClaims(claims);
|
||||||
return new DefaultEvaluationContext(new KeycloakIdentity(authorization.getKeycloakSession(), Tokens.getAccessToken(authorizationRequest.getSubjectToken(), authorization.getKeycloakSession())), claims, authorization.getKeycloakSession());
|
} catch (Exception cause) {
|
||||||
} catch (IOException cause) {
|
throw new CorsErrorResponseException(request.getCors(), "invalid_request", "Invalid claims",
|
||||||
throw new RuntimeException("Failed to map claims from claim token [" + claimToken + "]", cause);
|
Status.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
KeycloakIdentity identity;
|
||||||
|
|
||||||
|
try {
|
||||||
|
identity = new KeycloakIdentity(authorization.getKeycloakSession(),
|
||||||
|
Tokens.getAccessToken(request.getSubjectToken(), authorization.getKeycloakSession()));
|
||||||
|
} catch (Exception cause) {
|
||||||
|
throw new CorsErrorResponseException(request.getCors(), "unauthorized_client", "Invalid identity", Status.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DefaultEvaluationContext(identity, claims, authorization.getKeycloakSession());
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new RuntimeException("Claim token can not be null");
|
throw new CorsErrorResponseException(request.getCors(), "invalid_request", "Claim token can not be null", Status.BAD_REQUEST);
|
||||||
});
|
});
|
||||||
SUPPORTED_CLAIM_TOKEN_FORMATS.put(CLAIM_TOKEN_FORMAT_ID_TOKEN, (authorizationRequest, authorization) -> {
|
SUPPORTED_CLAIM_TOKEN_FORMATS.put(CLAIM_TOKEN_FORMAT_ID_TOKEN, (request, authorization) -> {
|
||||||
try {
|
KeycloakSession keycloakSession = authorization.getKeycloakSession();
|
||||||
KeycloakSession keycloakSession = authorization.getKeycloakSession();
|
String subjectToken = request.getSubjectToken();
|
||||||
String accessToken = authorizationRequest.getSubjectToken();
|
|
||||||
|
|
||||||
if (accessToken == null) {
|
if (subjectToken == null) {
|
||||||
throw new RuntimeException("Claim token can not be null and must be a valid IDToken");
|
throw new CorsErrorResponseException(request.getCors(), "invalid_request", "Subject token can not be null and must be a valid ID or Access Token",
|
||||||
}
|
Status.BAD_REQUEST);
|
||||||
|
|
||||||
IDToken idToken = new TokenManager().verifyIDTokenSignature(keycloakSession, accessToken);
|
|
||||||
return new DefaultEvaluationContext(new KeycloakIdentity(keycloakSession, idToken), authorizationRequest.getClaims(), keycloakSession);
|
|
||||||
} catch (OAuthErrorException cause) {
|
|
||||||
throw new RuntimeException("Failed to verify ID token", cause);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IDToken idToken;
|
||||||
|
|
||||||
|
try {
|
||||||
|
idToken = new TokenManager().verifyIDTokenSignature(keycloakSession, subjectToken);
|
||||||
|
} catch (Exception cause) {
|
||||||
|
throw new CorsErrorResponseException(request.getCors(), "unauthorized_client", "Invalid signature", Status.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
KeycloakIdentity identity;
|
||||||
|
|
||||||
|
try {
|
||||||
|
identity = new KeycloakIdentity(keycloakSession, idToken);
|
||||||
|
} catch (Exception cause) {
|
||||||
|
throw new CorsErrorResponseException(request.getCors(), "unauthorized_client", "Invalid identity", Status.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DefaultEvaluationContext(identity, request.getClaims(), keycloakSession);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -370,7 +396,7 @@ public class AuthorizationTokenService {
|
||||||
claimTokenFormat = CLAIM_TOKEN_FORMAT_ID_TOKEN;
|
claimTokenFormat = CLAIM_TOKEN_FORMAT_ID_TOKEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
BiFunction<AuthorizationRequest, AuthorizationProvider, EvaluationContext> evaluationContextProvider = SUPPORTED_CLAIM_TOKEN_FORMATS.get(claimTokenFormat);
|
BiFunction<KeycloakAuthorizationRequest, AuthorizationProvider, EvaluationContext> evaluationContextProvider = SUPPORTED_CLAIM_TOKEN_FORMATS.get(claimTokenFormat);
|
||||||
|
|
||||||
if (evaluationContextProvider == null) {
|
if (evaluationContextProvider == null) {
|
||||||
throw new CorsErrorResponseException(request.getCors(), OAuthErrorException.INVALID_REQUEST, "Claim token format [" + claimTokenFormat + "] not supported", Status.BAD_REQUEST);
|
throw new CorsErrorResponseException(request.getCors(), OAuthErrorException.INVALID_REQUEST, "Claim token format [" + claimTokenFormat + "] not supported", Status.BAD_REQUEST);
|
||||||
|
|
|
@ -58,6 +58,7 @@ import org.keycloak.authorization.client.AuthzClient;
|
||||||
import org.keycloak.authorization.client.Configuration;
|
import org.keycloak.authorization.client.Configuration;
|
||||||
import org.keycloak.authorization.client.representation.TokenIntrospectionResponse;
|
import org.keycloak.authorization.client.representation.TokenIntrospectionResponse;
|
||||||
import org.keycloak.authorization.client.util.HttpResponseException;
|
import org.keycloak.authorization.client.util.HttpResponseException;
|
||||||
|
import org.keycloak.authorization.permission.ResourcePermission;
|
||||||
import org.keycloak.common.util.Base64Url;
|
import org.keycloak.common.util.Base64Url;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
|
@ -1872,6 +1873,55 @@ public class EntitlementAPITest extends AbstractAuthzTest {
|
||||||
assertEquals(PUBLIC_TEST_CLIENT, token.getIssuedFor());
|
assertEquals(PUBLIC_TEST_CLIENT, token.getIssuedFor());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUsingExpiredToken() throws Exception {
|
||||||
|
ClientResource client = getClient(getRealm(), RESOURCE_SERVER_TEST);
|
||||||
|
AuthorizationResource authorization = client.authorization();
|
||||||
|
|
||||||
|
JSPolicyRepresentation policy = new JSPolicyRepresentation();
|
||||||
|
|
||||||
|
policy.setName(KeycloakModelUtils.generateId());
|
||||||
|
policy.setCode("$evaluation.grant();");
|
||||||
|
|
||||||
|
authorization.policies().js().create(policy).close();
|
||||||
|
|
||||||
|
ResourceRepresentation resource = new ResourceRepresentation();
|
||||||
|
|
||||||
|
resource.setName("Sensors");
|
||||||
|
|
||||||
|
try (Response response = authorization.resources().create(resource)) {
|
||||||
|
resource = response.readEntity(ResourceRepresentation.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation();
|
||||||
|
|
||||||
|
permission.setName("View Sensor");
|
||||||
|
permission.addPolicy(policy.getName());
|
||||||
|
|
||||||
|
authorization.permissions().resource().create(permission).close();
|
||||||
|
|
||||||
|
String accessToken = new OAuthClient().realm("authz-test").clientId(RESOURCE_SERVER_TEST).doGrantAccessTokenRequest("secret", "marta", "password").getAccessToken();
|
||||||
|
AuthzClient authzClient = getAuthzClient(AUTHZ_CLIENT_CONFIG);
|
||||||
|
AccessTokenResponse response = authzClient.authorization(accessToken).authorize();
|
||||||
|
assertNotNull(response.getToken());
|
||||||
|
|
||||||
|
getRealm().logoutAll();
|
||||||
|
|
||||||
|
AuthorizationRequest request = new AuthorizationRequest();
|
||||||
|
|
||||||
|
request.addPermission("Sensors");
|
||||||
|
request.setSubjectToken(accessToken);
|
||||||
|
|
||||||
|
try {
|
||||||
|
authzClient.authorization().authorize(request);
|
||||||
|
fail("should fail, session invalidated");
|
||||||
|
} catch (Exception e) {
|
||||||
|
Throwable expected = e.getCause();
|
||||||
|
assertEquals(400, HttpResponseException.class.cast(expected).getStatusCode());
|
||||||
|
assertTrue(HttpResponseException.class.cast(expected).toString().contains("unauthorized_client"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void testRptRequestWithResourceName(String configFile) {
|
private void testRptRequestWithResourceName(String configFile) {
|
||||||
Metadata metadata = new Metadata();
|
Metadata metadata = new Metadata();
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue