oidc logout profile

This commit is contained in:
Bill Burke 2015-03-22 12:45:36 -04:00
parent 97d5f4aafc
commit f546358d66
6 changed files with 74 additions and 16 deletions

View file

@ -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";

View file

@ -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();
} }

View file

@ -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());
} }

View file

@ -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();
} }

View file

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

View file

@ -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);
} }