change logout behavior
This commit is contained in:
parent
b96f8aef6d
commit
dc4e8603d7
17 changed files with 225 additions and 96 deletions
|
@ -12,6 +12,7 @@
|
||||||
<!ENTITY JBossAdapter SYSTEM "modules/jboss-adapter.xml">
|
<!ENTITY JBossAdapter SYSTEM "modules/jboss-adapter.xml">
|
||||||
<!ENTITY JavascriptAdapter SYSTEM "modules/javascript-adapter.xml">
|
<!ENTITY JavascriptAdapter SYSTEM "modules/javascript-adapter.xml">
|
||||||
<!ENTITY InstalledApplications SYSTEM "modules/installed-applications.xml">
|
<!ENTITY InstalledApplications SYSTEM "modules/installed-applications.xml">
|
||||||
|
<!ENTITY Logout SYSTEM "modules/logout.xml">
|
||||||
<!ENTITY SocialConfig SYSTEM "modules/social-config.xml">
|
<!ENTITY SocialConfig SYSTEM "modules/social-config.xml">
|
||||||
<!ENTITY SocialFacebook SYSTEM "modules/social-facebook.xml">
|
<!ENTITY SocialFacebook SYSTEM "modules/social-facebook.xml">
|
||||||
<!ENTITY SocialGitHub SYSTEM "modules/social-github.xml">
|
<!ENTITY SocialGitHub SYSTEM "modules/social-github.xml">
|
||||||
|
@ -80,6 +81,7 @@ This one is short
|
||||||
&JBossAdapter;
|
&JBossAdapter;
|
||||||
&JavascriptAdapter;
|
&JavascriptAdapter;
|
||||||
&InstalledApplications;
|
&InstalledApplications;
|
||||||
|
&Logout;
|
||||||
</chapter>
|
</chapter>
|
||||||
|
|
||||||
<chapter>
|
<chapter>
|
||||||
|
|
|
@ -1,5 +1,16 @@
|
||||||
<chapter id="Migration_from_older_versions">
|
<chapter id="Migration_from_older_versions">
|
||||||
<title>Migration from older versions</title>
|
<title>Migration from older versions</title>
|
||||||
|
<sect1>
|
||||||
|
<title>Migrating from 1.0 Beta 4 to RC-1</title>
|
||||||
|
<itemizedlist>
|
||||||
|
<listitem>
|
||||||
|
logout REST API has been refactored. The GET request on the logout URI does not take a session_state
|
||||||
|
parameter anymore. You must be logged in in order to log out the session.
|
||||||
|
You can also POST to the lougt REST URI. This action requires a valid refresh token to perform the logout.
|
||||||
|
The signature is the same as refresh token minus the grant type form parameter. See documentation for details.
|
||||||
|
</listitem>
|
||||||
|
</itemizedlist>
|
||||||
|
</sect1>
|
||||||
<sect1>
|
<sect1>
|
||||||
<title>Migrating from 1.0 Beta 1 to Beta 4</title>
|
<title>Migrating from 1.0 Beta 1 to Beta 4</title>
|
||||||
<itemizedlist>
|
<itemizedlist>
|
||||||
|
|
|
@ -88,7 +88,7 @@ try {
|
||||||
if (isPublic()) { // if client is public access type
|
if (isPublic()) { // if client is public access type
|
||||||
formparams.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, "customer-portal"));
|
formparams.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, "customer-portal"));
|
||||||
} else {
|
} else {
|
||||||
String authorization = BasicAuthHelper.createHeader("customer-portal", "secret-secret-secret);
|
String authorization = BasicAuthHelper.createHeader("customer-portal", "secret-secret-secret");
|
||||||
post.setHeader("Authorization", authorization);
|
post.setHeader("Authorization", authorization);
|
||||||
}
|
}
|
||||||
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
|
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
|
||||||
|
@ -125,4 +125,36 @@ GET /my/rest/api
|
||||||
Authorization: Bearer 2YotnFZFEjr1zCsicMWpAA
|
Authorization: Bearer 2YotnFZFEjr1zCsicMWpAA
|
||||||
</programlisting>
|
</programlisting>
|
||||||
</para>
|
</para>
|
||||||
|
<para>
|
||||||
|
To logout you must use the refresh token contained in the AccessTokenResponse object.
|
||||||
|
</para>
|
||||||
|
<programlisting>
|
||||||
|
<![CDATA[
|
||||||
|
List<NameValuePair> formparams = new ArrayList<NameValuePair>();
|
||||||
|
if (isPublic()) { // if client is public access type
|
||||||
|
formparams.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, "customer-portal"));
|
||||||
|
} else {
|
||||||
|
String authorization = BasicAuthHelper.createHeader("customer-portal", "secret-secret-secret");
|
||||||
|
post.setHeader("Authorization", authorization);
|
||||||
|
}
|
||||||
|
formparams.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, tokenResponse.getRefreshToken()));
|
||||||
|
HttpResponse response = null;
|
||||||
|
URI logoutUri = KeycloakUriBuilder.fromUri(getBaseUrl(request) + "/auth")
|
||||||
|
.path(ServiceUrlConstants.TOKEN_SERVICE_LOGOUT_PATH)
|
||||||
|
.build("demo");
|
||||||
|
HttpPost post = new HttpPost(logoutUri);
|
||||||
|
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
|
||||||
|
post.setEntity(form);
|
||||||
|
response = client.execute(post);
|
||||||
|
int status = response.getStatusLine().getStatusCode();
|
||||||
|
HttpEntity entity = response.getEntity();
|
||||||
|
if (status != 204) {
|
||||||
|
error(status, entity);
|
||||||
|
}
|
||||||
|
if (entity == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
InputStream is = entity.getContent();
|
||||||
|
if (is != null) is.close();
|
||||||
|
]]></programlisting>
|
||||||
</chapter>
|
</chapter>
|
9
docbook/reference/en/en-US/modules/logout.xml
Executable file
9
docbook/reference/en/en-US/modules/logout.xml
Executable file
|
@ -0,0 +1,9 @@
|
||||||
|
<section>
|
||||||
|
<title>Logout</title>
|
||||||
|
<para>
|
||||||
|
There are multiple ways you can logout from a web application. For Java EE servlet containers, you can call
|
||||||
|
HttpServletRequest.logout().
|
||||||
|
For any other browser application, you can point the browser at the url <literal>http://auth-server/auth/realms/{realm-name}/tokens/logout?redirect_uri=encodedRedirectUri</literal>.
|
||||||
|
This will log you out if you have an SSO session with your browser.
|
||||||
|
</para>
|
||||||
|
</section>
|
|
@ -102,17 +102,23 @@ public class AdminClient {
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
HttpGet get = new HttpGet(KeycloakUriBuilder.fromUri(getBaseUrl(request) + "/auth")
|
HttpPost post = new HttpPost(KeycloakUriBuilder.fromUri(getBaseUrl(request) + "/auth")
|
||||||
.path(ServiceUrlConstants.TOKEN_SERVICE_LOGOUT_PATH)
|
.path(ServiceUrlConstants.TOKEN_SERVICE_LOGOUT_PATH)
|
||||||
.queryParam("session_state", res.getSessionState())
|
|
||||||
.build("demo"));
|
.build("demo"));
|
||||||
HttpResponse response = client.execute(get);
|
List<NameValuePair> formparams = new ArrayList<NameValuePair>();
|
||||||
|
formparams.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, res.getRefreshToken()));
|
||||||
|
formparams.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, "admin-client"));
|
||||||
|
HttpResponse response = client.execute(post);
|
||||||
|
boolean status = response.getStatusLine().getStatusCode() != 204;
|
||||||
HttpEntity entity = response.getEntity();
|
HttpEntity entity = response.getEntity();
|
||||||
if (entity == null) {
|
if (entity == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
InputStream is = entity.getContent();
|
InputStream is = entity.getContent();
|
||||||
if (is != null) is.close();
|
if (is != null) is.close();
|
||||||
|
if (status) {
|
||||||
|
throw new RuntimeException("failed to logout");
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
client.getConnectionManager().shutdown();
|
client.getConnectionManager().shutdown();
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,14 @@ public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext
|
||||||
return super.getTokenString();
|
return super.getTokenString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void logout(KeycloakDeployment deployment) {
|
||||||
|
try {
|
||||||
|
ServerRequest.invokeLogout(deployment, refreshToken);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("failed to invoke remote logout", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isActive() {
|
public boolean isActive() {
|
||||||
return this.token.isActive() && this.token.getIssuedAt() > deployment.getNotBefore();
|
return this.token.isActive() && this.token.getIssuedAt() > deployment.getNotBefore();
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,19 +48,41 @@ public class ServerRequest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void invokeLogout(KeycloakDeployment deployment, String sessionId) throws IOException, HttpFailure {
|
public static void invokeLogout(KeycloakDeployment deployment, String refreshToken) throws IOException, HttpFailure {
|
||||||
URI uri = deployment.getLogoutUrl().clone().queryParam("session_state", sessionId).build();
|
String client_id = deployment.getResourceName();
|
||||||
HttpGet logout = new HttpGet(uri);
|
Map<String, String> credentials = deployment.getResourceCredentials();
|
||||||
HttpResponse response = deployment.getClient().execute(logout);
|
HttpClient client = deployment.getClient();
|
||||||
|
URI uri = deployment.getLogoutUrl().clone().build();
|
||||||
|
List<NameValuePair> formparams = new ArrayList<NameValuePair>();
|
||||||
|
for (Map.Entry<String, String> entry : credentials.entrySet()) {
|
||||||
|
formparams.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
|
||||||
|
}
|
||||||
|
formparams.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, refreshToken));
|
||||||
|
HttpResponse response = null;
|
||||||
|
HttpPost post = new HttpPost(uri);
|
||||||
|
if (!deployment.isPublicClient()) {
|
||||||
|
String clientSecret = credentials.get(CredentialRepresentation.SECRET);
|
||||||
|
if (clientSecret != null) {
|
||||||
|
String authorization = BasicAuthHelper.createHeader(client_id, clientSecret);
|
||||||
|
post.setHeader("Authorization", authorization);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
formparams.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, client_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
|
||||||
|
post.setEntity(form);
|
||||||
|
response = client.execute(post);
|
||||||
int status = response.getStatusLine().getStatusCode();
|
int status = response.getStatusLine().getStatusCode();
|
||||||
HttpEntity entity = response.getEntity();
|
HttpEntity entity = response.getEntity();
|
||||||
if (status != 200) {
|
if (status != 204) {
|
||||||
error(status, entity);
|
error(status, entity);
|
||||||
}
|
}
|
||||||
if (entity == null) {
|
if (entity == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
entity.getContent().close();
|
InputStream is = entity.getContent();
|
||||||
|
if (is != null) is.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static AccessTokenResponse invokeAccessCodeToToken(KeycloakDeployment deployment, String code, String redirectUri) throws HttpFailure, IOException {
|
public static AccessTokenResponse invokeAccessCodeToToken(KeycloakDeployment deployment, String code, String redirectUri) throws HttpFailure, IOException {
|
||||||
|
|
|
@ -64,10 +64,8 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
|
||||||
Session session = request.getSessionInternal(false);
|
Session session = request.getSessionInternal(false);
|
||||||
if (session != null) {
|
if (session != null) {
|
||||||
session.removeNote(KeycloakSecurityContext.class.getName());
|
session.removeNote(KeycloakSecurityContext.class.getName());
|
||||||
try {
|
if (ksc instanceof RefreshableKeycloakSecurityContext) {
|
||||||
ServerRequest.invokeLogout(deploymentContext.getDeployment(), ksc.getToken().getSessionState());
|
((RefreshableKeycloakSecurityContext)ksc).logout(deploymentContext.getDeployment());
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("failed to invoke remote logout", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,11 +81,8 @@ public class ServletKeycloakAuthMech extends UndertowKeycloakAuthMech {
|
||||||
if (account == null) return;
|
if (account == null) return;
|
||||||
session.removeAttribute(KeycloakSecurityContext.class.getName());
|
session.removeAttribute(KeycloakSecurityContext.class.getName());
|
||||||
session.removeAttribute(KeycloakUndertowAccount.class.getName());
|
session.removeAttribute(KeycloakUndertowAccount.class.getName());
|
||||||
String sessionId = account.getKeycloakSecurityContext().getToken().getSessionState();
|
if (account.getKeycloakSecurityContext() != null) {
|
||||||
try {
|
account.getKeycloakSecurityContext().logout(deploymentContext.getDeployment());
|
||||||
ServerRequest.invokeLogout(deploymentContext.getDeployment(), sessionId);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("failed to invoke remote logout", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -72,11 +72,8 @@ public abstract class UndertowKeycloakAuthMech implements AuthenticationMechanis
|
||||||
KeycloakUndertowAccount account = (KeycloakUndertowAccount)session.getAttribute(KeycloakUndertowAccount.class.getName());
|
KeycloakUndertowAccount account = (KeycloakUndertowAccount)session.getAttribute(KeycloakUndertowAccount.class.getName());
|
||||||
if (account == null) return;
|
if (account == null) return;
|
||||||
session.removeAttribute(KeycloakUndertowAccount.class.getName());
|
session.removeAttribute(KeycloakUndertowAccount.class.getName());
|
||||||
String sessionId = account.getKeycloakSecurityContext().getToken().getSessionState();
|
if (account.getKeycloakSecurityContext() != null) {
|
||||||
try {
|
account.getKeycloakSecurityContext().logout(deploymentContext.getDeployment());
|
||||||
ServerRequest.invokeLogout(deploymentContext.getDeployment(), sessionId);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("failed to invoke remote logout", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -64,23 +64,7 @@ public class TokenManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public AccessToken refreshAccessToken(KeycloakSession session, UriInfo uriInfo, ClientConnection connection, RealmModel realm, ClientModel client, String encodedRefreshToken, Audit audit) throws OAuthErrorException {
|
public AccessToken refreshAccessToken(KeycloakSession session, UriInfo uriInfo, ClientConnection connection, RealmModel realm, ClientModel client, String encodedRefreshToken, Audit audit) throws OAuthErrorException {
|
||||||
JWSInput jws = new JWSInput(encodedRefreshToken);
|
RefreshToken refreshToken = verifyRefreshToken(realm, encodedRefreshToken);
|
||||||
RefreshToken refreshToken = null;
|
|
||||||
try {
|
|
||||||
if (!RSAProvider.verify(jws, realm.getPublicKey())) {
|
|
||||||
throw new RuntimeException("Invalid refresh token");
|
|
||||||
}
|
|
||||||
refreshToken = jws.readJsonContent(RefreshToken.class);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token", e);
|
|
||||||
}
|
|
||||||
if (refreshToken.isExpired()) {
|
|
||||||
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Refresh token expired");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (refreshToken.getIssuedAt() < realm.getNotBefore()) {
|
|
||||||
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale refresh token");
|
|
||||||
}
|
|
||||||
|
|
||||||
audit.user(refreshToken.getSubject()).session(refreshToken.getSessionState()).detail(Details.REFRESH_TOKEN_ID, refreshToken.getId());
|
audit.user(refreshToken.getSubject()).session(refreshToken.getSessionState()).detail(Details.REFRESH_TOKEN_ID, refreshToken.getId());
|
||||||
|
|
||||||
|
@ -122,6 +106,27 @@ public class TokenManager {
|
||||||
return accessToken;
|
return accessToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RefreshToken verifyRefreshToken(RealmModel realm, String encodedRefreshToken) throws OAuthErrorException {
|
||||||
|
JWSInput jws = new JWSInput(encodedRefreshToken);
|
||||||
|
RefreshToken refreshToken = null;
|
||||||
|
try {
|
||||||
|
if (!RSAProvider.verify(jws, realm.getPublicKey())) {
|
||||||
|
throw new RuntimeException("Invalid refresh token");
|
||||||
|
}
|
||||||
|
refreshToken = jws.readJsonContent(RefreshToken.class);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token", e);
|
||||||
|
}
|
||||||
|
if (refreshToken.isExpired()) {
|
||||||
|
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Refresh token expired");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (refreshToken.getIssuedAt() < realm.getNotBefore()) {
|
||||||
|
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale refresh token");
|
||||||
|
}
|
||||||
|
return refreshToken;
|
||||||
|
}
|
||||||
|
|
||||||
public AccessToken createClientAccessToken(Set<RoleModel> requestedRoles, RealmModel realm, ClientModel client, UserModel user, UserSessionModel session) {
|
public AccessToken createClientAccessToken(Set<RoleModel> requestedRoles, RealmModel realm, ClientModel client, UserModel user, UserSessionModel session) {
|
||||||
AccessToken token = initToken(realm, client, user, session);
|
AccessToken token = initToken(realm, client, user, session);
|
||||||
for (RoleModel role : requestedRoles) {
|
for (RoleModel role : requestedRoles) {
|
||||||
|
|
|
@ -31,6 +31,7 @@ import org.keycloak.models.UserSessionModel;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.representations.AccessToken;
|
||||||
import org.keycloak.representations.AccessTokenResponse;
|
import org.keycloak.representations.AccessTokenResponse;
|
||||||
|
import org.keycloak.representations.RefreshToken;
|
||||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||||
import org.keycloak.ClientConnection;
|
import org.keycloak.ClientConnection;
|
||||||
import org.keycloak.services.managers.AccessCode;
|
import org.keycloak.services.managers.AccessCode;
|
||||||
|
@ -1040,39 +1041,29 @@ public class TokenService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logout user session.
|
* Logout user session. User must be logged in via a session cookie.
|
||||||
*
|
*
|
||||||
* @param sessionState
|
|
||||||
* @param redirectUri
|
* @param redirectUri
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
@Path("logout")
|
@Path("logout")
|
||||||
@GET
|
@GET
|
||||||
@NoCache
|
@NoCache
|
||||||
public Response logout(final @QueryParam("session_state") String sessionState, final @QueryParam("redirect_uri") String redirectUri) {
|
public Response logout(final @QueryParam("redirect_uri") String redirectUri) {
|
||||||
// todo do we care if anybody can trigger this?
|
// todo do we care if anybody can trigger this?
|
||||||
|
|
||||||
audit.event(EventType.LOGOUT);
|
audit.event(EventType.LOGOUT);
|
||||||
if (redirectUri != null) {
|
if (redirectUri != null) {
|
||||||
audit.detail(Details.REDIRECT_URI, redirectUri);
|
audit.detail(Details.REDIRECT_URI, redirectUri);
|
||||||
}
|
}
|
||||||
if (sessionState != null) {
|
|
||||||
audit.session(sessionState);
|
|
||||||
}
|
|
||||||
|
|
||||||
// authenticate identity cookie, but ignore an access token timeout as we're logging out anyways.
|
// authenticate identity cookie, but ignore an access token timeout as we're logging out anyways.
|
||||||
AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, uriInfo, clientConnection, headers, false);
|
AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, uriInfo, clientConnection, headers, false);
|
||||||
if (authResult != null) {
|
if (authResult != null) {
|
||||||
logout(authResult.getSession());
|
logout(authResult.getSession());
|
||||||
} else if (sessionState != null) {
|
|
||||||
UserSessionModel userSession = session.sessions().getUserSession(realm, sessionState);
|
|
||||||
if (userSession != null) {
|
|
||||||
logout(userSession);
|
|
||||||
} else {
|
|
||||||
audit.error(Errors.USER_SESSION_NOT_FOUND);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
audit.error(Errors.USER_NOT_LOGGED_IN);
|
audit.error(Errors.USER_NOT_LOGGED_IN);
|
||||||
|
OAuthFlows oauth = Flows.oauth(session, realm, request, uriInfo, clientConnection, authManager, tokenManager);
|
||||||
|
return oauth.forwardToSecurityFailure("Not logged in.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (redirectUri != null) {
|
if (redirectUri != null) {
|
||||||
|
@ -1088,6 +1079,61 @@ public class TokenService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logout a session via a non-browser invocation. Similar signature to refresh token except there is no grant_type.
|
||||||
|
* You must pass in the refresh token and
|
||||||
|
* authenticate the client if it is not public.
|
||||||
|
*
|
||||||
|
* If the client is a confidential client
|
||||||
|
* you must include the client-id (application name or oauth client name) and secret in an Basic Auth Authorization header.
|
||||||
|
*
|
||||||
|
* If the client is a public client, then you must include a "client_id" form parameter with the app's or oauth client's name.
|
||||||
|
*
|
||||||
|
* returns 204 if successful, 400 if not with a json error response.
|
||||||
|
*
|
||||||
|
* @param authorizationHeader
|
||||||
|
* @param form
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Path("logout")
|
||||||
|
@POST
|
||||||
|
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||||
|
public Response logoutToken(final @HeaderParam(HttpHeaders.AUTHORIZATION) String authorizationHeader,
|
||||||
|
final MultivaluedMap<String, String> form) {
|
||||||
|
logger.info("--> logoutToken");
|
||||||
|
if (!checkSsl()) {
|
||||||
|
throw new NotAcceptableException("HTTPS required");
|
||||||
|
}
|
||||||
|
|
||||||
|
audit.event(EventType.LOGOUT);
|
||||||
|
|
||||||
|
ClientModel client = authorizeClient(authorizationHeader, form, audit);
|
||||||
|
String refreshToken = form.getFirst(OAuth2Constants.REFRESH_TOKEN);
|
||||||
|
if (refreshToken == null) {
|
||||||
|
Map<String, String> error = new HashMap<String, String>();
|
||||||
|
error.put(OAuth2Constants.ERROR, OAuthErrorException.INVALID_REQUEST);
|
||||||
|
error.put(OAuth2Constants.ERROR_DESCRIPTION, "No refresh token");
|
||||||
|
audit.error(Errors.INVALID_TOKEN);
|
||||||
|
logger.error("OAuth Error: no refresh token");
|
||||||
|
return Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
RefreshToken token = tokenManager.verifyRefreshToken(realm, refreshToken);
|
||||||
|
UserSessionModel userSessionModel = session.sessions().getUserSession(realm, token.getSessionState());
|
||||||
|
if (userSessionModel != null) {
|
||||||
|
logout(userSessionModel);
|
||||||
|
}
|
||||||
|
} catch (OAuthErrorException e) {
|
||||||
|
Map<String, String> error = new HashMap<String, String>();
|
||||||
|
error.put(OAuth2Constants.ERROR, e.getError());
|
||||||
|
if (e.getDescription() != null) error.put(OAuth2Constants.ERROR_DESCRIPTION, e.getDescription());
|
||||||
|
audit.error(Errors.INVALID_TOKEN);
|
||||||
|
logger.error("OAuth Error", e);
|
||||||
|
return Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build();
|
||||||
|
}
|
||||||
|
return Cors.add(request, Response.noContent()).auth().allowedOrigins(client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
|
||||||
|
}
|
||||||
|
|
||||||
private void logout(UserSessionModel userSession) {
|
private void logout(UserSessionModel userSession) {
|
||||||
authManager.logout(session, realm, userSession, uriInfo, clientConnection);
|
authManager.logout(session, realm, userSession, uriInfo, clientConnection);
|
||||||
audit.user(userSession.getUser()).session(userSession).success();
|
audit.user(userSession.getUser()).session(userSession).success();
|
||||||
|
|
|
@ -171,11 +171,31 @@ public class OAuthClient {
|
||||||
return new AccessTokenResponse(client.execute(post));
|
return new AccessTokenResponse(client.execute(post));
|
||||||
}
|
}
|
||||||
|
|
||||||
public HttpResponse doLogout(String redirectUri, String sessionState) throws IOException {
|
public HttpResponse doLogout(String refreshToken, String clientSecret) throws IOException {
|
||||||
HttpClient client = new DefaultHttpClient();
|
HttpClient client = new DefaultHttpClient();
|
||||||
HttpGet get = new HttpGet(getLogoutUrl(redirectUri, sessionState));
|
HttpPost post = new HttpPost(getLogoutUrl(null, null));
|
||||||
|
|
||||||
return client.execute(get);
|
List<NameValuePair> parameters = new LinkedList<NameValuePair>();
|
||||||
|
if (refreshToken != null) {
|
||||||
|
parameters.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, refreshToken));
|
||||||
|
}
|
||||||
|
if (clientId != null && clientSecret != null) {
|
||||||
|
String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
|
||||||
|
post.setHeader("Authorization", authorization);
|
||||||
|
}
|
||||||
|
else if (clientId != null) {
|
||||||
|
parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, clientId));
|
||||||
|
}
|
||||||
|
|
||||||
|
UrlEncodedFormEntity formEntity;
|
||||||
|
try {
|
||||||
|
formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
post.setEntity(formEntity);
|
||||||
|
|
||||||
|
return client.execute(post);
|
||||||
}
|
}
|
||||||
|
|
||||||
public AccessTokenResponse doRefreshTokenRequest(String refreshToken, String password) {
|
public AccessTokenResponse doRefreshTokenRequest(String refreshToken, String password) {
|
||||||
|
|
|
@ -214,7 +214,8 @@ public class AdapterTest {
|
||||||
|
|
||||||
|
|
||||||
driver.navigate().to("http://localhost:8081/customer-portal");
|
driver.navigate().to("http://localhost:8081/customer-portal");
|
||||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
String currentUrl = driver.getCurrentUrl();
|
||||||
|
Assert.assertTrue(currentUrl.startsWith(LOGIN_URL));
|
||||||
driver.navigate().to("http://localhost:8081/product-portal");
|
driver.navigate().to("http://localhost:8081/product-portal");
|
||||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||||
|
|
||||||
|
|
30
testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LogoutTest.java
Normal file → Executable file
30
testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LogoutTest.java
Normal file → Executable file
|
@ -124,32 +124,15 @@ public class LogoutTest {
|
||||||
|
|
||||||
String sessionId = events.expectLogin().assertEvent().getSessionId();
|
String sessionId = events.expectLogin().assertEvent().getSessionId();
|
||||||
|
|
||||||
// Login session 2
|
|
||||||
WebDriver driver2 = WebRule.createWebDriver();
|
|
||||||
|
|
||||||
OAuthClient oauth2 = new OAuthClient(driver2);
|
|
||||||
oauth2.doLogin("test-user@localhost", "password");
|
|
||||||
|
|
||||||
String sessionId2 = events.expectLogin().assertEvent().getSessionId();
|
|
||||||
assertNotEquals(sessionId, sessionId2);
|
|
||||||
|
|
||||||
// Check session 1 logged-in
|
// Check session 1 logged-in
|
||||||
oauth.openLoginForm();
|
oauth.openLoginForm();
|
||||||
events.expectLogin().session(sessionId).detail(Details.AUTH_METHOD, "sso").removeDetail(Details.USERNAME).assertEvent();
|
events.expectLogin().session(sessionId).detail(Details.AUTH_METHOD, "sso").removeDetail(Details.USERNAME).assertEvent();
|
||||||
|
|
||||||
// Check session 2 logged-in
|
// Logout session 1 by redirect
|
||||||
oauth2.openLoginForm();
|
|
||||||
events.expectLogin().session(sessionId2).detail(Details.AUTH_METHOD, "sso").removeDetail(Details.USERNAME).assertEvent();
|
|
||||||
|
|
||||||
// Logout session 1 by redirect
|
|
||||||
driver.navigate().to(oauth.getLogoutUrl(AppPage.baseUrl, null));
|
driver.navigate().to(oauth.getLogoutUrl(AppPage.baseUrl, null));
|
||||||
events.expectLogout(sessionId).detail(Details.REDIRECT_URI, AppPage.baseUrl).assertEvent();
|
events.expectLogout(sessionId).detail(Details.REDIRECT_URI, AppPage.baseUrl).assertEvent();
|
||||||
|
|
||||||
// Check session 2 logged-in
|
// Check session 1 not logged-in
|
||||||
oauth2.openLoginForm();
|
|
||||||
events.expectLogin().session(sessionId2).detail(Details.AUTH_METHOD, "sso").removeDetail(Details.USERNAME).assertEvent();
|
|
||||||
|
|
||||||
// Check session 1 not logged-in
|
|
||||||
oauth.openLoginForm();
|
oauth.openLoginForm();
|
||||||
assertEquals(oauth.getLoginFormUrl(), driver.getCurrentUrl());
|
assertEquals(oauth.getLoginFormUrl(), driver.getCurrentUrl());
|
||||||
|
|
||||||
|
@ -157,19 +140,10 @@ public class LogoutTest {
|
||||||
oauth.doLogin("test-user@localhost", "password");
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
String sessionId3 = events.expectLogin().assertEvent().getSessionId();
|
String sessionId3 = events.expectLogin().assertEvent().getSessionId();
|
||||||
assertNotEquals(sessionId, sessionId3);
|
assertNotEquals(sessionId, sessionId3);
|
||||||
assertNotEquals(sessionId2, sessionId3);
|
|
||||||
|
|
||||||
// Logout session 2 by session_state
|
|
||||||
oauth2.doLogout(null, sessionId2);
|
|
||||||
events.expectLogout(sessionId2).removeDetail(Details.REDIRECT_URI).assertEvent();
|
|
||||||
|
|
||||||
// Check session 3 logged-in
|
// Check session 3 logged-in
|
||||||
oauth.openLoginForm();
|
oauth.openLoginForm();
|
||||||
events.expectLogin().session(sessionId3).detail(Details.AUTH_METHOD, "sso").removeDetail(Details.USERNAME).assertEvent();
|
events.expectLogin().session(sessionId3).detail(Details.AUTH_METHOD, "sso").removeDetail(Details.USERNAME).assertEvent();
|
||||||
|
|
||||||
// Check session 2 not logged-in
|
|
||||||
oauth2.openLoginForm();
|
|
||||||
assertEquals(oauth2.getLoginFormUrl(), driver2.getCurrentUrl());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -305,8 +305,13 @@ public class AccessTokenTest {
|
||||||
{
|
{
|
||||||
builder = UriBuilder.fromUri(org.keycloak.testsuite.Constants.AUTH_SERVER_ROOT);
|
builder = UriBuilder.fromUri(org.keycloak.testsuite.Constants.AUTH_SERVER_ROOT);
|
||||||
URI logoutUri = TokenService.logoutUrl(builder).build("test");
|
URI logoutUri = TokenService.logoutUrl(builder).build("test");
|
||||||
Response response = client.target(logoutUri).queryParam("session_state", tokenResponse.getSessionState()).request().get();
|
String header = BasicAuthHelper.createHeader("test-app", "password");
|
||||||
Assert.assertEquals(200, response.getStatus());
|
Form form = new Form();
|
||||||
|
form.param("refresh_token", tokenResponse.getRefreshToken());
|
||||||
|
Response response = client.target(logoutUri).request()
|
||||||
|
.header(HttpHeaders.AUTHORIZATION, header)
|
||||||
|
.post(Entity.form(form));
|
||||||
|
Assert.assertEquals(204, response.getStatus());
|
||||||
response.close();
|
response.close();
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
|
|
|
@ -131,13 +131,9 @@ public class ResourceOwnerPasswordCredentialsGrantTest {
|
||||||
.removeDetail(Details.REDIRECT_URI)
|
.removeDetail(Details.REDIRECT_URI)
|
||||||
.assertEvent();
|
.assertEvent();
|
||||||
|
|
||||||
HttpResponse logoutResponse = oauth.doLogout(null, accessToken.getSessionState());
|
HttpResponse logoutResponse = oauth.doLogout(response.getRefreshToken(), "secret");
|
||||||
assertEquals(200, logoutResponse.getStatusLine().getStatusCode());
|
assertEquals(204, logoutResponse.getStatusLine().getStatusCode());
|
||||||
events.expectLogout(accessToken.getSessionState()).removeDetail(Details.REDIRECT_URI).assertEvent();
|
events.expectLogout(accessToken.getSessionState()).client("resource-owner").removeDetail(Details.REDIRECT_URI).assertEvent();
|
||||||
|
|
||||||
logoutResponse = oauth.doLogout(null, accessToken.getSessionState());
|
|
||||||
assertEquals(200, logoutResponse.getStatusLine().getStatusCode());
|
|
||||||
events.expectLogout(accessToken.getSessionState()).user((String) null).removeDetail(Details.REDIRECT_URI).error(Errors.USER_SESSION_NOT_FOUND).assertEvent();
|
|
||||||
|
|
||||||
response = oauth.doRefreshTokenRequest(response.getRefreshToken(), "secret");
|
response = oauth.doRefreshTokenRequest(response.getRefreshToken(), "secret");
|
||||||
assertEquals(400, response.getStatusCode());
|
assertEquals(400, response.getStatusCode());
|
||||||
|
|
Loading…
Reference in a new issue