From 755fd78fa0ee0f2a67417a119382c63e02c1091e Mon Sep 17 00:00:00 2001
From: Alexandre Rocha Wendling <alexandrerw@celepar.pr.gov.br>
Date: Tue, 16 Jul 2024 14:15:23 +0000
Subject: [PATCH] Proxy ticket service and proxy ticket validation Proxy endpoints improvements suggested by Jacek Kowalski Add ticket type to storage key Rename isreuse to isReusable Remove "parsing" of "codeUUID" that is String, not UUID Improve error reporting in CAS 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 
 README.md                                                                                   |    1 
 src/main/java/org/keycloak/protocol/cas/endpoints/AbstractValidateEndpoint.java             |  150 ++++++++++++++++++---
 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 ++
 17 files changed, 366 insertions(+), 45 deletions(-)

diff --git a/README.md b/README.md
index e38faeb..3c48f4b 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,6 @@
 
 The following features are **missing**:
 * SAML request/response [CAS 3.0 - optional]
-* Proxy ticket service and proxy ticket validation [CAS 2.0]
 
 The following features are out of scope:
 * Long-Term Tickets - Remember-Me [CAS 3.0 - optional]
diff --git a/src/main/java/org/keycloak/protocol/cas/CASLoginProtocol.java b/src/main/java/org/keycloak/protocol/cas/CASLoginProtocol.java
index adf1019..52dc060 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..e166bb0 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,78 @@
         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 isReusable = 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 codeUUID = parsed[0];
+        String userSessionId = parsed[1];
+        String clientUUID = parsed[2];
+
+        event.detail(Details.CODE_ID, userSessionId);
+        event.session(userSessionId);
+
+        // 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, "Code not valid", Response.Status.BAD_REQUEST);
             }
+        }
 
+        clientSession = userSession.getAuthenticatedClientSessionByClient(clientUUID);
+        if (clientSession == null) {
+            event.error(Errors.INVALID_CODE);
             throw new CASValidationException(CASErrorCode.INVALID_TICKET, "Code not valid", Response.Status.BAD_REQUEST);
         }
 
-        clientSession = parseResult.getClientSession();
+        SingleUseObjectProvider codeStore = session.singleUseObjects();
+        Map<String, String> codeDataSerialized = isReusable ? codeStore.get(prefix + codeUUID) : codeStore.remove(prefix + codeUUID);
 
-        if (parseResult.isExpiredCode()) {
+        // Either code not available
+        if (codeDataSerialized == null) {
+            event.error(Errors.INVALID_CODE);
+            throw new CASValidationException(CASErrorCode.INVALID_TICKET, "Code not valid", Response.Status.BAD_REQUEST);
+        }
+
+        OAuth2Code codeData = OAuth2Code.deserializeCode(codeDataSerialized);
+
+        String persistedUserSessionId = codeData.getUserSessionId();
+        if (!userSessionId.equals(persistedUserSessionId)) {
+            event.error(Errors.INVALID_CODE);
+            throw new CASValidationException(CASErrorCode.INVALID_TICKET, "Code not valid", 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 +171,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, "Invalid service", 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 returned an error", Response.Status.BAD_REQUEST);
         }
     }
 
@@ -160,4 +228,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