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 |
} |