Take the resulting {@link Collection} and prepend the "SCOPE_" keyword to each
* element, adding as {@link GrantedAuthority}s.
*
+ *
+ * An {@link OpaqueTokenIntrospector} is responsible for retrieving token attributes from
+ * an authorization server.
+ *
+ * An {@link OpaqueTokenAuthenticationConverter} is responsible for turning a successful
+ * introspection result into an {@link Authentication} instance (which may include mapping
+ * {@link GrantedAuthority}s from token attributes or retrieving from another source).
*
* @author Josh Cummings
+ * @author Jerome Wacongne <ch4mp@c4-soft.com>
* @since 5.2
* @see AuthenticationProvider
*/
@@ -68,6 +77,8 @@ public final class OpaqueTokenAuthenticationProvider implements AuthenticationPr
private final OpaqueTokenIntrospector introspector;
+ private OpaqueTokenAuthenticationConverter authenticationConverter = OpaqueTokenAuthenticationProvider::convert;
+
/**
* Creates a {@code OpaqueTokenAuthenticationProvider} with the provided parameters
* @param introspector The {@link OpaqueTokenIntrospector} to use
@@ -80,7 +91,11 @@ public final class OpaqueTokenAuthenticationProvider implements AuthenticationPr
/**
* Introspect and validate the opaque
* Bearer
- * Token.
+ * Token and then delegates {@link Authentication} instantiation to
+ * {@link OpaqueTokenAuthenticationConverter}.
+ *
+ * If created Authentication is instance of {@link AbstractAuthenticationToken} and
+ * details are null, then introspection result details are used.
* @param authentication the authentication request object.
* @return A successful authentication
* @throws AuthenticationException if authentication failed for some reason
@@ -92,8 +107,16 @@ public final class OpaqueTokenAuthenticationProvider implements AuthenticationPr
}
BearerTokenAuthenticationToken bearer = (BearerTokenAuthenticationToken) authentication;
OAuth2AuthenticatedPrincipal principal = getOAuth2AuthenticatedPrincipal(bearer);
- AbstractAuthenticationToken result = convert(principal, bearer.getToken());
- result.setDetails(bearer.getDetails());
+ Authentication result = this.authenticationConverter.convert(bearer.getToken(), principal);
+ if (result == null) {
+ return null;
+ }
+ if (AbstractAuthenticationToken.class.isAssignableFrom(result.getClass())) {
+ final AbstractAuthenticationToken auth = (AbstractAuthenticationToken) result;
+ if (auth.getDetails() == null) {
+ auth.setDetails(bearer.getDetails());
+ }
+ }
this.logger.debug("Authenticated token");
return result;
}
@@ -116,11 +139,32 @@ public final class OpaqueTokenAuthenticationProvider implements AuthenticationPr
return BearerTokenAuthenticationToken.class.isAssignableFrom(authentication);
}
- private AbstractAuthenticationToken convert(OAuth2AuthenticatedPrincipal principal, String token) {
- Instant iat = principal.getAttribute(OAuth2TokenIntrospectionClaimNames.IAT);
- Instant exp = principal.getAttribute(OAuth2TokenIntrospectionClaimNames.EXP);
- OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, token, iat, exp);
- return new BearerTokenAuthentication(principal, accessToken, principal.getAuthorities());
+ /**
+ * Default {@link OpaqueTokenAuthenticationConverter}.
+ * @param introspectedToken the bearer string that was successfully introspected
+ * @param authenticatedPrincipal the successful introspection output
+ * @return a {@link BearerTokenAuthentication}
+ */
+ static BearerTokenAuthentication convert(String introspectedToken,
+ OAuth2AuthenticatedPrincipal authenticatedPrincipal) {
+ Instant iat = authenticatedPrincipal.getAttribute(OAuth2TokenIntrospectionClaimNames.IAT);
+ Instant exp = authenticatedPrincipal.getAttribute(OAuth2TokenIntrospectionClaimNames.EXP);
+ OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, introspectedToken,
+ iat, exp);
+ return new BearerTokenAuthentication(authenticatedPrincipal, accessToken,
+ authenticatedPrincipal.getAuthorities());
+ }
+
+ /**
+ * Provide with a custom bean to turn successful introspection result into an
+ * {@link Authentication} instance of your choice. By default,
+ * {@link BearerTokenAuthentication} will be built.
+ * @param authenticationConverter the converter to use
+ * @since 5.8
+ */
+ public void setAuthenticationConverter(OpaqueTokenAuthenticationConverter authenticationConverter) {
+ Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
+ this.authenticationConverter = authenticationConverter;
}
}
diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenReactiveAuthenticationManager.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenReactiveAuthenticationManager.java
index 79c271e923..1736c6efb5 100644
--- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenReactiveAuthenticationManager.java
+++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenReactiveAuthenticationManager.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2021 the original author or authors.
+ * 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.
@@ -16,22 +16,21 @@
package org.springframework.security.oauth2.server.resource.authentication;
-import java.time.Instant;
-import java.util.Collection;
-
import reactor.core.publisher.Mono;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
-import org.springframework.security.oauth2.core.OAuth2AccessToken;
-import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
+import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException;
import org.springframework.security.oauth2.server.resource.introspection.BadOpaqueTokenException;
import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionException;
+import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenAuthenticationConverter;
+import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector;
import org.springframework.util.Assert;
@@ -46,16 +45,16 @@ import org.springframework.util.Assert;
* verifying an opaque access token, returning its attributes set as part of the
* {@link Authentication} statement.
*
- * Scopes are translated into {@link GrantedAuthority}s according to the following
- * algorithm:
- *
- * - If there is a "scope" attribute, then convert to a {@link Collection} of
- * {@link String}s.
- *
- Take the resulting {@link Collection} and prepend the "SCOPE_" keyword to each
- * element, adding as {@link GrantedAuthority}s.
- *
+ * A {@link ReactiveOpaqueTokenIntrospector} is responsible for retrieving token
+ * attributes from an authorization server.
+ *
+ * A {@link ReactiveOpaqueTokenAuthenticationConverter} is responsible for turning a
+ * successful introspection result into an {@link Authentication} instance (which may
+ * include mapping {@link GrantedAuthority}s from token attributes or retrieving from
+ * another source).
*
* @author Josh Cummings
+ * @author Jerome Wacongne <ch4mp@c4-soft.com>
* @since 5.2
* @see ReactiveAuthenticationManager
*/
@@ -63,6 +62,8 @@ public class OpaqueTokenReactiveAuthenticationManager implements ReactiveAuthent
private final ReactiveOpaqueTokenIntrospector introspector;
+ private ReactiveOpaqueTokenAuthenticationConverter authenticationConverter = OpaqueTokenReactiveAuthenticationManager::convert;
+
/**
* Creates a {@code OpaqueTokenReactiveAuthenticationManager} with the provided
* parameters
@@ -73,6 +74,17 @@ public class OpaqueTokenReactiveAuthenticationManager implements ReactiveAuthent
this.introspector = introspector;
}
+ /**
+ * Introspect and validate the opaque
+ * Bearer
+ * Token and then delegates {@link Authentication} instantiation to
+ * {@link ReactiveOpaqueTokenAuthenticationConverter}.
+ *
+ * If created Authentication is instance of {@link AbstractAuthenticationToken} and
+ * details are null, then introspection result details are used.
+ * @param authentication the authentication request object.
+ * @return A successful authentication
+ */
@Override
public Mono authenticate(Authentication authentication) {
// @formatter:off
@@ -80,21 +92,14 @@ public class OpaqueTokenReactiveAuthenticationManager implements ReactiveAuthent
.filter(BearerTokenAuthenticationToken.class::isInstance)
.cast(BearerTokenAuthenticationToken.class)
.map(BearerTokenAuthenticationToken::getToken)
- .flatMap(this::authenticate)
- .cast(Authentication.class);
+ .flatMap(this::authenticate);
// @formatter:on
}
- private Mono authenticate(String token) {
+ private Mono authenticate(String token) {
// @formatter:off
return this.introspector.introspect(token)
- .map((principal) -> {
- Instant iat = principal.getAttribute(OAuth2TokenIntrospectionClaimNames.IAT);
- Instant exp = principal.getAttribute(OAuth2TokenIntrospectionClaimNames.EXP);
- // construct token
- OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, token, iat, exp);
- return new BearerTokenAuthentication(principal, accessToken, principal.getAuthorities());
- })
+ .flatMap((principal) -> this.authenticationConverter.convert(token, principal))
.onErrorMap(OAuth2IntrospectionException.class, this::onError);
// @formatter:on
}
@@ -106,4 +111,27 @@ public class OpaqueTokenReactiveAuthenticationManager implements ReactiveAuthent
return new AuthenticationServiceException(ex.getMessage(), ex);
}
+ /**
+ * Default {@link ReactiveOpaqueTokenAuthenticationConverter}.
+ * @param introspectedToken the bearer string that was successfully introspected
+ * @param authenticatedPrincipal the successful introspection output
+ * @return an async wrapper of default {@link OpaqueTokenAuthenticationConverter}
+ * result
+ */
+ static Mono convert(String introspectedToken, OAuth2AuthenticatedPrincipal authenticatedPrincipal) {
+ return Mono.just(OpaqueTokenAuthenticationProvider.convert(introspectedToken, authenticatedPrincipal));
+ }
+
+ /**
+ * Provide with a custom bean to turn successful introspection result into an
+ * {@link Authentication} instance of your choice. By default,
+ * {@link BearerTokenAuthentication} will be built.
+ * @param authenticationConverter the converter to use
+ * @since 5.8
+ */
+ public void setAuthenticationConverter(ReactiveOpaqueTokenAuthenticationConverter authenticationConverter) {
+ Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
+ this.authenticationConverter = authenticationConverter;
+ }
+
}
diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/OpaqueTokenAuthenticationConverter.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/OpaqueTokenAuthenticationConverter.java
new file mode 100644
index 0000000000..d0d00f5caf
--- /dev/null
+++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/OpaqueTokenAuthenticationConverter.java
@@ -0,0 +1,39 @@
+/*
+ * 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.server.resource.introspection;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
+
+/**
+ * Convert a successful introspection result into an authentication result.
+ *
+ * @author Jerome Wacongne <ch4mp@c4-soft.com>
+ * @since 5.8
+ */
+@FunctionalInterface
+public interface OpaqueTokenAuthenticationConverter {
+
+ /**
+ * Converts a successful introspection result into an authentication result.
+ * @param introspectedToken the bearer token used to perform token introspection
+ * @param authenticatedPrincipal the result of token introspection
+ * @return an {@link Authentication} instance
+ */
+ Authentication convert(String introspectedToken, OAuth2AuthenticatedPrincipal authenticatedPrincipal);
+
+}
diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/ReactiveOpaqueTokenAuthenticationConverter.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/ReactiveOpaqueTokenAuthenticationConverter.java
new file mode 100644
index 0000000000..9e9a63c567
--- /dev/null
+++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/ReactiveOpaqueTokenAuthenticationConverter.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2002-2021 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.server.resource.introspection;
+
+import reactor.core.publisher.Mono;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
+
+/**
+ * Convert a successful introspection result into an authentication result.
+ *
+ * @author Jerome Wacongne <ch4mp@c4-soft.com>
+ * @since 5.8
+ */
+@FunctionalInterface
+public interface ReactiveOpaqueTokenAuthenticationConverter {
+
+ /**
+ * Converts a successful introspection result into an authentication result.
+ * @param introspectedToken the bearer token used to perform token introspection
+ * @param authenticatedPrincipal the result of token introspection
+ * @return an {@link Authentication} instance
+ */
+ Mono convert(String introspectedToken, OAuth2AuthenticatedPrincipal authenticatedPrincipal);
+
+}
diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProviderTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProviderTests.java
index 69eaecb9b2..c1c96f0ef6 100644
--- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProviderTests.java
+++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProviderTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2021 the original author or authors.
+ * 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.
@@ -25,6 +25,7 @@ import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
@@ -32,6 +33,7 @@ import org.springframework.security.oauth2.core.TestOAuth2AuthenticatedPrincipal
import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionAuthenticatedPrincipal;
import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionException;
+import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
import static org.assertj.core.api.Assertions.assertThat;
@@ -40,6 +42,8 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
/**
* Tests for {@link OpaqueTokenAuthenticationProvider}
@@ -114,4 +118,33 @@ public class OpaqueTokenAuthenticationProviderTests {
// @formatter:on
}
+ @Test
+ public void setAuthenticationConverterWhenNullThenThrowsIllegalArgumentException() {
+ OpaqueTokenIntrospector introspector = mock(OpaqueTokenIntrospector.class);
+ OpaqueTokenAuthenticationProvider provider = new OpaqueTokenAuthenticationProvider(introspector);
+ // @formatter:off
+ assertThatIllegalArgumentException()
+ .isThrownBy(() -> provider.setAuthenticationConverter(null))
+ .withMessage("authenticationConverter cannot be null");
+ // @formatter:on
+ }
+
+ @Test
+ public void authenticateWhenCustomAuthenticationConverterThenUses() {
+ OpaqueTokenIntrospector introspector = mock(OpaqueTokenIntrospector.class);
+ OAuth2AuthenticatedPrincipal principal = TestOAuth2AuthenticatedPrincipals.active();
+ given(introspector.introspect(any())).willReturn(principal);
+ OpaqueTokenAuthenticationProvider provider = new OpaqueTokenAuthenticationProvider(introspector);
+ OpaqueTokenAuthenticationConverter authenticationConverter = mock(OpaqueTokenAuthenticationConverter.class);
+ given(authenticationConverter.convert(any(), any(OAuth2AuthenticatedPrincipal.class)))
+ .willReturn(new TestingAuthenticationToken(principal, null, Collections.emptyList()));
+ provider.setAuthenticationConverter(authenticationConverter);
+
+ Authentication result = provider.authenticate(new BearerTokenAuthenticationToken("token"));
+ assertThat(result).isNotNull();
+ verify(introspector).introspect("token");
+ verify(authenticationConverter).convert("token", principal);
+ verifyNoMoreInteractions(introspector, authenticationConverter);
+ }
+
}
diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenReactiveAuthenticationManagerTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenReactiveAuthenticationManagerTests.java
index 7f671a3730..f6d8fdbbd7 100644
--- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenReactiveAuthenticationManagerTests.java
+++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenReactiveAuthenticationManagerTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2021 the original author or authors.
+ * 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.
@@ -26,6 +26,7 @@ import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
@@ -33,6 +34,7 @@ import org.springframework.security.oauth2.core.TestOAuth2AuthenticatedPrincipal
import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionAuthenticatedPrincipal;
import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionException;
+import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector;
import static org.assertj.core.api.Assertions.assertThat;
@@ -41,6 +43,8 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
/**
* Tests for {@link OpaqueTokenReactiveAuthenticationManager}
@@ -112,4 +116,34 @@ public class OpaqueTokenReactiveAuthenticationManagerTests {
// @formatter:on
}
+ @Test
+ public void setAuthenticationConverterWhenNullThenThrowsIllegalArgumentException() {
+ ReactiveOpaqueTokenIntrospector introspector = mock(ReactiveOpaqueTokenIntrospector.class);
+ OpaqueTokenReactiveAuthenticationManager provider = new OpaqueTokenReactiveAuthenticationManager(introspector);
+ // @formatter:off
+ assertThatIllegalArgumentException()
+ .isThrownBy(() -> provider.setAuthenticationConverter(null))
+ .withMessage("authenticationConverter cannot be null");
+ // @formatter:on
+ }
+
+ @Test
+ public void authenticateWhenCustomAuthenticationConverterThenUses() {
+ ReactiveOpaqueTokenIntrospector introspector = mock(ReactiveOpaqueTokenIntrospector.class);
+ OAuth2AuthenticatedPrincipal principal = TestOAuth2AuthenticatedPrincipals.active();
+ given(introspector.introspect(any())).willReturn(Mono.just(principal));
+ OpaqueTokenReactiveAuthenticationManager provider = new OpaqueTokenReactiveAuthenticationManager(introspector);
+ ReactiveOpaqueTokenAuthenticationConverter authenticationConverter = mock(
+ ReactiveOpaqueTokenAuthenticationConverter.class);
+ given(authenticationConverter.convert(any(), any(OAuth2AuthenticatedPrincipal.class)))
+ .willReturn(Mono.just(new TestingAuthenticationToken(principal, null, Collections.emptyList())));
+ provider.setAuthenticationConverter(authenticationConverter);
+
+ Authentication result = provider.authenticate(new BearerTokenAuthenticationToken("token")).block();
+ assertThat(result).isNotNull();
+ verify(introspector).introspect("token");
+ verify(authenticationConverter).convert("token", principal);
+ verifyNoMoreInteractions(introspector, authenticationConverter);
+ }
+
}