ParEndpoint.java
/*
* 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.protocol.oidc.par.endpoints;
import org.keycloak.http.HttpRequest;
import org.keycloak.OAuthErrorException;
import org.keycloak.common.Profile;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.headers.SecurityHeadersProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.SingleUseObjectProvider;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpointChecker;
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest;
import org.keycloak.protocol.oidc.par.ParResponse;
import org.keycloak.protocol.oidc.par.clientpolicy.context.PushedAuthorizationRequestContext;
import org.keycloak.protocol.oidc.par.endpoints.request.ParEndpointRequestParserProcessor;
import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.resources.Cors;
import org.keycloak.utils.ProfileHelper;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriBuilder;
import static org.keycloak.protocol.oidc.OIDCLoginProtocol.REQUEST_URI_PARAM;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* Pushed Authorization Request endpoint
*/
public class ParEndpoint extends AbstractParEndpoint {
public static final String PAR_CREATED_TIME = "par.created.time";
private static final String REQUEST_URI_PREFIX = "urn:ietf:params:oauth:request_uri:";
public static final int REQUEST_URI_PREFIX_LENGTH = REQUEST_URI_PREFIX.length();
private final HttpRequest httpRequest;
private AuthorizationEndpointRequest authorizationRequest;
public static UriBuilder parUrl(UriBuilder baseUriBuilder) {
UriBuilder uriBuilder = OIDCLoginProtocolService.tokenServiceBaseUrl(baseUriBuilder);
return uriBuilder.path(OIDCLoginProtocolService.class, "resolveExtension").resolveTemplate("extension", ParRootEndpoint.PROVIDER_ID, false).path(ParRootEndpoint.class, "request");
}
public ParEndpoint(KeycloakSession session, EventBuilder event) {
super(session, event);
this.httpRequest = session.getContext().getHttpRequest();
}
@Path("/")
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.APPLICATION_JSON)
public Response request() {
ProfileHelper.requireFeature(Profile.Feature.PAR);
cors = Cors.add(httpRequest).auth().allowedMethods("POST").auth().exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS);
event.event(EventType.PUSHED_AUTHORIZATION_REQUEST);
checkSsl();
checkRealm();
authorizeClient();
if (httpRequest.getDecodedFormParameters().containsKey(REQUEST_URI_PARAM)) {
throw throwErrorResponseException(OAuthErrorException.INVALID_REQUEST, "It is not allowed to include request_uri to PAR.", Response.Status.BAD_REQUEST);
}
try {
authorizationRequest = ParEndpointRequestParserProcessor.parseRequest(event, session, client, httpRequest.getDecodedFormParameters());
} catch (Exception e) {
throw throwErrorResponseException(OAuthErrorException.INVALID_REQUEST_OBJECT, e.getMessage(), Response.Status.BAD_REQUEST);
}
AuthorizationEndpointChecker checker = new AuthorizationEndpointChecker()
.event(event)
.client(client)
.realm(realm)
.request(authorizationRequest)
.session(session);
try {
checker.checkRedirectUri();
} catch (AuthorizationEndpointChecker.AuthorizationCheckException ex) {
throw throwErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Invalid parameter: redirect_uri", Response.Status.BAD_REQUEST);
}
try {
checker.checkResponseType();
} catch (AuthorizationEndpointChecker.AuthorizationCheckException ex) {
if (ex.getError().equals(OAuthErrorException.UNSUPPORTED_RESPONSE_TYPE)) {
throw throwErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Unsupported response type", Response.Status.BAD_REQUEST);
} else {
ex.throwAsCorsErrorResponseException(cors);
}
}
try {
checker.checkValidScope();
} catch (AuthorizationEndpointChecker.AuthorizationCheckException ex) {
// PAR throws this as "invalid_request" error
throw throwErrorResponseException(OAuthErrorException.INVALID_REQUEST, ex.getErrorDescription(), Response.Status.BAD_REQUEST);
}
try {
checker.checkInvalidRequestMessage();
checker.checkOIDCRequest();
checker.checkOIDCParams();
checker.checkPKCEParams();
} catch (AuthorizationEndpointChecker.AuthorizationCheckException ex) {
ex.throwAsCorsErrorResponseException(cors);
}
try {
session.clientPolicy().triggerOnEvent(new PushedAuthorizationRequestContext(authorizationRequest, httpRequest.getDecodedFormParameters()));
} catch (ClientPolicyException cpe) {
throw throwErrorResponseException(cpe.getError(), cpe.getErrorDetail(), Response.Status.BAD_REQUEST);
}
Map<String, String> params = new HashMap<>();
String key = UUID.randomUUID().toString();
String requestUri = REQUEST_URI_PREFIX + key;
int expiresIn = realm.getParPolicy().getRequestUriLifespan();
httpRequest.getDecodedFormParameters().forEach((k, v) -> {
// PAR store only accepts Map so that MultivaluedMap needs to be converted to Map.
String singleValue = String.valueOf(v).replace("[", "").replace("]", "");
params.put(k, singleValue);
});
params.put(PAR_CREATED_TIME, String.valueOf(System.currentTimeMillis()));
SingleUseObjectProvider singleUseStore = session.singleUseObjects();
singleUseStore.put(key, expiresIn, params);
ParResponse parResponse = new ParResponse(requestUri, expiresIn);
session.getProvider(SecurityHeadersProvider.class).options().allowEmptyContentType();
return cors.builder(Response.status(Response.Status.CREATED)
.entity(parResponse)
.type(MediaType.APPLICATION_JSON_TYPE))
.build();
}
}