commit | author | age
|
7f7e0c
|
1 |
package org.keycloak.protocol.cas; |
MP |
2 |
|
fdb9f6
|
3 |
import jakarta.ws.rs.core.HttpHeaders; |
JK |
4 |
import jakarta.ws.rs.core.Response; |
|
5 |
import jakarta.ws.rs.core.UriInfo; |
57a6c1
|
6 |
import org.apache.http.HttpEntity; |
MP |
7 |
import org.jboss.logging.Logger; |
7f7e0c
|
8 |
import org.keycloak.common.util.KeycloakUriBuilder; |
b88dc3
|
9 |
import org.keycloak.events.Details; |
7f7e0c
|
10 |
import org.keycloak.events.EventBuilder; |
MP |
11 |
import org.keycloak.events.EventType; |
4a6620
|
12 |
import org.keycloak.forms.login.LoginFormsProvider; |
7f7e0c
|
13 |
import org.keycloak.models.*; |
3458cd
|
14 |
import org.keycloak.protocol.ClientData; |
7f7e0c
|
15 |
import org.keycloak.protocol.LoginProtocol; |
755fd7
|
16 |
import org.keycloak.protocol.cas.endpoints.AbstractValidateEndpoint; |
57a6c1
|
17 |
import org.keycloak.protocol.cas.utils.LogoutHelper; |
3882f0
|
18 |
import org.keycloak.services.ErrorPage; |
7f7e0c
|
19 |
import org.keycloak.services.managers.ResourceAdminManager; |
f75caf
|
20 |
import org.keycloak.sessions.AuthenticationSessionModel; |
7f7e0c
|
21 |
|
57a6c1
|
22 |
import java.io.IOException; |
7f7e0c
|
23 |
import java.net.URI; |
MP |
24 |
|
|
25 |
public class CASLoginProtocol implements LoginProtocol { |
57a6c1
|
26 |
private static final Logger logger = Logger.getLogger(CASLoginProtocol.class); |
MP |
27 |
|
7f7e0c
|
28 |
public static final String LOGIN_PROTOCOL = "cas"; |
MP |
29 |
|
|
30 |
public static final String SERVICE_PARAM = "service"; |
74023a
|
31 |
public static final String TARGET_PARAM = "TARGET"; |
7f7e0c
|
32 |
public static final String RENEW_PARAM = "renew"; |
MP |
33 |
public static final String GATEWAY_PARAM = "gateway"; |
|
34 |
public static final String TICKET_PARAM = "ticket"; |
|
35 |
public static final String FORMAT_PARAM = "format"; |
755fd7
|
36 |
public static final String PGTURL_PARAM = "pgtUrl"; |
ARW |
37 |
public static final String TARGET_SERVICE_PARAM = "targetService"; |
|
38 |
public static final String PGT_PARAM = "pgt"; |
7f7e0c
|
39 |
|
MP |
40 |
public static final String TICKET_RESPONSE_PARAM = "ticket"; |
891484
|
41 |
public static final String SAMLART_RESPONSE_PARAM = "SAMLart"; |
7f7e0c
|
42 |
|
MP |
43 |
public static final String SERVICE_TICKET_PREFIX = "ST-"; |
755fd7
|
44 |
public static final String PROXY_GRANTING_TICKET_IOU_PREFIX = "PGTIOU-"; |
ARW |
45 |
public static final String PROXY_GRANTING_TICKET_PREFIX = "PGT-"; |
|
46 |
public static final String PROXY_TICKET_PREFIX = "PT-"; |
57a6c1
|
47 |
public static final String SESSION_SERVICE_TICKET = "service_ticket"; |
4a6620
|
48 |
|
MP |
49 |
public static final String LOGOUT_REDIRECT_URI = "CAS_LOGOUT_REDIRECT_URI"; |
7f7e0c
|
50 |
|
MP |
51 |
protected KeycloakSession session; |
|
52 |
protected RealmModel realm; |
|
53 |
protected UriInfo uriInfo; |
|
54 |
protected HttpHeaders headers; |
|
55 |
protected EventBuilder event; |
|
56 |
|
7124d2
|
57 |
public CASLoginProtocol(KeycloakSession session, RealmModel realm, UriInfo uriInfo, HttpHeaders headers, EventBuilder event) { |
7f7e0c
|
58 |
this.session = session; |
MP |
59 |
this.realm = realm; |
|
60 |
this.uriInfo = uriInfo; |
|
61 |
this.headers = headers; |
|
62 |
this.event = event; |
|
63 |
} |
|
64 |
|
|
65 |
public CASLoginProtocol() { |
|
66 |
} |
|
67 |
|
|
68 |
@Override |
|
69 |
public CASLoginProtocol setSession(KeycloakSession session) { |
|
70 |
this.session = session; |
|
71 |
return this; |
|
72 |
} |
|
73 |
|
|
74 |
@Override |
|
75 |
public CASLoginProtocol setRealm(RealmModel realm) { |
|
76 |
this.realm = realm; |
|
77 |
return this; |
|
78 |
} |
|
79 |
|
|
80 |
@Override |
|
81 |
public CASLoginProtocol setUriInfo(UriInfo uriInfo) { |
|
82 |
this.uriInfo = uriInfo; |
|
83 |
return this; |
|
84 |
} |
|
85 |
|
|
86 |
@Override |
|
87 |
public CASLoginProtocol setHttpHeaders(HttpHeaders headers) { |
|
88 |
this.headers = headers; |
|
89 |
return this; |
|
90 |
} |
|
91 |
|
|
92 |
@Override |
|
93 |
public CASLoginProtocol setEventBuilder(EventBuilder event) { |
|
94 |
this.event = event; |
|
95 |
return this; |
|
96 |
} |
|
97 |
|
|
98 |
@Override |
c140ce
|
99 |
public Response authenticated(AuthenticationSessionModel authSession, UserSessionModel userSession, ClientSessionContext clientSessionCtx) { |
b8d686
|
100 |
AuthenticatedClientSessionModel clientSession = clientSessionCtx.getClientSession(); |
7f7e0c
|
101 |
|
c140ce
|
102 |
String service = authSession.getRedirectUri(); |
7f7e0c
|
103 |
//TODO validate service |
5570d4
|
104 |
|
7f7e0c
|
105 |
KeycloakUriBuilder uriBuilder = KeycloakUriBuilder.fromUri(service); |
891484
|
106 |
|
755fd7
|
107 |
String loginTicket = AbstractValidateEndpoint.getST(session, clientSession, service); |
891484
|
108 |
|
DR |
109 |
if (authSession.getClientNotes().containsKey(CASLoginProtocol.TARGET_PARAM)) { |
|
110 |
// This was a SAML 1.1 auth request so return the ticket ID as "SAMLart" instead of "ticket" |
|
111 |
uriBuilder.queryParam(SAMLART_RESPONSE_PARAM, loginTicket); |
|
112 |
} else { |
|
113 |
uriBuilder.queryParam(TICKET_RESPONSE_PARAM, loginTicket); |
|
114 |
} |
7f7e0c
|
115 |
|
MP |
116 |
URI redirectUri = uriBuilder.build(); |
|
117 |
|
|
118 |
Response.ResponseBuilder location = Response.status(302).location(redirectUri); |
|
119 |
return location.build(); |
|
120 |
} |
|
121 |
|
|
122 |
@Override |
f75caf
|
123 |
public Response sendError(AuthenticationSessionModel authSession, Error error) { |
3882f0
|
124 |
if (authSession.getClientNotes().containsKey(CASLoginProtocol.GATEWAY_PARAM)) { |
JK |
125 |
if (error == Error.PASSIVE_INTERACTION_REQUIRED || error == Error.PASSIVE_LOGIN_REQUIRED) { |
|
126 |
return Response.status(302).location(URI.create(authSession.getRedirectUri())).build(); |
|
127 |
} |
|
128 |
} |
|
129 |
return ErrorPage.error(session, authSession, Response.Status.INTERNAL_SERVER_ERROR, error.name()); |
7f7e0c
|
130 |
} |
MP |
131 |
|
|
132 |
@Override |
3458cd
|
133 |
public ClientData getClientData(AuthenticationSessionModel authSession) { |
JK |
134 |
return new ClientData(authSession.getRedirectUri(), null, null, null); |
|
135 |
} |
|
136 |
|
|
137 |
@Override |
|
138 |
public Response sendError(ClientModel clientModel, ClientData clientData, Error error) { |
|
139 |
return null; |
|
140 |
} |
|
141 |
|
|
142 |
@Override |
0ec410
|
143 |
public Response backchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { |
57a6c1
|
144 |
String logoutUrl = clientSession.getRedirectUri(); |
MP |
145 |
String serviceTicket = clientSession.getNote(CASLoginProtocol.SESSION_SERVICE_TICKET); |
|
146 |
//check if session is fully authenticated (i.e. serviceValidate has been called) |
|
147 |
if (serviceTicket != null && !serviceTicket.isEmpty()) { |
|
148 |
sendSingleLogoutRequest(logoutUrl, serviceTicket); |
|
149 |
} |
7f7e0c
|
150 |
ClientModel client = clientSession.getClient(); |
b92028
|
151 |
return new ResourceAdminManager(session).logoutClientSession(realm, client, clientSession); |
7f7e0c
|
152 |
} |
MP |
153 |
|
57a6c1
|
154 |
private void sendSingleLogoutRequest(String logoutUrl, String serviceTicket) { |
MP |
155 |
try { |
281a7e
|
156 |
HttpEntity requestEntity = LogoutHelper.buildSingleLogoutRequest(serviceTicket); |
57a6c1
|
157 |
LogoutHelper.postWithRedirect(session, logoutUrl, requestEntity); |
MP |
158 |
logger.debug("Sent CAS single logout for service " + logoutUrl); |
|
159 |
} catch (IOException e) { |
|
160 |
logger.warn("Failed to call CAS service for logout: " + logoutUrl, e); |
|
161 |
} |
|
162 |
} |
|
163 |
|
7f7e0c
|
164 |
@Override |
f75caf
|
165 |
public Response frontchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { |
7f7e0c
|
166 |
// todo oidc redirect support |
MP |
167 |
throw new RuntimeException("NOT IMPLEMENTED"); |
|
168 |
} |
|
169 |
|
|
170 |
@Override |
d5f868
|
171 |
public Response finishBrowserLogout(UserSessionModel userSession, AuthenticationSessionModel logoutSession) { |
4a6620
|
172 |
String redirectUri = userSession.getNote(CASLoginProtocol.LOGOUT_REDIRECT_URI); |
MP |
173 |
|
b88dc3
|
174 |
event.event(EventType.LOGOUT) |
AP |
175 |
.user(userSession.getUser()) |
|
176 |
.session(userSession) |
|
177 |
.detail(Details.USERNAME, userSession.getUser().getUsername()); |
cbb2f2
|
178 |
|
4a6620
|
179 |
if (redirectUri != null) { |
b88dc3
|
180 |
event.detail(Details.REDIRECT_URI, redirectUri); |
AP |
181 |
event.success(); |
cbb2f2
|
182 |
return Response.status(302).location(URI.create(redirectUri)).build(); |
4a6620
|
183 |
} |
b88dc3
|
184 |
|
AP |
185 |
event.success(); |
|
186 |
|
|
187 |
LoginFormsProvider infoPage = session.getProvider(LoginFormsProvider.class).setSuccess("Logout successful"); |
|
188 |
infoPage.setAttribute("skipLink", true); |
|
189 |
return infoPage.createInfoPage(); |
7f7e0c
|
190 |
} |
MP |
191 |
|
|
192 |
@Override |
f75caf
|
193 |
public boolean requireReauthentication(UserSessionModel userSession, AuthenticationSessionModel authSession) { |
MP |
194 |
return "true".equals(authSession.getClientNote(CASLoginProtocol.RENEW_PARAM)); |
7f7e0c
|
195 |
} |
MP |
196 |
|
|
197 |
@Override |
|
198 |
public void close() { |
|
199 |
|
|
200 |
} |
|
201 |
} |