From 5d7080a6157ca47763bf1e682a67ea4875475fad Mon Sep 17 00:00:00 2001 From: Alexandre Rocha Wendling <alexandrerw@celepar.pr.gov.br> Date: Fri, 21 Jun 2024 08:57:40 +0000 Subject: [PATCH] Proxy ticket service and proxy ticket validation --- src/main/java/org/keycloak/protocol/cas/endpoints/ProxyValidateEndpoint.java | 73 ++++++++++ src/main/java/org/keycloak/protocol/cas/CASLoginProtocolService.java | 7 src/main/java/org/keycloak/protocol/cas/utils/CASValidationException.java | 1 src/main/java/org/keycloak/protocol/cas/representations/CASServiceResponseProxyFailure.java | 30 ++++ src/main/java/org/keycloak/protocol/cas/endpoints/ProxyEndpoint.java | 57 ++++++++ src/main/java/org/keycloak/protocol/cas/endpoints/ServiceValidateEndpoint.java | 2 src/test/java/org/keycloak/protocol/cas/ServiceResponseTest.java | 4 src/main/java/org/keycloak/protocol/cas/endpoints/AbstractValidateEndpoint.java | 155 ++++++++++++++++++--- src/main/java/org/keycloak/protocol/cas/utils/AttributesMapAdapter.java | 2 src/main/java/org/keycloak/protocol/cas/representations/CASErrorCode.java | 2 src/main/java/org/keycloak/protocol/cas/endpoints/SamlValidateEndpoint.java | 2 src/main/java/org/keycloak/protocol/cas/representations/CASServiceResponse.java | 18 ++ src/main/java/org/keycloak/protocol/cas/representations/CASServiceResponseProxySuccess.java | 18 ++ src/main/java/org/keycloak/protocol/cas/endpoints/ValidateEndpoint.java | 5 src/main/java/org/keycloak/protocol/cas/CASLoginProtocol.java | 19 +- src/main/java/org/keycloak/protocol/cas/utils/ServiceResponseHelper.java | 20 ++ 16 files changed, 371 insertions(+), 44 deletions(-) diff --git a/src/main/java/org/keycloak/protocol/cas/CASLoginProtocol.java b/src/main/java/org/keycloak/protocol/cas/CASLoginProtocol.java index dfe8321..7bb0445 100644 --- a/src/main/java/org/keycloak/protocol/cas/CASLoginProtocol.java +++ b/src/main/java/org/keycloak/protocol/cas/CASLoginProtocol.java @@ -6,23 +6,20 @@ import org.apache.http.HttpEntity; import org.jboss.logging.Logger; import org.keycloak.common.util.KeycloakUriBuilder; -import org.keycloak.common.util.Time; import org.keycloak.events.Details; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.models.*; import org.keycloak.protocol.LoginProtocol; +import org.keycloak.protocol.cas.endpoints.AbstractValidateEndpoint; import org.keycloak.protocol.cas.utils.LogoutHelper; -import org.keycloak.protocol.oidc.utils.OAuth2Code; -import org.keycloak.protocol.oidc.utils.OAuth2CodeParser; import org.keycloak.services.ErrorPage; import org.keycloak.services.managers.ResourceAdminManager; import org.keycloak.sessions.AuthenticationSessionModel; import java.io.IOException; import java.net.URI; -import java.util.UUID; public class CASLoginProtocol implements LoginProtocol { private static final Logger logger = Logger.getLogger(CASLoginProtocol.class); @@ -35,11 +32,17 @@ public static final String GATEWAY_PARAM = "gateway"; public static final String TICKET_PARAM = "ticket"; public static final String FORMAT_PARAM = "format"; + public static final String PGTURL_PARAM = "pgtUrl"; + public static final String TARGET_SERVICE_PARAM = "targetService"; + public static final String PGT_PARAM = "pgt"; public static final String TICKET_RESPONSE_PARAM = "ticket"; public static final String SAMLART_RESPONSE_PARAM = "SAMLart"; public static final String SERVICE_TICKET_PREFIX = "ST-"; + public static final String PROXY_GRANTING_TICKET_IOU_PREFIX = "PGTIOU-"; + public static final String PROXY_GRANTING_TICKET_PREFIX = "PGT-"; + public static final String PROXY_TICKET_PREFIX = "PT-"; public static final String SESSION_SERVICE_TICKET = "service_ticket"; public static final String LOGOUT_REDIRECT_URI = "CAS_LOGOUT_REDIRECT_URI"; @@ -98,15 +101,9 @@ String service = authSession.getRedirectUri(); //TODO validate service - OAuth2Code codeData = new OAuth2Code(UUID.randomUUID().toString(), - Time.currentTime() + userSession.getRealm().getAccessCodeLifespan(), - null, null, authSession.getRedirectUri(), null, null, - userSession.getId()); - String code = OAuth2CodeParser.persistCode(session, clientSession, codeData); - KeycloakUriBuilder uriBuilder = KeycloakUriBuilder.fromUri(service); - String loginTicket = SERVICE_TICKET_PREFIX + code; + String loginTicket = AbstractValidateEndpoint.getST(session, clientSession, service); if (authSession.getClientNotes().containsKey(CASLoginProtocol.TARGET_PARAM)) { // This was a SAML 1.1 auth request so return the ticket ID as "SAMLart" instead of "ticket" diff --git a/src/main/java/org/keycloak/protocol/cas/CASLoginProtocolService.java b/src/main/java/org/keycloak/protocol/cas/CASLoginProtocolService.java index a985901..8d8f944 100644 --- a/src/main/java/org/keycloak/protocol/cas/CASLoginProtocolService.java +++ b/src/main/java/org/keycloak/protocol/cas/CASLoginProtocolService.java @@ -1,8 +1,8 @@ package org.keycloak.protocol.cas; import jakarta.ws.rs.Path; -import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.UriBuilder; + import org.keycloak.events.EventBuilder; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -51,13 +51,12 @@ @Path("proxyValidate") public Object proxyValidate() { - //TODO implement - return serviceValidate(); + return new ProxyValidateEndpoint(session, realm, event); } @Path("proxy") public Object proxy() { - return Response.serverError().entity("Not implemented").build(); + return new ProxyEndpoint(session, realm, event); } @Path("p3/serviceValidate") diff --git a/src/main/java/org/keycloak/protocol/cas/endpoints/AbstractValidateEndpoint.java b/src/main/java/org/keycloak/protocol/cas/endpoints/AbstractValidateEndpoint.java index 6ad1ad5..2330cdc 100644 --- a/src/main/java/org/keycloak/protocol/cas/endpoints/AbstractValidateEndpoint.java +++ b/src/main/java/org/keycloak/protocol/cas/endpoints/AbstractValidateEndpoint.java @@ -5,29 +5,39 @@ import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.events.EventBuilder; +import org.keycloak.common.util.Time; import org.keycloak.models.*; import org.keycloak.protocol.ProtocolMapper; import org.keycloak.protocol.cas.CASLoginProtocol; import org.keycloak.protocol.cas.mappers.CASAttributeMapper; import org.keycloak.protocol.cas.representations.CASErrorCode; import org.keycloak.protocol.cas.utils.CASValidationException; -import org.keycloak.protocol.oidc.utils.OAuth2CodeParser; +import org.keycloak.protocol.oidc.utils.OAuth2Code; import org.keycloak.protocol.oidc.utils.RedirectUtils; import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.services.managers.UserSessionCrossDCManager; import org.keycloak.services.util.DefaultClientSessionContext; import java.util.HashMap; import java.util.Map; import java.util.Set; +import java.util.UUID; +import java.util.regex.Pattern; import java.util.stream.Collectors; +import org.apache.http.client.utils.URIBuilder; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.HttpResponse; +import org.apache.http.impl.client.HttpClientBuilder; public abstract class AbstractValidateEndpoint { protected final Logger logger = Logger.getLogger(getClass()); + private static final Pattern DOT = Pattern.compile("\\."); protected KeycloakSession session; protected RealmModel realm; protected EventBuilder event; protected ClientModel client; protected AuthenticatedClientSessionModel clientSession; + protected String pgtIou; public AbstractValidateEndpoint(KeycloakSession session, RealmModel realm, EventBuilder event) { this.session = session; @@ -74,50 +84,83 @@ session.getContext().setClient(client); } - protected void checkTicket(String ticket, boolean requireReauth) { + protected void checkTicket(String ticket, String prefix, boolean requireReauth) { if (ticket == null) { event.error(Errors.INVALID_CODE); throw new CASValidationException(CASErrorCode.INVALID_REQUEST, "Missing parameter: " + CASLoginProtocol.TICKET_PARAM, Response.Status.BAD_REQUEST); } - if (!ticket.startsWith(CASLoginProtocol.SERVICE_TICKET_PREFIX)) { + + if (!ticket.startsWith(prefix)) { event.error(Errors.INVALID_CODE); throw new CASValidationException(CASErrorCode.INVALID_TICKET_SPEC, "Malformed service ticket", Response.Status.BAD_REQUEST); } - String code = ticket.substring(CASLoginProtocol.SERVICE_TICKET_PREFIX.length()); + Boolean isreuse = ticket.startsWith(CASLoginProtocol.PROXY_GRANTING_TICKET_PREFIX); - OAuth2CodeParser.ParseResult parseResult = OAuth2CodeParser.parseCode(session, code, realm, event); - if (parseResult.isIllegalCode()) { + String[] parsed = DOT.split(ticket.substring(prefix.length()), 3); + if (parsed.length != 3) { event.error(Errors.INVALID_CODE); + throw new CASValidationException(CASErrorCode.INVALID_TICKET_SPEC, "Invalid format of the code", Response.Status.BAD_REQUEST); + } - // Attempt to use same code twice should invalidate existing clientSession - AuthenticatedClientSessionModel clientSession = parseResult.getClientSession(); - if (clientSession != null) { - clientSession.detachFromUserSession(); + String userSessionId = parsed[1]; + String clientUUID = parsed[2]; + + event.detail(Details.CODE_ID, userSessionId); + event.session(userSessionId); + + // Parse UUID + String codeUUID; + try { + codeUUID = parsed[0]; + } catch (IllegalArgumentException re) { + event.error(Errors.INVALID_CODE); + throw new CASValidationException(CASErrorCode.INVALID_TICKET_SPEC, "Invalid format of the UUID in the code", Response.Status.BAD_REQUEST); + } + + // Retrieve UserSession + UserSessionModel userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, userSessionId, clientUUID); + if (userSession == null) { + // Needed to track if code is invalid + userSession = session.sessions().getUserSession(realm, userSessionId); + if (userSession == null) { + event.error(Errors.USER_SESSION_NOT_FOUND); + throw new CASValidationException(CASErrorCode.INVALID_TICKET, "User session not found", Response.Status.BAD_REQUEST); } + } + clientSession = userSession.getAuthenticatedClientSessionByClient(clientUUID); + if (clientSession == null) { throw new CASValidationException(CASErrorCode.INVALID_TICKET, "Code not valid", Response.Status.BAD_REQUEST); } - clientSession = parseResult.getClientSession(); + SingleUseObjectProvider codeStore = session.singleUseObjects(); + Map<String, String> codeDataSerialized = isreuse? codeStore.get(prefix + codeUUID) : codeStore.remove(prefix + codeUUID); - if (parseResult.isExpiredCode()) { + // Either code not available + if (codeDataSerialized == null) { + throw new CASValidationException(CASErrorCode.INVALID_TICKET, "Code already used", Response.Status.BAD_REQUEST); + } + + OAuth2Code codeData = OAuth2Code.deserializeCode(codeDataSerialized); + + String persistedUserSessionId = codeData.getUserSessionId(); + if (!userSessionId.equals(persistedUserSessionId)) { + throw new CASValidationException(CASErrorCode.INVALID_TICKET, "Code "+codeUUID+"' is bound to a different session", Response.Status.BAD_REQUEST); + } + + // Finally doublecheck if code is not expired + int currentTime = Time.currentTime(); + if (currentTime > codeData.getExpiration()) { event.error(Errors.EXPIRED_CODE); throw new CASValidationException(CASErrorCode.INVALID_TICKET, "Code is expired", Response.Status.BAD_REQUEST); } - clientSession.setNote(CASLoginProtocol.SESSION_SERVICE_TICKET, ticket); + clientSession.setNote(prefix, ticket); if (requireReauth && AuthenticationManager.isSSOAuthentication(clientSession)) { event.error(Errors.SESSION_EXPIRED); throw new CASValidationException(CASErrorCode.INVALID_TICKET, "Interactive authentication was requested but not performed", Response.Status.BAD_REQUEST); - } - - UserSessionModel userSession = clientSession.getUserSession(); - - if (userSession == null) { - event.error(Errors.USER_SESSION_NOT_FOUND); - throw new CASValidationException(CASErrorCode.INVALID_TICKET, "User session not found", Response.Status.BAD_REQUEST); } UserModel user = userSession.getUser(); @@ -133,14 +176,44 @@ event.user(userSession.getUser()); event.session(userSession.getId()); - if (!client.getClientId().equals(clientSession.getClient().getClientId())) { - event.error(Errors.INVALID_CODE); - throw new CASValidationException(CASErrorCode.INVALID_SERVICE, "Auth error", Response.Status.BAD_REQUEST); + if (client == null) { + client = clientSession.getClient(); + } else { + if (!client.getClientId().equals(clientSession.getClient().getClientId())) { + event.error(Errors.INVALID_CODE); + throw new CASValidationException(CASErrorCode.INVALID_SERVICE, "Auth error", Response.Status.BAD_REQUEST); + } } if (!AuthenticationManager.isSessionValid(realm, userSession)) { event.error(Errors.USER_SESSION_NOT_FOUND); throw new CASValidationException(CASErrorCode.INVALID_TICKET, "Session not active", Response.Status.BAD_REQUEST); + } + + } + + protected void createProxyGrant(String pgtUrl) { + if ( RedirectUtils.verifyRedirectUri(session, pgtUrl, client) == null ) { + event.error(Errors.INVALID_REQUEST); + throw new CASValidationException(CASErrorCode.INVALID_PROXY_CALLBACK, "Proxy callback is invalid", Response.Status.BAD_REQUEST); + } + + String pgtIou = getPGTIOU(); + String pgtId = getPGT(session, clientSession, pgtUrl); + + try { + HttpResponse response = HttpClientBuilder.create().build().execute( + new HttpGet(new URIBuilder(pgtUrl).setParameter("pgtIou",pgtIou).setParameter("pgtId",pgtId).build()) + ); + + if (response.getStatusLine().getStatusCode() != 200) { + throw new Exception(); + } + + this.pgtIou = pgtIou; + } catch (Exception e) { + event.error(Errors.INVALID_REQUEST); + throw new CASValidationException(CASErrorCode.PROXY_CALLBACK_ERROR, "Proxy callback return with error", Response.Status.BAD_REQUEST); } } @@ -160,4 +233,40 @@ } return attributes; } + + protected String getPGTIOU() + { + return CASLoginProtocol.PROXY_GRANTING_TICKET_IOU_PREFIX + UUID.randomUUID().toString(); + } + + protected String getPGT(KeycloakSession session, AuthenticatedClientSessionModel clientSession, String pgtUrl) + { + return persistedTicket(pgtUrl, CASLoginProtocol.PROXY_GRANTING_TICKET_PREFIX); + } + + protected String getPT(KeycloakSession session, AuthenticatedClientSessionModel clientSession, String targetService) + { + return persistedTicket(targetService, CASLoginProtocol.PROXY_TICKET_PREFIX); + } + + protected String getST(String redirectUri) + { + return persistedTicket(redirectUri, CASLoginProtocol.SERVICE_TICKET_PREFIX); + } + + public static String getST(KeycloakSession session, AuthenticatedClientSessionModel clientSession, String redirectUri) + { + ValidateEndpoint vp = new ValidateEndpoint(session,null,null); + vp.clientSession = clientSession; + return vp.getST(redirectUri); + } + + protected String persistedTicket(String redirectUriParam, String prefix) + { + String key = UUID.randomUUID().toString(); + UserSessionModel userSession = clientSession.getUserSession(); + OAuth2Code codeData = new OAuth2Code(key, Time.currentTime() + userSession.getRealm().getAccessCodeLifespan(), null, null, redirectUriParam, null, null, userSession.getId()); + session.singleUseObjects().put(prefix + key, clientSession.getUserSession().getRealm().getAccessCodeLifespan(), codeData.serializeCode()); + return prefix + key + "." + clientSession.getUserSession().getId() + "." + clientSession.getClient().getId(); + } } diff --git a/src/main/java/org/keycloak/protocol/cas/endpoints/ProxyEndpoint.java b/src/main/java/org/keycloak/protocol/cas/endpoints/ProxyEndpoint.java new file mode 100644 index 0000000..41f81e0 --- /dev/null +++ b/src/main/java/org/keycloak/protocol/cas/endpoints/ProxyEndpoint.java @@ -0,0 +1,57 @@ +package org.keycloak.protocol.cas.endpoints; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.Response; +import org.jboss.resteasy.annotations.cache.NoCache; +import org.keycloak.events.EventBuilder; +import org.keycloak.events.EventType; +import org.keycloak.models.*; +import org.keycloak.protocol.cas.CASLoginProtocol; +import org.keycloak.protocol.cas.representations.CASServiceResponse; +import org.keycloak.protocol.cas.utils.CASValidationException; +import org.keycloak.protocol.cas.utils.ContentTypeHelper; +import org.keycloak.protocol.cas.utils.ServiceResponseHelper; + +public class ProxyEndpoint extends AbstractValidateEndpoint { + + public ProxyEndpoint(KeycloakSession session, RealmModel realm, EventBuilder event) { + super(session, realm, event); + } + + @GET + @NoCache + public Response build() { + MultivaluedMap<String, String> params = session.getContext().getUri().getQueryParameters(); + String targetService = params.getFirst(CASLoginProtocol.TARGET_SERVICE_PARAM); + String pgt = params.getFirst(CASLoginProtocol.PGT_PARAM); + + event.event(EventType.CODE_TO_TOKEN); + + try { + checkSsl(); + checkRealm(); + checkTicket(pgt, CASLoginProtocol.PROXY_GRANTING_TICKET_PREFIX, false); + event.success(); + return successResponse(getPT(this.session, clientSession, targetService)); + } catch (CASValidationException e) { + return errorResponse(e); + } + } + + protected Response successResponse(String pt) { + CASServiceResponse serviceResponse = ServiceResponseHelper.createProxySuccess(pt); + return prepare(Response.Status.OK, serviceResponse); + } + + protected Response errorResponse(CASValidationException e) { + CASServiceResponse serviceResponse = ServiceResponseHelper.createProxyFailure(e.getError(), e.getErrorDescription()); + return prepare(e.getStatus(), serviceResponse); + } + + private Response prepare(Response.Status status, CASServiceResponse serviceResponse) { + MediaType responseMediaType = new ContentTypeHelper(session.getContext().getUri()).selectResponseType(); + return ServiceResponseHelper.createResponse(status, responseMediaType, serviceResponse); + } +} diff --git a/src/main/java/org/keycloak/protocol/cas/endpoints/ProxyValidateEndpoint.java b/src/main/java/org/keycloak/protocol/cas/endpoints/ProxyValidateEndpoint.java new file mode 100644 index 0000000..35468f8 --- /dev/null +++ b/src/main/java/org/keycloak/protocol/cas/endpoints/ProxyValidateEndpoint.java @@ -0,0 +1,73 @@ +package org.keycloak.protocol.cas.endpoints; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.Response; +import java.util.Map; +import org.jboss.resteasy.annotations.cache.NoCache; +import org.keycloak.events.Errors; +import org.keycloak.events.EventBuilder; +import org.keycloak.events.EventType; +import org.keycloak.models.*; +import org.keycloak.protocol.cas.CASLoginProtocol; +import org.keycloak.protocol.cas.representations.CASErrorCode; +import org.keycloak.protocol.cas.representations.CASServiceResponse; +import org.keycloak.protocol.cas.utils.CASValidationException; +import org.keycloak.protocol.cas.utils.ContentTypeHelper; +import org.keycloak.protocol.cas.utils.ServiceResponseHelper; + +public class ProxyValidateEndpoint extends AbstractValidateEndpoint { + + public ProxyValidateEndpoint(KeycloakSession session,RealmModel realm, EventBuilder event) { + super(session, realm, event); + } + + @GET + @NoCache + public Response build() { + MultivaluedMap<String, String> params = session.getContext().getUri().getQueryParameters(); + String ticket = params.getFirst(CASLoginProtocol.TICKET_PARAM); + String pgtUrl = params.getFirst(CASLoginProtocol.PGTURL_PARAM); + boolean renew = params.containsKey(CASLoginProtocol.RENEW_PARAM); + + event.event(EventType.CODE_TO_TOKEN); + + try { + String prefix = ticket.startsWith(CASLoginProtocol.PROXY_TICKET_PREFIX)? CASLoginProtocol.PROXY_TICKET_PREFIX:( + ticket.startsWith(CASLoginProtocol.SERVICE_TICKET_PREFIX)? CASLoginProtocol.SERVICE_TICKET_PREFIX : null + ); + + if (prefix == null) { + event.error(Errors.INVALID_CODE); + throw new CASValidationException(CASErrorCode.INVALID_TICKET_SPEC, "Malformed service ticket", Response.Status.BAD_REQUEST); + } + + checkSsl(); + checkRealm(); + checkTicket(ticket, prefix, renew); + if (pgtUrl != null) createProxyGrant(pgtUrl); + event.success(); + return successResponse(); + } catch (CASValidationException e) { + return errorResponse(e); + } + } + + protected Response successResponse() { + UserSessionModel userSession = clientSession.getUserSession(); + Map<String, Object> attributes = getUserAttributes(); + CASServiceResponse serviceResponse = ServiceResponseHelper.createSuccess(userSession.getUser().getUsername(),attributes); + return prepare(Response.Status.OK, serviceResponse); + } + + protected Response errorResponse(CASValidationException e) { + CASServiceResponse serviceResponse = ServiceResponseHelper.createFailure(e.getError(), e.getErrorDescription()); + return prepare(e.getStatus(), serviceResponse); + } + + private Response prepare(Response.Status status, CASServiceResponse serviceResponse) { + MediaType responseMediaType = new ContentTypeHelper(session.getContext().getUri()).selectResponseType(); + return ServiceResponseHelper.createResponse(status, responseMediaType, serviceResponse); + } +} diff --git a/src/main/java/org/keycloak/protocol/cas/endpoints/SamlValidateEndpoint.java b/src/main/java/org/keycloak/protocol/cas/endpoints/SamlValidateEndpoint.java index 5442d70..211a0c9 100644 --- a/src/main/java/org/keycloak/protocol/cas/endpoints/SamlValidateEndpoint.java +++ b/src/main/java/org/keycloak/protocol/cas/endpoints/SamlValidateEndpoint.java @@ -56,7 +56,7 @@ String issuer = Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName()); String ticket = getTicket(input); - checkTicket(ticket, renew); + checkTicket(ticket, CASLoginProtocol.SERVICE_TICKET_PREFIX, renew); UserModel user = clientSession.getUserSession().getUser(); Map<String, Object> attributes = getUserAttributes(); diff --git a/src/main/java/org/keycloak/protocol/cas/endpoints/ServiceValidateEndpoint.java b/src/main/java/org/keycloak/protocol/cas/endpoints/ServiceValidateEndpoint.java index c0c1e2b..014ee49 100644 --- a/src/main/java/org/keycloak/protocol/cas/endpoints/ServiceValidateEndpoint.java +++ b/src/main/java/org/keycloak/protocol/cas/endpoints/ServiceValidateEndpoint.java @@ -22,7 +22,7 @@ protected Response successResponse() { UserSessionModel userSession = clientSession.getUserSession(); Map<String, Object> attributes = getUserAttributes(); - CASServiceResponse serviceResponse = ServiceResponseHelper.createSuccess(userSession.getUser().getUsername(), attributes); + CASServiceResponse serviceResponse = ServiceResponseHelper.createSuccess(userSession.getUser().getUsername(), attributes, this.pgtIou, null); return prepare(Response.Status.OK, serviceResponse); } diff --git a/src/main/java/org/keycloak/protocol/cas/endpoints/ValidateEndpoint.java b/src/main/java/org/keycloak/protocol/cas/endpoints/ValidateEndpoint.java index 8e547e6..a3c14a4 100644 --- a/src/main/java/org/keycloak/protocol/cas/endpoints/ValidateEndpoint.java +++ b/src/main/java/org/keycloak/protocol/cas/endpoints/ValidateEndpoint.java @@ -26,6 +26,7 @@ public Response build() { MultivaluedMap<String, String> params = session.getContext().getUri().getQueryParameters(); String service = params.getFirst(CASLoginProtocol.SERVICE_PARAM); + String pgtUrl = params.getFirst(CASLoginProtocol.PGTURL_PARAM); String ticket = params.getFirst(CASLoginProtocol.TICKET_PARAM); boolean renew = params.containsKey(CASLoginProtocol.RENEW_PARAM); @@ -36,7 +37,9 @@ checkRealm(); checkClient(service); - checkTicket(ticket, renew); + checkTicket(ticket, CASLoginProtocol.SERVICE_TICKET_PREFIX, renew); + + if (pgtUrl != null) createProxyGrant(pgtUrl); event.success(); return successResponse(); diff --git a/src/main/java/org/keycloak/protocol/cas/representations/CASErrorCode.java b/src/main/java/org/keycloak/protocol/cas/representations/CASErrorCode.java index d80825c..278c5c6 100644 --- a/src/main/java/org/keycloak/protocol/cas/representations/CASErrorCode.java +++ b/src/main/java/org/keycloak/protocol/cas/representations/CASErrorCode.java @@ -9,6 +9,8 @@ UNAUTHORIZED_SERVICE_PROXY, /** The proxy callback specified is invalid. The credentials specified for proxy authentication do not meet the security requirements */ INVALID_PROXY_CALLBACK, + /** The proxy callback specified return with error*/ + PROXY_CALLBACK_ERROR, /** the ticket provided was not valid, or the ticket did not come from an initial login and renew was set on validation. */ INVALID_TICKET, /** the ticket provided was valid, but the service specified did not match the service associated with the ticket. */ diff --git a/src/main/java/org/keycloak/protocol/cas/representations/CASServiceResponse.java b/src/main/java/org/keycloak/protocol/cas/representations/CASServiceResponse.java index 6716322..3e8fad8 100644 --- a/src/main/java/org/keycloak/protocol/cas/representations/CASServiceResponse.java +++ b/src/main/java/org/keycloak/protocol/cas/representations/CASServiceResponse.java @@ -6,6 +6,8 @@ public class CASServiceResponse { private CASServiceResponseAuthenticationFailure authenticationFailure; private CASServiceResponseAuthenticationSuccess authenticationSuccess; + private CASServiceResponseProxySuccess proxySuccess; + private CASServiceResponseProxyFailure proxyFailure; public CASServiceResponseAuthenticationFailure getAuthenticationFailure() { return this.authenticationFailure; @@ -22,4 +24,20 @@ public void setAuthenticationSuccess(final CASServiceResponseAuthenticationSuccess authenticationSuccess) { this.authenticationSuccess = authenticationSuccess; } + + public CASServiceResponseProxySuccess getProxySuccess() { + return this.proxySuccess; + } + + public void setProxySuccess(final CASServiceResponseProxySuccess proxySuccess) { + this.proxySuccess = proxySuccess; + } + + public CASServiceResponseProxyFailure getProxyFailure() { + return this.proxyFailure; + } + + public void setProxyFailure(final CASServiceResponseProxyFailure proxyFailure) { + this.proxyFailure = proxyFailure; + } } diff --git a/src/main/java/org/keycloak/protocol/cas/representations/CASServiceResponseProxyFailure.java b/src/main/java/org/keycloak/protocol/cas/representations/CASServiceResponseProxyFailure.java new file mode 100644 index 0000000..8b16a63 --- /dev/null +++ b/src/main/java/org/keycloak/protocol/cas/representations/CASServiceResponseProxyFailure.java @@ -0,0 +1,30 @@ +package org.keycloak.protocol.cas.representations; + +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; +import jakarta.xml.bind.annotation.XmlAttribute; +import jakarta.xml.bind.annotation.XmlValue; + +@XmlAccessorType(XmlAccessType.FIELD) +public class CASServiceResponseProxyFailure { + @XmlAttribute + private String code; + @XmlValue + private String description; + + public String getCode() { + return this.code; + } + + public void setCode(final String code) { + this.code = code; + } + + public String getDescription() { + return this.description; + } + + public void setDescription(final String description) { + this.description = description; + } +} diff --git a/src/main/java/org/keycloak/protocol/cas/representations/CASServiceResponseProxySuccess.java b/src/main/java/org/keycloak/protocol/cas/representations/CASServiceResponseProxySuccess.java new file mode 100644 index 0000000..356261d --- /dev/null +++ b/src/main/java/org/keycloak/protocol/cas/representations/CASServiceResponseProxySuccess.java @@ -0,0 +1,18 @@ +package org.keycloak.protocol.cas.representations; + +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; + + +@XmlAccessorType(XmlAccessType.FIELD) +public class CASServiceResponseProxySuccess { + private String proxyTicket; + + public String getProxyTicket() { + return this.proxyTicket; + } + + public void setProxyTicket(final String proxyTicket) { + this.proxyTicket = proxyTicket; + } +} diff --git a/src/main/java/org/keycloak/protocol/cas/utils/AttributesMapAdapter.java b/src/main/java/org/keycloak/protocol/cas/utils/AttributesMapAdapter.java index 3da76a9..90992c3 100644 --- a/src/main/java/org/keycloak/protocol/cas/utils/AttributesMapAdapter.java +++ b/src/main/java/org/keycloak/protocol/cas/utils/AttributesMapAdapter.java @@ -39,7 +39,7 @@ this.elements = new ArrayList<>(); for (Map.Entry<String, Object> entry : attributes.entrySet()) { if (entry.getValue() instanceof Collection) { - for (Object item : ((Collection) entry.getValue())) { + for (Object item : ((Collection<?>) entry.getValue())) { addElement(entry.getKey(), item); } } else { diff --git a/src/main/java/org/keycloak/protocol/cas/utils/CASValidationException.java b/src/main/java/org/keycloak/protocol/cas/utils/CASValidationException.java index 60da5f1..30d08a3 100644 --- a/src/main/java/org/keycloak/protocol/cas/utils/CASValidationException.java +++ b/src/main/java/org/keycloak/protocol/cas/utils/CASValidationException.java @@ -5,6 +5,7 @@ import org.keycloak.protocol.cas.representations.CASErrorCode; public class CASValidationException extends WebApplicationException { + private static final long serialVersionUID = 4929825917145240776L; private final CASErrorCode error; private final String errorDescription; private final Response.Status status; diff --git a/src/main/java/org/keycloak/protocol/cas/utils/ServiceResponseHelper.java b/src/main/java/org/keycloak/protocol/cas/utils/ServiceResponseHelper.java index ada4d7f..ed6b635 100644 --- a/src/main/java/org/keycloak/protocol/cas/utils/ServiceResponseHelper.java +++ b/src/main/java/org/keycloak/protocol/cas/utils/ServiceResponseHelper.java @@ -7,6 +7,8 @@ import org.keycloak.protocol.cas.representations.CASServiceResponse; import org.keycloak.protocol.cas.representations.CASServiceResponseAuthenticationFailure; import org.keycloak.protocol.cas.representations.CASServiceResponseAuthenticationSuccess; +import org.keycloak.protocol.cas.representations.CASServiceResponseProxySuccess; +import org.keycloak.protocol.cas.representations.CASServiceResponseProxyFailure; import java.util.List; import java.util.Map; @@ -43,6 +45,24 @@ return response; } + public static CASServiceResponse createProxySuccess(String pt) { + CASServiceResponse response = new CASServiceResponse(); + CASServiceResponseProxySuccess success = new CASServiceResponseProxySuccess(); + success.setProxyTicket(pt); + response.setProxySuccess(success); + return response; + } + + public static CASServiceResponse createProxyFailure(CASErrorCode errorCode, String errorDescription) { + CASServiceResponse response = new CASServiceResponse(); + CASServiceResponseProxyFailure failure = new CASServiceResponseProxyFailure(); + failure.setCode(errorCode == null ? CASErrorCode.INTERNAL_ERROR.name() : errorCode.name()); + failure.setDescription(errorDescription); + response.setProxyFailure(failure); + + return response; + } + public static Response createResponse(Response.Status status, MediaType mediaType, CASServiceResponse serviceResponse) { Response.ResponseBuilder builder = Response.status(status) .header(HttpHeaders.CONTENT_TYPE, mediaType.withCharset("utf-8")); diff --git a/src/test/java/org/keycloak/protocol/cas/ServiceResponseTest.java b/src/test/java/org/keycloak/protocol/cas/ServiceResponseTest.java index bed1f00..29ea43a 100644 --- a/src/test/java/org/keycloak/protocol/cas/ServiceResponseTest.java +++ b/src/test/java/org/keycloak/protocol/cas/ServiceResponseTest.java @@ -52,10 +52,10 @@ assertEquals("username", xpath.evaluate("/cas:serviceResponse/cas:authenticationSuccess/cas:user", doc)); int idx = 0; for (Node node : xpath.selectNodes("/cas:serviceResponse/cas:authenticationSuccess/cas:attributes/cas:list", doc)) { - assertEquals(((List)attributes.get("list")).get(idx), node.getTextContent()); + assertEquals(((List<?>)attributes.get("list")).get(idx), node.getTextContent()); idx++; } - assertEquals(((List)attributes.get("list")).size(), idx); + assertEquals(((List<?>)attributes.get("list")).size(), idx); assertEquals(attributes.get("int").toString(), xpath.evaluate("/cas:serviceResponse/cas:authenticationSuccess/cas:attributes/cas:int", doc)); assertEquals(attributes.get("string").toString(), xpath.evaluate("/cas:serviceResponse/cas:authenticationSuccess/cas:attributes/cas:string", doc)); -- Gitblit v1.9.1