Add OIDC Back-Channel Logout Support

Closes gh-12570
This commit is contained in:
Josh Cummings
2023-01-17 17:25:16 -07:00
parent 1461c0f648
commit cb33fd7850
51 changed files with 5397 additions and 114 deletions

View File

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

View File

@@ -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 &quot;claims&quot; 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() {
}
}

View File

@@ -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 &quot;claims&quot; 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;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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