mirror of https://github.com/jacekkow/keycloak-protocol-cas

Matthias Piepkorn
2017-11-14 5570d4519cf3fdfdea45aec409a48c61e781e933
commit | author | age
7f7e0c 1 package org.keycloak.protocol.cas.endpoints;
MP 2
3 import org.jboss.logging.Logger;
4 import org.jboss.resteasy.annotations.cache.NoCache;
5 import org.jboss.resteasy.spi.HttpRequest;
6 import org.keycloak.common.ClientConnection;
7 import org.keycloak.events.Details;
8 import org.keycloak.events.Errors;
9 import org.keycloak.events.EventBuilder;
10 import org.keycloak.events.EventType;
11 import org.keycloak.models.*;
12 import org.keycloak.protocol.cas.CASLoginProtocol;
352436 13 import org.keycloak.protocol.cas.representations.CASErrorCode;
MP 14 import org.keycloak.protocol.cas.utils.CASValidationException;
7f7e0c 15 import org.keycloak.protocol.oidc.utils.RedirectUtils;
MP 16 import org.keycloak.services.managers.AuthenticationManager;
17 import org.keycloak.services.managers.ClientSessionCode;
18
19 import javax.ws.rs.GET;
20 import javax.ws.rs.core.*;
5570d4 21 import java.lang.reflect.Method;
7f7e0c 22
MP 23 public class ValidateEndpoint {
57a6c1 24     protected static final Logger logger = Logger.getLogger(ValidateEndpoint.class);
7f7e0c 25
MP 26     private static final String RESPONSE_OK = "yes\n";
27     private static final String RESPONSE_FAILED = "no\n";
28
29     @Context
30     protected KeycloakSession session;
31
32     @Context
33     protected ClientConnection clientConnection;
34
35     @Context
36     protected HttpRequest request;
37
38     @Context
39     protected HttpHeaders headers;
40
41     @Context
42     protected UriInfo uriInfo;
43
44     protected RealmModel realm;
45     protected EventBuilder event;
46     protected ClientModel client;
f75caf 47     protected AuthenticatedClientSessionModel clientSession;
7f7e0c 48
MP 49     public ValidateEndpoint(RealmModel realm, EventBuilder event) {
50         this.realm = realm;
51         this.event = event;
52     }
53
54     @GET
55     @NoCache
56     public Response build() {
57         MultivaluedMap<String, String> params = uriInfo.getQueryParameters();
58         String service = params.getFirst(CASLoginProtocol.SERVICE_PARAM);
59         String ticket = params.getFirst(CASLoginProtocol.TICKET_PARAM);
7124d2 60         boolean renew = params.containsKey(CASLoginProtocol.RENEW_PARAM);
7f7e0c 61
MP 62         event.event(EventType.CODE_TO_TOKEN);
63
64         try {
65             checkSsl();
66             checkRealm();
67             checkClient(service);
68
69             checkTicket(ticket, renew);
70
71             event.success();
72             return successResponse();
352436 73         } catch (CASValidationException e) {
7f7e0c 74             return errorResponse(e);
MP 75         }
76     }
77
78     protected Response successResponse() {
79         return Response.ok(RESPONSE_OK).type(MediaType.TEXT_PLAIN).build();
80     }
81
352436 82     protected Response errorResponse(CASValidationException e) {
MP 83         return Response.status(e.getStatus()).entity(RESPONSE_FAILED).type(MediaType.TEXT_PLAIN).build();
7f7e0c 84     }
MP 85
86     private void checkSsl() {
87         if (!uriInfo.getBaseUri().getScheme().equals("https") && realm.getSslRequired().isRequired(clientConnection)) {
352436 88             throw new CASValidationException(CASErrorCode.INVALID_REQUEST, "HTTPS required", Response.Status.FORBIDDEN);
7f7e0c 89         }
MP 90     }
91
92     private void checkRealm() {
93         if (!realm.isEnabled()) {
352436 94             throw new CASValidationException(CASErrorCode.INTERNAL_ERROR, "Realm not enabled", Response.Status.FORBIDDEN);
7f7e0c 95         }
MP 96     }
97
98     private void checkClient(String service) {
99         if (service == null) {
100             event.error(Errors.INVALID_REQUEST);
352436 101             throw new CASValidationException(CASErrorCode.INVALID_REQUEST, "Missing parameter: " + CASLoginProtocol.SERVICE_PARAM, Response.Status.BAD_REQUEST);
7f7e0c 102         }
MP 103
104         client = realm.getClients().stream()
105                 .filter(c -> CASLoginProtocol.LOGIN_PROTOCOL.equals(c.getProtocol()))
106                 .filter(c -> RedirectUtils.verifyRedirectUri(uriInfo, service, realm, c) != null)
107                 .findFirst().orElse(null);
108         if (client == null) {
109             event.error(Errors.CLIENT_NOT_FOUND);
352436 110             throw new CASValidationException(CASErrorCode.INVALID_SERVICE, "Client not found", Response.Status.BAD_REQUEST);
7f7e0c 111         }
MP 112
113         if (!client.isEnabled()) {
114             event.error(Errors.CLIENT_DISABLED);
352436 115             throw new CASValidationException(CASErrorCode.INVALID_SERVICE, "Client disabled", Response.Status.BAD_REQUEST);
7f7e0c 116         }
MP 117
118         event.client(client.getClientId());
119
120         session.getContext().setClient(client);
121     }
122
123     private void checkTicket(String ticket, boolean requireReauth) {
352436 124         if (ticket == null) {
7f7e0c 125             event.error(Errors.INVALID_CODE);
352436 126             throw new CASValidationException(CASErrorCode.INVALID_REQUEST, "Missing parameter: " + CASLoginProtocol.TICKET_PARAM, Response.Status.BAD_REQUEST);
MP 127         }
128         if (!ticket.startsWith(CASLoginProtocol.SERVICE_TICKET_PREFIX)) {
129             event.error(Errors.INVALID_CODE);
130             throw new CASValidationException(CASErrorCode.INVALID_TICKET_SPEC, "Malformed service ticket", Response.Status.BAD_REQUEST);
7f7e0c 131         }
MP 132
133         String code = ticket.substring(CASLoginProtocol.SERVICE_TICKET_PREFIX.length());
134
f75caf 135         String[] parts = code.split("\\.");
MP 136         if (parts.length == 4) {
137             event.detail(Details.CODE_ID, parts[2]);
138         }
139
5570d4 140         ClientSessionCode.ParseResult<AuthenticatedClientSessionModel> parseResult;
MP 141         try {
142             // Keycloak >3.4 branch: Parameter event was added to ClientSessionCode.parseResult
143             Method parseResultMethod = ClientSessionCode.class.getMethod("parseResult",
144                     String.class, KeycloakSession.class, RealmModel.class, EventBuilder.class, Class.class);
145             parseResult = (ClientSessionCode.ParseResult<AuthenticatedClientSessionModel>) parseResultMethod.invoke(
146                     null, code, session, realm, event, AuthenticatedClientSessionModel.class);
147         } catch (ReflectiveOperationException e) {
148             // Keycloak <=3.3 branch
149             parseResult = ClientSessionCode.parseResult(code, session, realm, AuthenticatedClientSessionModel.class);
150         }
f75caf 151         if (parseResult.isAuthSessionNotFound() || parseResult.isIllegalHash()) {
7f7e0c 152             event.error(Errors.INVALID_CODE);
f75caf 153
MP 154             // Attempt to use same code twice should invalidate existing clientSession
155             AuthenticatedClientSessionModel clientSession = parseResult.getClientSession();
156             if (clientSession != null) {
157                 clientSession.setUserSession(null);
7f7e0c 158             }
f75caf 159
352436 160             throw new CASValidationException(CASErrorCode.INVALID_TICKET, "Code not valid", Response.Status.BAD_REQUEST);
7f7e0c 161         }
MP 162
163         clientSession = parseResult.getClientSession();
164
5570d4 165         try {
MP 166             // Keycloak >3.4 branch: Method isExpiredToken was added
167             Method isExpiredToken = ClientSessionCode.ParseResult.class.getMethod("isExpiredToken");
168             if ((Boolean) isExpiredToken.invoke(parseResult)) {
169                 event.error(Errors.EXPIRED_CODE);
170                 throw new CASValidationException(CASErrorCode.INVALID_TICKET, "Code is expired", Response.Status.BAD_REQUEST);
171             }
172         } catch (ReflectiveOperationException e) {
173             // Keycloak <=3.3 branch
174             if (!parseResult.getCode().isValid(AuthenticatedClientSessionModel.Action.CODE_TO_TOKEN.name(), ClientSessionCode.ActionType.CLIENT)) {
175                 event.error(Errors.INVALID_CODE);
176                 throw new CASValidationException(CASErrorCode.INVALID_TICKET, "Code is expired", Response.Status.BAD_REQUEST);
177             }
178
179             parseResult.getCode().setAction(null);
7f7e0c 180         }
MP 181
57a6c1 182         clientSession.setNote(CASLoginProtocol.SESSION_SERVICE_TICKET, ticket);
7f7e0c 183
7124d2 184         if (requireReauth && AuthenticationManager.isSSOAuthentication(clientSession)) {
MP 185             event.error(Errors.SESSION_EXPIRED);
186             throw new CASValidationException(CASErrorCode.INVALID_TICKET, "Interactive authentication was requested but not performed", Response.Status.BAD_REQUEST);
187         }
188
7f7e0c 189         UserSessionModel userSession = clientSession.getUserSession();
MP 190
191         if (userSession == null) {
192             event.error(Errors.USER_SESSION_NOT_FOUND);
352436 193             throw new CASValidationException(CASErrorCode.INVALID_TICKET, "User session not found", Response.Status.BAD_REQUEST);
7f7e0c 194         }
MP 195
196         UserModel user = userSession.getUser();
197         if (user == null) {
198             event.error(Errors.USER_NOT_FOUND);
352436 199             throw new CASValidationException(CASErrorCode.INVALID_TICKET, "User not found", Response.Status.BAD_REQUEST);
7f7e0c 200         }
MP 201         if (!user.isEnabled()) {
202             event.error(Errors.USER_DISABLED);
352436 203             throw new CASValidationException(CASErrorCode.INVALID_TICKET, "User disabled", Response.Status.BAD_REQUEST);
7f7e0c 204         }
MP 205
206         event.user(userSession.getUser());
207         event.session(userSession.getId());
208
209         if (!client.getClientId().equals(clientSession.getClient().getClientId())) {
210             event.error(Errors.INVALID_CODE);
352436 211             throw new CASValidationException(CASErrorCode.INVALID_SERVICE, "Auth error", Response.Status.BAD_REQUEST);
7f7e0c 212         }
MP 213
214         if (!AuthenticationManager.isSessionValid(realm, userSession)) {
215             event.error(Errors.USER_SESSION_NOT_FOUND);
352436 216             throw new CASValidationException(CASErrorCode.INVALID_TICKET, "Session not active", Response.Status.BAD_REQUEST);
7f7e0c 217         }
MP 218
219     }
220 }