Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
GRANDGERARD Gilles
keycloak-protocol-cas
Commits
57a6c100
Commit
57a6c100
authored
Jan 29, 2017
by
Matthias Piepkorn
Browse files
Add support for single logout
parent
7124d21d
Changes
6
Hide whitespace changes
Inline
Side-by-side
pom.xml
View file @
57a6c100
...
...
@@ -81,6 +81,12 @@
<version>
${keycloak.version}
</version>
<scope>
provided
</scope>
</dependency>
<dependency>
<groupId>
org.keycloak
</groupId>
<artifactId>
keycloak-saml-core
</artifactId>
<version>
${keycloak.version}
</version>
<scope>
provided
</scope>
</dependency>
<dependency>
<groupId>
junit
</groupId>
<artifactId>
junit
</artifactId>
...
...
src/main/java/org/keycloak/protocol/cas/CASLoginProtocol.java
View file @
57a6c100
package
org.keycloak.protocol.cas
;
import
org.apache.http.HttpEntity
;
import
org.jboss.logging.Logger
;
import
org.keycloak.common.util.KeycloakUriBuilder
;
import
org.keycloak.events.EventBuilder
;
import
org.keycloak.events.EventType
;
import
org.keycloak.models.*
;
import
org.keycloak.protocol.LoginProtocol
;
import
org.keycloak.protocol.cas.utils.LogoutHelper
;
import
org.keycloak.services.managers.ClientSessionCode
;
import
org.keycloak.services.managers.ResourceAdminManager
;
import
javax.ws.rs.core.HttpHeaders
;
import
javax.ws.rs.core.Response
;
import
javax.ws.rs.core.UriInfo
;
import
java.io.IOException
;
import
java.net.URI
;
public
class
CASLoginProtocol
implements
LoginProtocol
{
private
static
final
Logger
logger
=
Logger
.
getLogger
(
CASLoginProtocol
.
class
);
public
static
final
String
LOGIN_PROTOCOL
=
"cas"
;
public
static
final
String
SERVICE_PARAM
=
"service"
;
...
...
@@ -25,6 +31,7 @@ public class CASLoginProtocol implements LoginProtocol {
public
static
final
String
TICKET_RESPONSE_PARAM
=
"ticket"
;
public
static
final
String
SERVICE_TICKET_PREFIX
=
"ST-"
;
public
static
final
String
SESSION_SERVICE_TICKET
=
"service_ticket"
;
protected
KeycloakSession
session
;
protected
RealmModel
realm
;
...
...
@@ -96,10 +103,26 @@ public class CASLoginProtocol implements LoginProtocol {
@Override
public
void
backchannelLogout
(
UserSessionModel
userSession
,
ClientSessionModel
clientSession
)
{
String
logoutUrl
=
clientSession
.
getRedirectUri
();
String
serviceTicket
=
clientSession
.
getNote
(
CASLoginProtocol
.
SESSION_SERVICE_TICKET
);
//check if session is fully authenticated (i.e. serviceValidate has been called)
if
(
serviceTicket
!=
null
&&
!
serviceTicket
.
isEmpty
())
{
sendSingleLogoutRequest
(
logoutUrl
,
serviceTicket
);
}
ClientModel
client
=
clientSession
.
getClient
();
new
ResourceAdminManager
(
session
).
logoutClientSession
(
uriInfo
.
getRequestUri
(),
realm
,
client
,
clientSession
);
}
private
void
sendSingleLogoutRequest
(
String
logoutUrl
,
String
serviceTicket
)
{
HttpEntity
requestEntity
=
LogoutHelper
.
buildSingleLogoutRequest
(
serviceTicket
);
try
{
LogoutHelper
.
postWithRedirect
(
session
,
logoutUrl
,
requestEntity
);
logger
.
debug
(
"Sent CAS single logout for service "
+
logoutUrl
);
}
catch
(
IOException
e
)
{
logger
.
warn
(
"Failed to call CAS service for logout: "
+
logoutUrl
,
e
);
}
}
@Override
public
Response
frontchannelLogout
(
UserSessionModel
userSession
,
ClientSessionModel
clientSession
)
{
// todo oidc redirect support
...
...
src/main/java/org/keycloak/protocol/cas/endpoints/LogoutEndpoint.java
View file @
57a6c100
...
...
@@ -18,7 +18,7 @@ import javax.ws.rs.core.Response;
import
javax.ws.rs.core.UriInfo
;
public
class
LogoutEndpoint
{
private
static
final
Logger
logger
=
Logger
.
getLogger
(
org
.
keycloak
.
protocol
.
oidc
.
endpoints
.
LogoutEndpoint
.
class
);
private
static
final
Logger
logger
=
Logger
.
getLogger
(
LogoutEndpoint
.
class
);
@Context
private
KeycloakSession
session
;
...
...
src/main/java/org/keycloak/protocol/cas/endpoints/ValidateEndpoint.java
View file @
57a6c100
...
...
@@ -20,7 +20,7 @@ import javax.ws.rs.GET;
import
javax.ws.rs.core.*
;
public
class
ValidateEndpoint
{
protected
static
final
Logger
logger
=
Logger
.
getLogger
(
org
.
keycloak
.
protocol
.
oidc
.
endpoints
.
Logout
Endpoint
.
class
);
protected
static
final
Logger
logger
=
Logger
.
getLogger
(
Validate
Endpoint
.
class
);
private
static
final
String
RESPONSE_OK
=
"yes\n"
;
private
static
final
String
RESPONSE_FAILED
=
"no\n"
;
...
...
@@ -152,6 +152,7 @@ public class ValidateEndpoint {
throw
new
CASValidationException
(
CASErrorCode
.
INVALID_TICKET
,
"Code is expired"
,
Response
.
Status
.
BAD_REQUEST
);
}
clientSession
.
setNote
(
CASLoginProtocol
.
SESSION_SERVICE_TICKET
,
ticket
);
parseResult
.
getCode
().
setAction
(
null
);
if
(
requireReauth
&&
AuthenticationManager
.
isSSOAuthentication
(
clientSession
))
{
...
...
src/main/java/org/keycloak/protocol/cas/utils/LogoutHelper.java
0 → 100644
View file @
57a6c100
package
org.keycloak.protocol.cas.utils
;
import
org.apache.http.HttpEntity
;
import
org.apache.http.HttpResponse
;
import
org.apache.http.client.HttpClient
;
import
org.apache.http.client.methods.HttpPost
;
import
org.apache.http.entity.ContentType
;
import
org.apache.http.entity.StringEntity
;
import
org.keycloak.connections.httpclient.HttpClientProvider
;
import
org.keycloak.models.KeycloakSession
;
import
org.keycloak.saml.common.exceptions.ConfigurationException
;
import
org.keycloak.saml.processing.core.saml.v2.common.IDGenerator
;
import
org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil
;
import
javax.ws.rs.core.HttpHeaders
;
import
javax.xml.datatype.XMLGregorianCalendar
;
import
java.io.IOException
;
import
java.io.InputStream
;
import
java.nio.charset.StandardCharsets
;
public
class
LogoutHelper
{
//although it looks alike, the CAS SLO protocol has nothing to do with SAML; so we build the format
//required by the spec manually
private
static
final
String
TEMPLATE
=
"<samlp:LogoutRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" ID=\"$ID\" Version=\"2.0\" IssueInstant=\"$ISSUE_INSTANT\">\n"
+
" <saml:NameID xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">@NOT_USED@</saml:NameID>\n"
+
" <samlp:SessionIndex>$SESSION_IDENTIFIER</samlp:SessionIndex>\n"
+
"</samlp:LogoutRequest>"
;
public
static
HttpEntity
buildSingleLogoutRequest
(
String
serviceTicket
)
{
String
id
=
IDGenerator
.
create
(
"ID_"
);
XMLGregorianCalendar
issueInstant
;
try
{
issueInstant
=
XMLTimeUtil
.
getIssueInstant
();
}
catch
(
ConfigurationException
e
)
{
throw
new
RuntimeException
(
e
);
}
String
document
=
TEMPLATE
.
replace
(
"$ID"
,
id
).
replace
(
"$ISSUE_INSTANT"
,
issueInstant
.
toString
())
.
replace
(
"$SESSION_IDENTIFIER"
,
serviceTicket
);
return
new
StringEntity
(
document
,
ContentType
.
APPLICATION_XML
.
withCharset
(
StandardCharsets
.
UTF_8
));
}
public
static
void
postWithRedirect
(
KeycloakSession
session
,
String
url
,
HttpEntity
postBody
)
throws
IOException
{
HttpClient
httpClient
=
session
.
getProvider
(
HttpClientProvider
.
class
).
getHttpClient
();
for
(
int
i
=
0
;
i
<
2
;
i
++)
{
// follow redirects once
HttpPost
post
=
new
HttpPost
(
url
);
post
.
setEntity
(
postBody
);
HttpResponse
response
=
httpClient
.
execute
(
post
);
try
{
int
status
=
response
.
getStatusLine
().
getStatusCode
();
if
(
status
==
302
&&
!
url
.
endsWith
(
"/"
))
{
String
redirect
=
response
.
getFirstHeader
(
HttpHeaders
.
LOCATION
).
getValue
();
String
withSlash
=
url
+
"/"
;
if
(
withSlash
.
equals
(
redirect
))
{
url
=
withSlash
;
continue
;
}
}
}
finally
{
HttpEntity
entity
=
response
.
getEntity
();
if
(
entity
!=
null
)
{
InputStream
is
=
entity
.
getContent
();
if
(
is
!=
null
)
is
.
close
();
}
}
break
;
}
}
}
src/test/java/org/keycloak/protocol/cas/LogoutHelperTest.java
0 → 100644
View file @
57a6c100
package
org.keycloak.protocol.cas
;
import
org.apache.http.HttpEntity
;
import
org.junit.Test
;
import
org.keycloak.protocol.cas.utils.LogoutHelper
;
import
org.keycloak.saml.common.constants.JBossSAMLURIConstants
;
import
org.keycloak.saml.common.util.DocumentUtil
;
import
org.w3c.dom.Document
;
import
org.w3c.dom.Node
;
import
static
org
.
junit
.
Assert
.
assertEquals
;
import
static
org
.
junit
.
Assert
.
assertFalse
;
public
class
LogoutHelperTest
{
@Test
public
void
testLogoutRequest
()
throws
Exception
{
HttpEntity
requestEntity
=
LogoutHelper
.
buildSingleLogoutRequest
(
"ST-test"
);
Document
doc
=
DocumentUtil
.
getDocument
(
requestEntity
.
getContent
());
assertEquals
(
"LogoutRequest"
,
doc
.
getDocumentElement
().
getLocalName
());
assertEquals
(
JBossSAMLURIConstants
.
PROTOCOL_NSURI
.
get
(),
doc
.
getDocumentElement
().
getNamespaceURI
());
assertEquals
(
"2.0"
,
doc
.
getDocumentElement
().
getAttribute
(
"Version"
));
assertFalse
(
doc
.
getDocumentElement
().
getAttribute
(
"ID"
).
isEmpty
());
assertFalse
(
doc
.
getDocumentElement
().
getAttribute
(
"IssueInstant"
).
isEmpty
());
Node
nameID
=
doc
.
getDocumentElement
().
getElementsByTagNameNS
(
JBossSAMLURIConstants
.
ASSERTION_NSURI
.
get
(),
"NameID"
).
item
(
0
);
assertFalse
(
nameID
.
getTextContent
()
==
null
||
nameID
.
getTextContent
().
isEmpty
());
Node
sessionIndex
=
doc
.
getDocumentElement
().
getElementsByTagNameNS
(
JBossSAMLURIConstants
.
PROTOCOL_NSURI
.
get
(),
"SessionIndex"
).
item
(
0
);
assertEquals
(
"ST-test"
,
sessionIndex
.
getTextContent
());
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment