Optimize LogoutEndpoint.backchannelLogout endpoint
closes #32683 Signed-off-by: mposolda <mposolda@gmail.com>
This commit is contained in:
parent
4b95b42590
commit
866101e72e
4 changed files with 81 additions and 23 deletions
|
@ -21,4 +21,8 @@ public enum LogoutTokenValidationCode {
|
||||||
public String getErrorMessage() {
|
public String getErrorMessage() {
|
||||||
return errorMessage;
|
return errorMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LogoutTokenValidationContext toCtx() {
|
||||||
|
return new LogoutTokenValidationContext(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
*
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.protocol.oidc;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import org.keycloak.broker.oidc.OIDCIdentityProvider;
|
||||||
|
import org.keycloak.representations.LogoutToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class LogoutTokenValidationContext {
|
||||||
|
|
||||||
|
private final LogoutToken logoutToken;
|
||||||
|
private final LogoutTokenValidationCode status;
|
||||||
|
private final List<OIDCIdentityProvider> validIdentityProviders;
|
||||||
|
|
||||||
|
LogoutTokenValidationContext(LogoutTokenValidationCode status) {
|
||||||
|
this(status, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
LogoutTokenValidationContext(LogoutTokenValidationCode status, LogoutToken logoutToken, List<OIDCIdentityProvider> validIdentityProviders) {
|
||||||
|
this.logoutToken = logoutToken;
|
||||||
|
this.status = status;
|
||||||
|
this.validIdentityProviders = validIdentityProviders;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LogoutToken getLogoutToken() {
|
||||||
|
return logoutToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LogoutTokenValidationCode getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<OIDCIdentityProvider> getValidIdentityProviders() {
|
||||||
|
return validIdentityProviders;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1454,45 +1454,45 @@ public class TokenManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public LogoutTokenValidationCode verifyLogoutToken(KeycloakSession session, String encodedLogoutToken) {
|
public LogoutTokenValidationContext verifyLogoutToken(KeycloakSession session, String encodedLogoutToken) {
|
||||||
Optional<LogoutToken> logoutTokenOptional = toLogoutToken(encodedLogoutToken);
|
Optional<LogoutToken> logoutTokenOptional = toLogoutToken(encodedLogoutToken);
|
||||||
if (logoutTokenOptional.isEmpty()) {
|
if (logoutTokenOptional.isEmpty()) {
|
||||||
return LogoutTokenValidationCode.DECODE_TOKEN_FAILED;
|
return LogoutTokenValidationCode.DECODE_TOKEN_FAILED.toCtx();
|
||||||
}
|
}
|
||||||
|
|
||||||
LogoutToken logoutToken = logoutTokenOptional.get();
|
LogoutToken logoutToken = logoutTokenOptional.get();
|
||||||
List<OIDCIdentityProvider> identityProviders = getOIDCIdentityProviders(logoutToken, session).toList();
|
List<OIDCIdentityProvider> identityProviders = getOIDCIdentityProviders(logoutToken, session).toList();
|
||||||
if (identityProviders.isEmpty()) {
|
if (identityProviders.isEmpty()) {
|
||||||
return LogoutTokenValidationCode.COULD_NOT_FIND_IDP;
|
return LogoutTokenValidationCode.COULD_NOT_FIND_IDP.toCtx();
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<OIDCIdentityProvider> validOidcIdentityProviders =
|
List<OIDCIdentityProvider> validOidcIdentityProviders =
|
||||||
validateLogoutTokenAgainstIdpProvider(identityProviders.stream(), encodedLogoutToken);
|
validateLogoutTokenAgainstIdpProvider(identityProviders.stream(), encodedLogoutToken).toList();
|
||||||
if (validOidcIdentityProviders.findAny().isEmpty()) {
|
if (validOidcIdentityProviders.isEmpty()) {
|
||||||
return LogoutTokenValidationCode.TOKEN_VERIFICATION_WITH_IDP_FAILED;
|
return LogoutTokenValidationCode.TOKEN_VERIFICATION_WITH_IDP_FAILED.toCtx();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (logoutToken.getSubject() == null && logoutToken.getSid() == null) {
|
if (logoutToken.getSubject() == null && logoutToken.getSid() == null) {
|
||||||
return LogoutTokenValidationCode.MISSING_SID_OR_SUBJECT;
|
return LogoutTokenValidationCode.MISSING_SID_OR_SUBJECT.toCtx();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!checkLogoutTokenForEvents(logoutToken)) {
|
if (!checkLogoutTokenForEvents(logoutToken)) {
|
||||||
return LogoutTokenValidationCode.BACKCHANNEL_LOGOUT_EVENT_MISSING;
|
return LogoutTokenValidationCode.BACKCHANNEL_LOGOUT_EVENT_MISSING.toCtx();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (logoutToken.getOtherClaims().get(NONCE) != null) {
|
if (logoutToken.getOtherClaims().get(NONCE) != null) {
|
||||||
return LogoutTokenValidationCode.NONCE_CLAIM_IN_TOKEN;
|
return LogoutTokenValidationCode.NONCE_CLAIM_IN_TOKEN.toCtx();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (logoutToken.getId() == null) {
|
if (logoutToken.getId() == null) {
|
||||||
return LogoutTokenValidationCode.LOGOUT_TOKEN_ID_MISSING;
|
return LogoutTokenValidationCode.LOGOUT_TOKEN_ID_MISSING.toCtx();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (logoutToken.getIat() == null) {
|
if (logoutToken.getIat() == null) {
|
||||||
return LogoutTokenValidationCode.MISSING_IAT_CLAIM;
|
return LogoutTokenValidationCode.MISSING_IAT_CLAIM.toCtx();
|
||||||
}
|
}
|
||||||
|
|
||||||
return LogoutTokenValidationCode.VALIDATION_SUCCESS;
|
return new LogoutTokenValidationContext(LogoutTokenValidationCode.VALIDATION_SUCCESS, logoutToken, validOidcIdentityProviders);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<LogoutToken> toLogoutToken(String encodedLogoutToken) {
|
public Optional<LogoutToken> toLogoutToken(String encodedLogoutToken) {
|
||||||
|
@ -1505,11 +1505,6 @@ public class TokenManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public Stream<OIDCIdentityProvider> getValidOIDCIdentityProvidersForBackchannelLogout(KeycloakSession session, String encodedLogoutToken, LogoutToken logoutToken) {
|
|
||||||
return validateLogoutTokenAgainstIdpProvider(getOIDCIdentityProviders(logoutToken, session), encodedLogoutToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public Stream<OIDCIdentityProvider> validateLogoutTokenAgainstIdpProvider(Stream<OIDCIdentityProvider> oidcIdps, String encodedLogoutToken) {
|
public Stream<OIDCIdentityProvider> validateLogoutTokenAgainstIdpProvider(Stream<OIDCIdentityProvider> oidcIdps, String encodedLogoutToken) {
|
||||||
return oidcIdps
|
return oidcIdps
|
||||||
.filter(oidcIdp -> {
|
.filter(oidcIdp -> {
|
||||||
|
|
|
@ -46,6 +46,7 @@ import org.keycloak.models.UserSessionModel;
|
||||||
import org.keycloak.models.utils.SystemClientUtil;
|
import org.keycloak.models.utils.SystemClientUtil;
|
||||||
import org.keycloak.protocol.oidc.BackchannelLogoutResponse;
|
import org.keycloak.protocol.oidc.BackchannelLogoutResponse;
|
||||||
import org.keycloak.protocol.oidc.LogoutTokenValidationCode;
|
import org.keycloak.protocol.oidc.LogoutTokenValidationCode;
|
||||||
|
import org.keycloak.protocol.oidc.LogoutTokenValidationContext;
|
||||||
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.protocol.oidc.TokenManager;
|
import org.keycloak.protocol.oidc.TokenManager;
|
||||||
|
@ -554,18 +555,18 @@ public class LogoutEndpoint {
|
||||||
Response.Status.BAD_REQUEST);
|
Response.Status.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
LogoutTokenValidationCode validationCode = tokenManager.verifyLogoutToken(session, encodedLogoutToken);
|
LogoutTokenValidationContext validationCtx = tokenManager.verifyLogoutToken(session, encodedLogoutToken);
|
||||||
if (!validationCode.equals(LogoutTokenValidationCode.VALIDATION_SUCCESS)) {
|
if (!validationCtx.getStatus().equals(LogoutTokenValidationCode.VALIDATION_SUCCESS)) {
|
||||||
String errorMessage = validationCode.getErrorMessage();
|
String errorMessage = validationCtx.getStatus().getErrorMessage();
|
||||||
event.detail(Details.REASON, errorMessage);
|
event.detail(Details.REASON, errorMessage);
|
||||||
event.error(Errors.INVALID_TOKEN);
|
event.error(Errors.INVALID_TOKEN);
|
||||||
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, errorMessage,
|
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, errorMessage,
|
||||||
Response.Status.BAD_REQUEST);
|
Response.Status.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
LogoutToken logoutToken = tokenManager.toLogoutToken(encodedLogoutToken).get();
|
LogoutToken logoutToken = validationCtx.getLogoutToken();
|
||||||
|
|
||||||
Stream<String> identityProviderAliases = tokenManager.getValidOIDCIdentityProvidersForBackchannelLogout(session, encodedLogoutToken, logoutToken)
|
Stream<String> identityProviderAliases = validationCtx.getValidIdentityProviders().stream()
|
||||||
.map(idp -> idp.getConfig().getAlias());
|
.map(idp -> idp.getConfig().getAlias());
|
||||||
|
|
||||||
boolean logoutOfflineSessions = Boolean.parseBoolean(logoutToken.getEvents()
|
boolean logoutOfflineSessions = Boolean.parseBoolean(logoutToken.getEvents()
|
||||||
|
|
Loading…
Reference in a new issue