Add OIDC Back-Channel Logout Support
Closes gh-12570
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.security.oauth2.client.oidc.authentication.logout;
|
||||
|
||||
import java.net.URL;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.security.oauth2.core.ClaimAccessor;
|
||||
|
||||
/**
|
||||
* A {@link ClaimAccessor} for the "claims" that can be returned in OIDC Logout
|
||||
* Tokens
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.2
|
||||
* @see OidcLogoutToken
|
||||
* @see <a target="_blank" href=
|
||||
* "https://openid.net/specs/openid-connect-backchannel-1_0.html#LogoutToken">OIDC
|
||||
* Back-Channel Logout Token</a>
|
||||
*/
|
||||
public interface LogoutTokenClaimAccessor extends ClaimAccessor {
|
||||
|
||||
/**
|
||||
* Returns the Issuer identifier {@code (iss)}.
|
||||
* @return the Issuer identifier
|
||||
*/
|
||||
default URL getIssuer() {
|
||||
return this.getClaimAsURL(LogoutTokenClaimNames.ISS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Subject identifier {@code (sub)}.
|
||||
* @return the Subject identifier
|
||||
*/
|
||||
default String getSubject() {
|
||||
return this.getClaimAsString(LogoutTokenClaimNames.SUB);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Audience(s) {@code (aud)} that this ID Token is intended for.
|
||||
* @return the Audience(s) that this ID Token is intended for
|
||||
*/
|
||||
default List<String> getAudience() {
|
||||
return this.getClaimAsStringList(LogoutTokenClaimNames.AUD);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the time at which the ID Token was issued {@code (iat)}.
|
||||
* @return the time at which the ID Token was issued
|
||||
*/
|
||||
default Instant getIssuedAt() {
|
||||
return this.getClaimAsInstant(LogoutTokenClaimNames.IAT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Map} that identifies this token as a logout token
|
||||
* @return the identifying {@link Map}
|
||||
*/
|
||||
default Map<String, Object> getEvents() {
|
||||
return getClaimAsMap(LogoutTokenClaimNames.EVENTS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code String} value {@code (sid)} representing the OIDC Provider session
|
||||
* @return the value representing the OIDC Provider session
|
||||
*/
|
||||
default String getSessionId() {
|
||||
return getClaimAsString(LogoutTokenClaimNames.SID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JWT ID {@code (jti)} claim which provides a unique identifier for the
|
||||
* JWT.
|
||||
* @return the JWT ID claim which provides a unique identifier for the JWT
|
||||
*/
|
||||
default String getId() {
|
||||
return this.getClaimAsString(LogoutTokenClaimNames.JTI);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.security.oauth2.client.oidc.authentication.logout;
|
||||
|
||||
/**
|
||||
* The names of the "claims" defined by the OpenID Back-Channel Logout 1.0
|
||||
* specification that can be returned in a Logout Token.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.2
|
||||
* @see OidcLogoutToken
|
||||
* @see <a target="_blank" href=
|
||||
* "https://openid.net/specs/openid-connect-backchannel-1_0.html#LogoutToken">OIDC
|
||||
* Back-Channel Logout Token</a>
|
||||
*/
|
||||
public final class LogoutTokenClaimNames {
|
||||
|
||||
/**
|
||||
* {@code jti} - the JTI identifier
|
||||
*/
|
||||
public static final String JTI = "jti";
|
||||
|
||||
/**
|
||||
* {@code iss} - the Issuer identifier
|
||||
*/
|
||||
public static final String ISS = "iss";
|
||||
|
||||
/**
|
||||
* {@code sub} - the Subject identifier
|
||||
*/
|
||||
public static final String SUB = "sub";
|
||||
|
||||
/**
|
||||
* {@code aud} - the Audience(s) that the ID Token is intended for
|
||||
*/
|
||||
public static final String AUD = "aud";
|
||||
|
||||
/**
|
||||
* {@code iat} - the time at which the ID Token was issued
|
||||
*/
|
||||
public static final String IAT = "iat";
|
||||
|
||||
/**
|
||||
* {@code events} - a JSON object that identifies this token as a logout token
|
||||
*/
|
||||
public static final String EVENTS = "events";
|
||||
|
||||
/**
|
||||
* {@code sid} - the session id for the OIDC provider
|
||||
*/
|
||||
public static final String SID = "sid";
|
||||
|
||||
private LogoutTokenClaimNames() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.security.oauth2.client.oidc.authentication.logout;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
|
||||
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An implementation of an {@link AbstractOAuth2Token} representing an OpenID Backchannel
|
||||
* Logout Token.
|
||||
*
|
||||
* <p>
|
||||
* The {@code OidcLogoutToken} is a security token that contains "claims" about
|
||||
* terminating sessions for a given OIDC Provider session id or End User.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.2
|
||||
* @see AbstractOAuth2Token
|
||||
* @see LogoutTokenClaimAccessor
|
||||
* @see <a target="_blank" href=
|
||||
* "https://openid.net/specs/openid-connect-backchannel-1_0.html#LogoutToken">Logout
|
||||
* Token</a>
|
||||
*/
|
||||
public class OidcLogoutToken extends AbstractOAuth2Token implements LogoutTokenClaimAccessor {
|
||||
|
||||
private static final String BACKCHANNEL_LOGOUT_TOKEN_EVENT_NAME = "http://schemas.openid.net/event/backchannel-logout";
|
||||
|
||||
private final Map<String, Object> claims;
|
||||
|
||||
/**
|
||||
* Constructs a {@link OidcLogoutToken} using the provided parameters.
|
||||
* @param tokenValue the Logout Token value
|
||||
* @param issuedAt the time at which the Logout Token was issued {@code (iat)}
|
||||
* @param claims the claims about the logout statement
|
||||
*/
|
||||
OidcLogoutToken(String tokenValue, Instant issuedAt, Map<String, Object> claims) {
|
||||
super(tokenValue, issuedAt, Instant.MAX);
|
||||
this.claims = Collections.unmodifiableMap(claims);
|
||||
Assert.notNull(claims, "claims must not be null");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getClaims() {
|
||||
return this.claims;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link OidcLogoutToken.Builder} based on the given token value
|
||||
* @param tokenValue the token value to use
|
||||
* @return the {@link OidcLogoutToken.Builder} for further configuration
|
||||
*/
|
||||
public static Builder withTokenValue(String tokenValue) {
|
||||
return new Builder(tokenValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link OidcLogoutToken}s
|
||||
*
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
public static final class Builder {
|
||||
|
||||
private String tokenValue;
|
||||
|
||||
private final Map<String, Object> claims = new LinkedHashMap<>();
|
||||
|
||||
private Builder(String tokenValue) {
|
||||
this.tokenValue = tokenValue;
|
||||
this.claims.put(LogoutTokenClaimNames.EVENTS,
|
||||
Collections.singletonMap(BACKCHANNEL_LOGOUT_TOKEN_EVENT_NAME, Collections.emptyMap()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this token value in the resulting {@link OidcLogoutToken}
|
||||
* @param tokenValue The token value to use
|
||||
* @return the {@link Builder} for further configurations
|
||||
*/
|
||||
public Builder tokenValue(String tokenValue) {
|
||||
this.tokenValue = tokenValue;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this claim in the resulting {@link OidcLogoutToken}
|
||||
* @param name The claim name
|
||||
* @param value The claim value
|
||||
* @return the {@link Builder} for further configurations
|
||||
*/
|
||||
public Builder claim(String name, Object value) {
|
||||
this.claims.put(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides access to every {@link #claim(String, Object)} declared so far with
|
||||
* the possibility to add, replace, or remove.
|
||||
* @param claimsConsumer the consumer
|
||||
* @return the {@link Builder} for further configurations
|
||||
*/
|
||||
public Builder claims(Consumer<Map<String, Object>> claimsConsumer) {
|
||||
claimsConsumer.accept(this.claims);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this audience in the resulting {@link OidcLogoutToken}
|
||||
* @param audience The audience(s) to use
|
||||
* @return the {@link Builder} for further configurations
|
||||
*/
|
||||
public Builder audience(Collection<String> audience) {
|
||||
return claim(LogoutTokenClaimNames.AUD, audience);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this issued-at timestamp in the resulting {@link OidcLogoutToken}
|
||||
* @param issuedAt The issued-at timestamp to use
|
||||
* @return the {@link Builder} for further configurations
|
||||
*/
|
||||
public Builder issuedAt(Instant issuedAt) {
|
||||
return claim(LogoutTokenClaimNames.IAT, issuedAt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this issuer in the resulting {@link OidcLogoutToken}
|
||||
* @param issuer The issuer to use
|
||||
* @return the {@link Builder} for further configurations
|
||||
*/
|
||||
public Builder issuer(String issuer) {
|
||||
return claim(LogoutTokenClaimNames.ISS, issuer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this id to identify the resulting {@link OidcLogoutToken}
|
||||
* @param jti The unique identifier to use
|
||||
* @return the {@link Builder} for further configurations
|
||||
*/
|
||||
public Builder jti(String jti) {
|
||||
return claim(LogoutTokenClaimNames.JTI, jti);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this subject in the resulting {@link OidcLogoutToken}
|
||||
* @param subject The subject to use
|
||||
* @return the {@link Builder} for further configurations
|
||||
*/
|
||||
public Builder subject(String subject) {
|
||||
return claim(LogoutTokenClaimNames.SUB, subject);
|
||||
}
|
||||
|
||||
/**
|
||||
* A JSON object that identifies this token as a logout token
|
||||
* @param events The JSON object to use
|
||||
* @return the {@link Builder} for further configurations
|
||||
*/
|
||||
public Builder events(Map<String, Object> events) {
|
||||
return claim(LogoutTokenClaimNames.EVENTS, events);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this session id to correlate the OIDC Provider session
|
||||
* @param sessionId The session id to use
|
||||
* @return the {@link Builder} for further configurations
|
||||
*/
|
||||
public Builder sessionId(String sessionId) {
|
||||
return claim(LogoutTokenClaimNames.SID, sessionId);
|
||||
}
|
||||
|
||||
public OidcLogoutToken build() {
|
||||
Assert.notNull(this.claims.get(LogoutTokenClaimNames.ISS), "issuer must not be null");
|
||||
Assert.isInstanceOf(Collection.class, this.claims.get(LogoutTokenClaimNames.AUD),
|
||||
"audience must be a collection");
|
||||
Assert.notEmpty((Collection<?>) this.claims.get(LogoutTokenClaimNames.AUD), "audience must not be empty");
|
||||
Assert.notNull(this.claims.get(LogoutTokenClaimNames.JTI), "jti must not be null");
|
||||
Assert.isTrue(hasLogoutTokenIdentifyingMember(),
|
||||
"logout token must contain an events claim that contains a member called " + "'"
|
||||
+ BACKCHANNEL_LOGOUT_TOKEN_EVENT_NAME + "' whose value is an empty Map");
|
||||
Assert.isNull(this.claims.get("nonce"), "logout token must not contain a nonce claim");
|
||||
Instant iat = toInstant(this.claims.get(IdTokenClaimNames.IAT));
|
||||
return new OidcLogoutToken(this.tokenValue, iat, this.claims);
|
||||
}
|
||||
|
||||
private boolean hasLogoutTokenIdentifyingMember() {
|
||||
if (!(this.claims.get(LogoutTokenClaimNames.EVENTS) instanceof Map<?, ?> events)) {
|
||||
return false;
|
||||
}
|
||||
if (!(events.get(BACKCHANNEL_LOGOUT_TOKEN_EVENT_NAME) instanceof Map<?, ?> object)) {
|
||||
return false;
|
||||
}
|
||||
return object.isEmpty();
|
||||
}
|
||||
|
||||
private Instant toInstant(Object timestamp) {
|
||||
if (timestamp != null) {
|
||||
Assert.isInstanceOf(Instant.class, timestamp, "timestamps must be of type Instant");
|
||||
}
|
||||
return (Instant) timestamp;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.security.oauth2.client.oidc.server.session;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
|
||||
import org.springframework.security.oauth2.client.oidc.session.InMemoryOidcSessionRegistry;
|
||||
import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation;
|
||||
|
||||
/**
|
||||
* An in-memory implementation of
|
||||
* {@link org.springframework.security.oauth2.client.oidc.server.session.ReactiveOidcSessionRegistry}
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.2
|
||||
*/
|
||||
public final class InMemoryReactiveOidcSessionRegistry implements ReactiveOidcSessionRegistry {
|
||||
|
||||
private final InMemoryOidcSessionRegistry delegate = new InMemoryOidcSessionRegistry();
|
||||
|
||||
@Override
|
||||
public Mono<Void> saveSessionInformation(OidcSessionInformation info) {
|
||||
this.delegate.saveSessionInformation(info);
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<OidcSessionInformation> removeSessionInformation(String clientSessionId) {
|
||||
return Mono.justOrEmpty(this.delegate.removeSessionInformation(clientSessionId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<OidcSessionInformation> removeSessionInformation(OidcLogoutToken token) {
|
||||
return Flux.fromIterable(this.delegate.removeSessionInformation(token));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.security.oauth2.client.oidc.server.session;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
|
||||
import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation;
|
||||
|
||||
/**
|
||||
* A registry to record the tie between the OIDC Provider session and the Client session.
|
||||
* This is handy when a provider makes a logout request that indicates the OIDC Provider
|
||||
* session or the End User.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.2
|
||||
* @see <a target="_blank" href=
|
||||
* "https://openid.net/specs/openid-connect-backchannel-1_0.html#LogoutToken">Logout
|
||||
* Token</a>
|
||||
*/
|
||||
public interface ReactiveOidcSessionRegistry {
|
||||
|
||||
/**
|
||||
* Register a OIDC Provider session with the provided client session. Generally
|
||||
* speaking, the client session should be the session tied to the current login.
|
||||
* @param info the {@link OidcSessionInformation} to use
|
||||
*/
|
||||
Mono<Void> saveSessionInformation(OidcSessionInformation info);
|
||||
|
||||
/**
|
||||
* Deregister the OIDC Provider session tied to the provided client session. Generally
|
||||
* speaking, the client session should be the session tied to the current logout.
|
||||
* @param clientSessionId the client session
|
||||
* @return any found {@link OidcSessionInformation}, could be {@code null}
|
||||
*/
|
||||
Mono<OidcSessionInformation> removeSessionInformation(String clientSessionId);
|
||||
|
||||
/**
|
||||
* Deregister the OIDC Provider sessions referenced by the provided OIDC Logout Token
|
||||
* by its session id or its subject. Note that the issuer and audience should also
|
||||
* match the corresponding values found in each {@link OidcSessionInformation}
|
||||
* returned.
|
||||
* @param logoutToken the {@link OidcLogoutToken}
|
||||
* @return any found {@link OidcSessionInformation}s, could be empty
|
||||
*/
|
||||
Flux<OidcSessionInformation> removeSessionInformation(OidcLogoutToken logoutToken);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.security.oauth2.client.oidc.session;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.logout.LogoutTokenClaimNames;
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
|
||||
|
||||
/**
|
||||
* An in-memory implementation of {@link OidcSessionRegistry}
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.2
|
||||
*/
|
||||
public final class InMemoryOidcSessionRegistry implements OidcSessionRegistry {
|
||||
|
||||
private final Log logger = LogFactory.getLog(InMemoryOidcSessionRegistry.class);
|
||||
|
||||
private final Map<String, OidcSessionInformation> sessions = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void saveSessionInformation(OidcSessionInformation info) {
|
||||
this.sessions.put(info.getSessionId(), info);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OidcSessionInformation removeSessionInformation(String clientSessionId) {
|
||||
OidcSessionInformation information = this.sessions.remove(clientSessionId);
|
||||
if (information != null) {
|
||||
this.logger.trace("Removed client session");
|
||||
}
|
||||
return information;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<OidcSessionInformation> removeSessionInformation(OidcLogoutToken token) {
|
||||
List<String> audience = token.getAudience();
|
||||
String issuer = token.getIssuer().toString();
|
||||
String subject = token.getSubject();
|
||||
String providerSessionId = token.getSessionId();
|
||||
Predicate<OidcSessionInformation> matcher = (providerSessionId != null)
|
||||
? sessionIdMatcher(audience, issuer, providerSessionId) : subjectMatcher(audience, issuer, subject);
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
String message = "Looking up sessions by issuer [%s] and %s [%s]";
|
||||
if (providerSessionId != null) {
|
||||
this.logger.trace(String.format(message, issuer, LogoutTokenClaimNames.SID, providerSessionId));
|
||||
}
|
||||
else {
|
||||
this.logger.trace(String.format(message, issuer, LogoutTokenClaimNames.SUB, subject));
|
||||
}
|
||||
}
|
||||
int size = this.sessions.size();
|
||||
Set<OidcSessionInformation> infos = new HashSet<>();
|
||||
this.sessions.values().removeIf((info) -> {
|
||||
boolean result = matcher.test(info);
|
||||
if (result) {
|
||||
infos.add(info);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
if (infos.isEmpty()) {
|
||||
this.logger.debug("Failed to remove any sessions since none matched");
|
||||
}
|
||||
else if (this.logger.isTraceEnabled()) {
|
||||
String message = "Found and removed %d session(s) from mapping of %d session(s)";
|
||||
this.logger.trace(String.format(message, infos.size(), size));
|
||||
}
|
||||
return infos;
|
||||
}
|
||||
|
||||
private static Predicate<OidcSessionInformation> sessionIdMatcher(List<String> audience, String issuer,
|
||||
String sessionId) {
|
||||
return (session) -> {
|
||||
List<String> thatAudience = session.getPrincipal().getAudience();
|
||||
String thatIssuer = session.getPrincipal().getIssuer().toString();
|
||||
String thatSessionId = session.getPrincipal().getClaimAsString(LogoutTokenClaimNames.SID);
|
||||
if (thatAudience == null) {
|
||||
return false;
|
||||
}
|
||||
return !Collections.disjoint(audience, thatAudience) && issuer.equals(thatIssuer)
|
||||
&& sessionId.equals(thatSessionId);
|
||||
};
|
||||
}
|
||||
|
||||
private static Predicate<OidcSessionInformation> subjectMatcher(List<String> audience, String issuer,
|
||||
String subject) {
|
||||
return (session) -> {
|
||||
List<String> thatAudience = session.getPrincipal().getAudience();
|
||||
String thatIssuer = session.getPrincipal().getIssuer().toString();
|
||||
String thatSubject = session.getPrincipal().getSubject();
|
||||
if (thatAudience == null) {
|
||||
return false;
|
||||
}
|
||||
return !Collections.disjoint(audience, thatAudience) && issuer.equals(thatIssuer)
|
||||
&& subject.equals(thatSubject);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.security.oauth2.client.oidc.session;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.security.core.session.SessionInformation;
|
||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||
|
||||
/**
|
||||
* A {@link SessionInformation} extension that enforces the principal be of type
|
||||
* {@link OidcUser}.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.2
|
||||
*/
|
||||
public class OidcSessionInformation extends SessionInformation {
|
||||
|
||||
private final Map<String, String> authorities;
|
||||
|
||||
/**
|
||||
* Construct an {@link OidcSessionInformation}
|
||||
* @param sessionId the Client's session id
|
||||
* @param authorities any material that authorizes operating on the session
|
||||
* @param user the OIDC Provider's session and end user
|
||||
*/
|
||||
public OidcSessionInformation(String sessionId, Map<String, String> authorities, OidcUser user) {
|
||||
super(user, sessionId, new Date());
|
||||
this.authorities = (authorities != null) ? new LinkedHashMap<>(authorities) : Collections.emptyMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Any material needed to authorize operations on this session
|
||||
* @return the {@link Map} of credentials
|
||||
*/
|
||||
public Map<String, String> getAuthorities() {
|
||||
return this.authorities;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public OidcUser getPrincipal() {
|
||||
return (OidcUser) super.getPrincipal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy this {@link OidcSessionInformation}, using a new session identifier
|
||||
* @param sessionId the new session identifier to use
|
||||
* @return a new {@link OidcSessionInformation} instance
|
||||
*/
|
||||
public OidcSessionInformation withSessionId(String sessionId) {
|
||||
return new OidcSessionInformation(sessionId, getAuthorities(), getPrincipal());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.security.oauth2.client.oidc.session;
|
||||
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
|
||||
|
||||
/**
|
||||
* A registry to record the tie between the OIDC Provider session and the Client session.
|
||||
* This is handy when a provider makes a logout request that indicates the OIDC Provider
|
||||
* session or the End User.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.2
|
||||
* @see <a target="_blank" href=
|
||||
* "https://openid.net/specs/openid-connect-backchannel-1_0.html#LogoutToken">Logout
|
||||
* Token</a>
|
||||
*/
|
||||
public interface OidcSessionRegistry {
|
||||
|
||||
/**
|
||||
* Register a OIDC Provider session with the provided client session. Generally
|
||||
* speaking, the client session should be the session tied to the current login.
|
||||
* @param info the {@link OidcSessionInformation} to use
|
||||
*/
|
||||
void saveSessionInformation(OidcSessionInformation info);
|
||||
|
||||
/**
|
||||
* Deregister the OIDC Provider session tied to the provided client session. Generally
|
||||
* speaking, the client session should be the session tied to the current logout.
|
||||
* @param clientSessionId the client session
|
||||
* @return any found {@link OidcSessionInformation}, could be {@code null}
|
||||
*/
|
||||
OidcSessionInformation removeSessionInformation(String clientSessionId);
|
||||
|
||||
/**
|
||||
* Deregister the OIDC Provider sessions referenced by the provided OIDC Logout Token
|
||||
* by its session id or its subject. Note that the issuer and audience should also
|
||||
* match the corresponding values found in each {@link OidcSessionInformation}
|
||||
* returned.
|
||||
* @param logoutToken the {@link OidcLogoutToken}
|
||||
* @return any found {@link OidcSessionInformation}s, could be empty
|
||||
*/
|
||||
Iterable<OidcSessionInformation> removeSessionInformation(OidcLogoutToken logoutToken);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.security.oauth2.client.oidc.authentication.logout;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||
|
||||
public final class TestOidcLogoutTokens {
|
||||
|
||||
public static OidcLogoutToken.Builder withUser(OidcUser user) {
|
||||
OidcLogoutToken.Builder builder = OidcLogoutToken.withTokenValue("token")
|
||||
.audience(Collections.singleton("client-id")).issuedAt(Instant.now())
|
||||
.issuer(user.getIssuer().toString()).jti("id").subject(user.getSubject());
|
||||
if (user.hasClaim(LogoutTokenClaimNames.SID)) {
|
||||
builder.sessionId(user.getClaimAsString(LogoutTokenClaimNames.SID));
|
||||
}
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static OidcLogoutToken.Builder withSessionId(String issuer, String sessionId) {
|
||||
return OidcLogoutToken.withTokenValue("token").audience(Collections.singleton("client-id"))
|
||||
.issuedAt(Instant.now()).issuer(issuer).jti("id").sessionId(sessionId);
|
||||
}
|
||||
|
||||
public static OidcLogoutToken.Builder withSubject(String issuer, String subject) {
|
||||
return OidcLogoutToken.withTokenValue("token").audience(Collections.singleton("client-id"))
|
||||
.issuedAt(Instant.now()).issuer(issuer).jti("id").subject(subject);
|
||||
}
|
||||
|
||||
private TestOidcLogoutTokens() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.security.oauth2.client.oidc.session;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.logout.TestOidcLogoutTokens;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||
import org.springframework.security.oauth2.core.oidc.TestOidcIdTokens;
|
||||
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
|
||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link InMemoryOidcSessionRegistry}
|
||||
*/
|
||||
public class InMemoryOidcSessionRegistryTests {
|
||||
|
||||
@Test
|
||||
public void registerWhenDefaultsThenStoresSessionInformation() {
|
||||
InMemoryOidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry();
|
||||
String sessionId = "client";
|
||||
OidcSessionInformation info = TestOidcSessionInformations.create(sessionId);
|
||||
sessionRegistry.saveSessionInformation(info);
|
||||
OidcLogoutToken logoutToken = TestOidcLogoutTokens.withUser(info.getPrincipal()).build();
|
||||
Iterable<OidcSessionInformation> infos = sessionRegistry.removeSessionInformation(logoutToken);
|
||||
assertThat(infos).containsExactly(info);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void registerWhenIdTokenHasSessionIdThenStoresSessionInformation() {
|
||||
InMemoryOidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry();
|
||||
OidcIdToken idToken = TestOidcIdTokens.idToken().claim("sid", "provider").build();
|
||||
OidcUser user = new DefaultOidcUser(AuthorityUtils.NO_AUTHORITIES, idToken);
|
||||
OidcSessionInformation info = TestOidcSessionInformations.create("client", user);
|
||||
sessionRegistry.saveSessionInformation(info);
|
||||
OidcLogoutToken logoutToken = TestOidcLogoutTokens.withSessionId(idToken.getIssuer().toString(), "provider")
|
||||
.build();
|
||||
Iterable<OidcSessionInformation> infos = sessionRegistry.removeSessionInformation(logoutToken);
|
||||
assertThat(infos).containsExactly(info);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unregisterWhenMultipleSessionsThenRemovesAllMatching() {
|
||||
InMemoryOidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry();
|
||||
OidcIdToken idToken = TestOidcIdTokens.idToken().claim("sid", "providerOne").subject("otheruser").build();
|
||||
OidcUser user = new DefaultOidcUser(AuthorityUtils.NO_AUTHORITIES, idToken);
|
||||
OidcSessionInformation oneSession = TestOidcSessionInformations.create("clientOne", user);
|
||||
sessionRegistry.saveSessionInformation(oneSession);
|
||||
idToken = TestOidcIdTokens.idToken().claim("sid", "providerTwo").build();
|
||||
user = new DefaultOidcUser(AuthorityUtils.NO_AUTHORITIES, idToken);
|
||||
OidcSessionInformation twoSession = TestOidcSessionInformations.create("clientTwo", user);
|
||||
sessionRegistry.saveSessionInformation(twoSession);
|
||||
idToken = TestOidcIdTokens.idToken().claim("sid", "providerThree").build();
|
||||
user = new DefaultOidcUser(AuthorityUtils.NO_AUTHORITIES, idToken);
|
||||
OidcSessionInformation threeSession = TestOidcSessionInformations.create("clientThree", user);
|
||||
sessionRegistry.saveSessionInformation(threeSession);
|
||||
OidcLogoutToken logoutToken = TestOidcLogoutTokens
|
||||
.withSubject(idToken.getIssuer().toString(), idToken.getSubject()).build();
|
||||
Iterable<OidcSessionInformation> infos = sessionRegistry.removeSessionInformation(logoutToken);
|
||||
assertThat(infos).containsExactlyInAnyOrder(twoSession, threeSession);
|
||||
logoutToken = TestOidcLogoutTokens.withSubject(idToken.getIssuer().toString(), "otheruser").build();
|
||||
infos = sessionRegistry.removeSessionInformation(logoutToken);
|
||||
assertThat(infos).containsExactly(oneSession);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unregisterWhenNoSessionsThenEmptyList() {
|
||||
InMemoryOidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry();
|
||||
OidcIdToken idToken = TestOidcIdTokens.idToken().claim("sid", "provider").build();
|
||||
OidcUser user = new DefaultOidcUser(AuthorityUtils.NO_AUTHORITIES, idToken);
|
||||
OidcSessionInformation info = TestOidcSessionInformations.create("client", user);
|
||||
sessionRegistry.saveSessionInformation(info);
|
||||
OidcLogoutToken logoutToken = TestOidcLogoutTokens.withSessionId(idToken.getIssuer().toString(), "wrong")
|
||||
.build();
|
||||
Iterable<?> infos = sessionRegistry.removeSessionInformation(logoutToken);
|
||||
assertThat(infos).isNotNull();
|
||||
assertThat(infos).isEmpty();
|
||||
logoutToken = TestOidcLogoutTokens.withSessionId("https://wrong", "provider").build();
|
||||
infos = sessionRegistry.removeSessionInformation(logoutToken);
|
||||
assertThat(infos).isNotNull();
|
||||
assertThat(infos).isEmpty();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.security.oauth2.client.oidc.session;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||
import org.springframework.security.oauth2.core.oidc.user.TestOidcUsers;
|
||||
|
||||
/**
|
||||
* Sample {@link OidcSessionInformation} instances
|
||||
*/
|
||||
public final class TestOidcSessionInformations {
|
||||
|
||||
public static OidcSessionInformation create() {
|
||||
return create("sessionId");
|
||||
}
|
||||
|
||||
public static OidcSessionInformation create(String sessionId) {
|
||||
return create(sessionId, TestOidcUsers.create());
|
||||
}
|
||||
|
||||
public static OidcSessionInformation create(String sessionId, OidcUser user) {
|
||||
return new OidcSessionInformation(sessionId, Map.of("_csrf", "token"), user);
|
||||
}
|
||||
|
||||
private TestOidcSessionInformations() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user