oidc logout profile
This commit is contained in:
parent
97d5f4aafc
commit
f546358d66
6 changed files with 74 additions and 16 deletions
|
@ -71,13 +71,11 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
public static final String SAML_SERVER_SIGNATURE = "saml.server.signature";
|
public static final String SAML_SERVER_SIGNATURE = "saml.server.signature";
|
||||||
public static final String SAML_ASSERTION_SIGNATURE = "saml.assertion.signature";
|
public static final String SAML_ASSERTION_SIGNATURE = "saml.assertion.signature";
|
||||||
public static final String SAML_AUTHNSTATEMENT = "saml.authnstatement";
|
public static final String SAML_AUTHNSTATEMENT = "saml.authnstatement";
|
||||||
public static final String SAML_MULTIVALUED_ROLES = "saml.multivalued.roles";
|
|
||||||
public static final String SAML_SIGNATURE_ALGORITHM = "saml.signature.algorithm";
|
public static final String SAML_SIGNATURE_ALGORITHM = "saml.signature.algorithm";
|
||||||
public static final String SAML_ENCRYPT = "saml.encrypt";
|
public static final String SAML_ENCRYPT = "saml.encrypt";
|
||||||
public static final String SAML_FORCE_POST_BINDING = "saml.force.post.binding";
|
public static final String SAML_FORCE_POST_BINDING = "saml.force.post.binding";
|
||||||
public static final String SAML_REQUEST_ID = "SAML_REQUEST_ID";
|
public static final String SAML_REQUEST_ID = "SAML_REQUEST_ID";
|
||||||
public static final String SAML_LOGOUT_BINDING = "saml.logout.binding";
|
public static final String SAML_LOGOUT_BINDING = "saml.logout.binding";
|
||||||
public static final String SAML_LOGOUT_ISSUER = "saml.logout.issuer";
|
|
||||||
public static final String SAML_LOGOUT_REQUEST_ID = "SAML_LOGOUT_REQUEST_ID";
|
public static final String SAML_LOGOUT_REQUEST_ID = "SAML_LOGOUT_REQUEST_ID";
|
||||||
public static final String SAML_LOGOUT_RELAY_STATE = "SAML_LOGOUT_RELAY_STATE";
|
public static final String SAML_LOGOUT_RELAY_STATE = "SAML_LOGOUT_RELAY_STATE";
|
||||||
public static final String SAML_LOGOUT_BINDING_URI = "SAML_LOGOUT_BINDING_URI";
|
public static final String SAML_LOGOUT_BINDING_URI = "SAML_LOGOUT_BINDING_URI";
|
||||||
|
|
|
@ -49,6 +49,7 @@ public class OIDCLoginProtocol implements LoginProtocol {
|
||||||
|
|
||||||
public static final String LOGIN_PROTOCOL = "openid-connect";
|
public static final String LOGIN_PROTOCOL = "openid-connect";
|
||||||
public static final String STATE_PARAM = "state";
|
public static final String STATE_PARAM = "state";
|
||||||
|
public static final String LOGOUT_STATE_PARAM = "OIDC_LOGOUT_STATE_PARAM";
|
||||||
public static final String SCOPE_PARAM = "scope";
|
public static final String SCOPE_PARAM = "scope";
|
||||||
public static final String CODE_PARAM = "code";
|
public static final String CODE_PARAM = "code";
|
||||||
public static final String RESPONSE_TYPE_PARAM = "response_type";
|
public static final String RESPONSE_TYPE_PARAM = "response_type";
|
||||||
|
@ -182,6 +183,7 @@ public class OIDCLoginProtocol implements LoginProtocol {
|
||||||
@Override
|
@Override
|
||||||
public Response finishLogout(UserSessionModel userSession) {
|
public Response finishLogout(UserSessionModel userSession) {
|
||||||
String redirectUri = userSession.getNote(OIDCLoginProtocol.LOGOUT_REDIRECT_URI);
|
String redirectUri = userSession.getNote(OIDCLoginProtocol.LOGOUT_REDIRECT_URI);
|
||||||
|
String state = userSession.getNote(OIDCLoginProtocol.LOGOUT_STATE_PARAM);
|
||||||
event.event(EventType.LOGOUT);
|
event.event(EventType.LOGOUT);
|
||||||
if (redirectUri != null) {
|
if (redirectUri != null) {
|
||||||
event.detail(Details.REDIRECT_URI, redirectUri);
|
event.detail(Details.REDIRECT_URI, redirectUri);
|
||||||
|
@ -190,7 +192,9 @@ public class OIDCLoginProtocol implements LoginProtocol {
|
||||||
|
|
||||||
|
|
||||||
if (redirectUri != null) {
|
if (redirectUri != null) {
|
||||||
return Response.status(302).location(UriBuilder.fromUri(redirectUri).build()).build();
|
UriBuilder uriBuilder = UriBuilder.fromUri(redirectUri);
|
||||||
|
if (state != null) uriBuilder.queryParam(STATE_PARAM, state);
|
||||||
|
return Response.status(302).location(uriBuilder.build()).build();
|
||||||
} else {
|
} else {
|
||||||
return Response.ok().build();
|
return Response.ok().build();
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,7 +86,7 @@ public class TokenManager {
|
||||||
|
|
||||||
UserSessionModel userSession = session.sessions().getUserSession(realm, oldToken.getSessionState());
|
UserSessionModel userSession = session.sessions().getUserSession(realm, oldToken.getSessionState());
|
||||||
if (!AuthenticationManager.isSessionValid(realm, userSession)) {
|
if (!AuthenticationManager.isSessionValid(realm, userSession)) {
|
||||||
AuthenticationManager.logout(session, realm, userSession, uriInfo, connection, headers);
|
AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, connection, headers);
|
||||||
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Session not active", "Session not active");
|
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Session not active", "Session not active");
|
||||||
}
|
}
|
||||||
ClientSessionModel clientSession = null;
|
ClientSessionModel clientSession = null;
|
||||||
|
@ -166,6 +166,26 @@ public class TokenManager {
|
||||||
}
|
}
|
||||||
return refreshToken;
|
return refreshToken;
|
||||||
}
|
}
|
||||||
|
public IDToken verifyIDToken(RealmModel realm, String encodedIDToken) throws OAuthErrorException {
|
||||||
|
JWSInput jws = new JWSInput(encodedIDToken);
|
||||||
|
IDToken idToken = null;
|
||||||
|
try {
|
||||||
|
if (!RSAProvider.verify(jws, realm.getPublicKey())) {
|
||||||
|
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token");
|
||||||
|
}
|
||||||
|
idToken = jws.readJsonContent(IDToken.class);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token", e);
|
||||||
|
}
|
||||||
|
if (idToken.isExpired()) {
|
||||||
|
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Refresh token expired");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (idToken.getIssuedAt() < realm.getNotBefore()) {
|
||||||
|
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale refresh token");
|
||||||
|
}
|
||||||
|
return idToken;
|
||||||
|
}
|
||||||
|
|
||||||
public AccessToken createClientAccessToken(KeycloakSession session, Set<RoleModel> requestedRoles, RealmModel realm, ClientModel client, UserModel user, UserSessionModel userSession, ClientSessionModel clientSession) {
|
public AccessToken createClientAccessToken(KeycloakSession session, Set<RoleModel> requestedRoles, RealmModel realm, ClientModel client, UserModel user, UserSessionModel userSession, ClientSessionModel clientSession) {
|
||||||
AccessToken token = initToken(realm, client, user, userSession, clientSession);
|
AccessToken token = initToken(realm, client, user, userSession, clientSession);
|
||||||
|
@ -405,6 +425,7 @@ public class TokenManager {
|
||||||
idToken.issuedNow();
|
idToken.issuedNow();
|
||||||
idToken.issuedFor(accessToken.getIssuedFor());
|
idToken.issuedFor(accessToken.getIssuedFor());
|
||||||
idToken.issuer(accessToken.getIssuer());
|
idToken.issuer(accessToken.getIssuer());
|
||||||
|
idToken.setSessionState(accessToken.getSessionState());
|
||||||
if (realm.getAccessTokenLifespan() > 0) {
|
if (realm.getAccessTokenLifespan() > 0) {
|
||||||
idToken.expiration(Time.currentTime() + realm.getAccessTokenLifespan());
|
idToken.expiration(Time.currentTime() + realm.getAccessTokenLifespan());
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package org.keycloak.protocol.oidc.endpoints;
|
package org.keycloak.protocol.oidc.endpoints;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||||
import org.jboss.resteasy.spi.HttpRequest;
|
import org.jboss.resteasy.spi.HttpRequest;
|
||||||
import org.keycloak.ClientConnection;
|
import org.keycloak.ClientConnection;
|
||||||
|
@ -18,6 +19,7 @@ import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.protocol.oidc.TokenManager;
|
import org.keycloak.protocol.oidc.TokenManager;
|
||||||
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
|
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
|
||||||
import org.keycloak.protocol.oidc.utils.RedirectUtils;
|
import org.keycloak.protocol.oidc.utils.RedirectUtils;
|
||||||
|
import org.keycloak.representations.IDToken;
|
||||||
import org.keycloak.representations.RefreshToken;
|
import org.keycloak.representations.RefreshToken;
|
||||||
import org.keycloak.services.ErrorResponseException;
|
import org.keycloak.services.ErrorResponseException;
|
||||||
import org.keycloak.services.managers.AuthenticationManager;
|
import org.keycloak.services.managers.AuthenticationManager;
|
||||||
|
@ -42,6 +44,7 @@ import javax.ws.rs.core.UriInfo;
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
*/
|
*/
|
||||||
public class LogoutEndpoint {
|
public class LogoutEndpoint {
|
||||||
|
protected static Logger logger = Logger.getLogger(LogoutEndpoint.class);
|
||||||
|
|
||||||
@Context
|
@Context
|
||||||
private KeycloakSession session;
|
private KeycloakSession session;
|
||||||
|
@ -78,28 +81,60 @@ public class LogoutEndpoint {
|
||||||
*/
|
*/
|
||||||
@GET
|
@GET
|
||||||
@NoCache
|
@NoCache
|
||||||
public Response logout(@QueryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM) String redirectUri) {
|
public Response logout(@QueryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM) String redirectUri, // deprecated
|
||||||
if (redirectUri != null) {
|
@QueryParam("id_token_hint") String encodedIdToken,
|
||||||
String validatedUri = RedirectUtils.verifyRealmRedirectUri(uriInfo, redirectUri, realm);
|
@QueryParam("post_logout_redirect_uri") String postLogoutRedirectUri,
|
||||||
|
@QueryParam("state") String state) {
|
||||||
|
String redirect = postLogoutRedirectUri != null ? postLogoutRedirectUri : redirectUri;
|
||||||
|
|
||||||
|
if (redirect != null) {
|
||||||
|
String validatedUri = RedirectUtils.verifyRealmRedirectUri(uriInfo, redirect, realm);
|
||||||
if (validatedUri == null) {
|
if (validatedUri == null) {
|
||||||
event.event(EventType.LOGOUT);
|
event.event(EventType.LOGOUT);
|
||||||
event.detail(Details.REDIRECT_URI, redirectUri);
|
event.detail(Details.REDIRECT_URI, redirect);
|
||||||
event.error(Errors.INVALID_REDIRECT_URI);
|
event.error(Errors.INVALID_REDIRECT_URI);
|
||||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_REDIRECT_URI);
|
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_REDIRECT_URI);
|
||||||
}
|
}
|
||||||
redirectUri = validatedUri;
|
redirect = validatedUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
UserSessionModel userSession = null;
|
||||||
|
boolean error = false;
|
||||||
|
if (encodedIdToken != null) {
|
||||||
|
try {
|
||||||
|
IDToken idToken = tokenManager.verifyIDToken(realm, encodedIdToken);
|
||||||
|
userSession = session.sessions().getUserSession(realm, idToken.getSessionState());
|
||||||
|
if (userSession == null) {
|
||||||
|
error = true;
|
||||||
|
}
|
||||||
|
} catch (OAuthErrorException e) {
|
||||||
|
error = true;
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
event.event(EventType.LOGOUT);
|
||||||
|
event.error(Errors.INVALID_TOKEN);
|
||||||
|
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.SESSION_NOT_ACTIVE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
if (redirectUri != null) authResult.getSession().setNote(OIDCLoginProtocol.LOGOUT_REDIRECT_URI, redirectUri);
|
userSession = userSession != null ? userSession : authResult.getSession();
|
||||||
authResult.getSession().setNote(AuthenticationManager.KEYCLOAK_LOGOUT_PROTOCOL, OIDCLoginProtocol.LOGIN_PROTOCOL);
|
if (redirectUri != null) userSession.setNote(OIDCLoginProtocol.LOGOUT_REDIRECT_URI, redirect);
|
||||||
|
if (state != null) userSession.setNote(OIDCLoginProtocol.LOGOUT_STATE_PARAM, state);
|
||||||
|
userSession.setNote(AuthenticationManager.KEYCLOAK_LOGOUT_PROTOCOL, OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||||
return AuthenticationManager.browserLogout(session, realm, authResult.getSession(), uriInfo, clientConnection, headers);
|
return AuthenticationManager.browserLogout(session, realm, authResult.getSession(), uriInfo, clientConnection, headers);
|
||||||
|
} else if (userSession != null) { // non browser logout
|
||||||
|
event.event(EventType.LOGOUT);
|
||||||
|
authManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers);
|
||||||
|
event.user(userSession.getUser()).session(userSession).success();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (redirectUri != null) {
|
if (redirectUri != null) {
|
||||||
return Response.status(302).location(UriBuilder.fromUri(redirectUri).build()).build();
|
UriBuilder uriBuilder = UriBuilder.fromUri(redirect);
|
||||||
|
if (state != null) uriBuilder.queryParam(OIDCLoginProtocol.STATE_PARAM, state);
|
||||||
|
return Response.status(302).location(uriBuilder.build()).build();
|
||||||
} else {
|
} else {
|
||||||
return Response.ok().build();
|
return Response.ok().build();
|
||||||
}
|
}
|
||||||
|
@ -149,7 +184,7 @@ public class LogoutEndpoint {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void logout(UserSessionModel userSession) {
|
private void logout(UserSessionModel userSession) {
|
||||||
authManager.logout(session, realm, userSession, uriInfo, clientConnection, headers);
|
authManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers);
|
||||||
event.user(userSession.getUser()).session(userSession).success();
|
event.user(userSession.getUser()).session(userSession).success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -84,7 +84,7 @@ public class AuthenticationManager {
|
||||||
return userSession != null && userSession.getLastSessionRefresh() + realm.getSsoSessionIdleTimeout() > currentTime && max > currentTime;
|
return userSession != null && userSession.getLastSessionRefresh() + realm.getSsoSessionIdleTimeout() > currentTime && max > currentTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void logout(KeycloakSession session, RealmModel realm, UserSessionModel userSession, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers) {
|
public static void backchannelLogout(KeycloakSession session, RealmModel realm, UserSessionModel userSession, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers) {
|
||||||
if (userSession == null) return;
|
if (userSession == null) return;
|
||||||
UserModel user = userSession.getUser();
|
UserModel user = userSession.getUser();
|
||||||
userSession.setState(UserSessionModel.State.LOGGING_OUT);
|
userSession.setState(UserSessionModel.State.LOGGING_OUT);
|
||||||
|
@ -461,7 +461,7 @@ public class AuthenticationManager {
|
||||||
|
|
||||||
UserSessionModel userSession = session.sessions().getUserSession(realm, token.getSessionState());
|
UserSessionModel userSession = session.sessions().getUserSession(realm, token.getSessionState());
|
||||||
if (!isSessionValid(realm, userSession)) {
|
if (!isSessionValid(realm, userSession)) {
|
||||||
if (userSession != null) logout(session, realm, userSession, uriInfo, connection, headers);
|
if (userSession != null) backchannelLogout(session, realm, userSession, uriInfo, connection, headers);
|
||||||
logger.debug("User session not active");
|
logger.debug("User session not active");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -586,7 +586,7 @@ public class LoginActionsService {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!AuthenticationManager.isSessionValid(realm, userSession)) {
|
if (!AuthenticationManager.isSessionValid(realm, userSession)) {
|
||||||
AuthenticationManager.logout(session, realm, userSession, uriInfo, clientConnection, headers);
|
AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers);
|
||||||
event.error(Errors.INVALID_CODE);
|
event.error(Errors.INVALID_CODE);
|
||||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.SESSION_NOT_ACTIVE);
|
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.SESSION_NOT_ACTIVE);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue