You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@openwebbeans.apache.org by rm...@apache.org on 2019/02/19 22:03:37 UTC

svn commit: r1853921 - in /openwebbeans/meecrowave/trunk/meecrowave-oauth2: ./ src/main/java/org/apache/meecrowave/oauth2/configuration/ src/main/resources/META-INF/ src/test/java/org/apache/meecrowave/oauth2/

Author: rmannibucau
Date: Tue Feb 19 22:03:37 2019
New Revision: 1853921

URL: http://svn.apache.org/viewvc?rev=1853921&view=rev
Log:
MEECROWAVE-183 ensure issuer can be propagated to the jwt token

Added:
    openwebbeans/meecrowave/trunk/meecrowave-oauth2/src/main/resources/META-INF/beans.xml
Modified:
    openwebbeans/meecrowave/trunk/meecrowave-oauth2/pom.xml
    openwebbeans/meecrowave/trunk/meecrowave-oauth2/src/main/java/org/apache/meecrowave/oauth2/configuration/OAuth2Configurer.java
    openwebbeans/meecrowave/trunk/meecrowave-oauth2/src/main/java/org/apache/meecrowave/oauth2/configuration/OAuth2Options.java
    openwebbeans/meecrowave/trunk/meecrowave-oauth2/src/test/java/org/apache/meecrowave/oauth2/Keystores.java
    openwebbeans/meecrowave/trunk/meecrowave-oauth2/src/test/java/org/apache/meecrowave/oauth2/OAuth2Test.java

Modified: openwebbeans/meecrowave/trunk/meecrowave-oauth2/pom.xml
URL: http://svn.apache.org/viewvc/openwebbeans/meecrowave/trunk/meecrowave-oauth2/pom.xml?rev=1853921&r1=1853920&r2=1853921&view=diff
==============================================================================
--- openwebbeans/meecrowave/trunk/meecrowave-oauth2/pom.xml (original)
+++ openwebbeans/meecrowave/trunk/meecrowave-oauth2/pom.xml Tue Feb 19 22:03:37 2019
@@ -106,6 +106,12 @@
       <version>1.46</version>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.apache.geronimo</groupId>
+      <artifactId>geronimo-jwt-auth</artifactId>
+      <version>1.0.1</version>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
   <build>

Modified: openwebbeans/meecrowave/trunk/meecrowave-oauth2/src/main/java/org/apache/meecrowave/oauth2/configuration/OAuth2Configurer.java
URL: http://svn.apache.org/viewvc/openwebbeans/meecrowave/trunk/meecrowave-oauth2/src/main/java/org/apache/meecrowave/oauth2/configuration/OAuth2Configurer.java?rev=1853921&r1=1853920&r2=1853921&view=diff
==============================================================================
--- openwebbeans/meecrowave/trunk/meecrowave-oauth2/src/main/java/org/apache/meecrowave/oauth2/configuration/OAuth2Configurer.java (original)
+++ openwebbeans/meecrowave/trunk/meecrowave-oauth2/src/main/java/org/apache/meecrowave/oauth2/configuration/OAuth2Configurer.java Tue Feb 19 22:03:37 2019
@@ -18,6 +18,36 @@
  */
 package org.apache.meecrowave.oauth2.configuration;
 
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptySet;
+import static java.util.Locale.ENGLISH;
+import static java.util.Optional.ofNullable;
+import static java.util.function.Function.identity;
+import static java.util.stream.Collectors.toMap;
+import static org.apache.cxf.rs.security.oauth2.common.AuthenticationMethod.PASSWORD;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.nio.charset.StandardCharsets;
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import javax.annotation.PostConstruct;
+import javax.crypto.spec.SecretKeySpec;
+import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Inject;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+
 import org.apache.catalina.realm.GenericPrincipal;
 import org.apache.cxf.Bus;
 import org.apache.cxf.common.util.StringUtils;
@@ -30,7 +60,11 @@ import org.apache.cxf.rs.security.jose.j
 import org.apache.cxf.rs.security.jose.jwe.JweUtils;
 import org.apache.cxf.rs.security.jose.jws.JwsSignatureProvider;
 import org.apache.cxf.rs.security.jose.jws.JwsUtils;
+import org.apache.cxf.rs.security.jose.jwt.JwtClaims;
+import org.apache.cxf.rs.security.oauth2.common.AccessToken;
 import org.apache.cxf.rs.security.oauth2.common.OAuthRedirectionState;
+import org.apache.cxf.rs.security.oauth2.common.ServerAccessToken;
+import org.apache.cxf.rs.security.oauth2.common.TokenIntrospection;
 import org.apache.cxf.rs.security.oauth2.common.UserSubject;
 import org.apache.cxf.rs.security.oauth2.grants.AbstractGrantHandler;
 import org.apache.cxf.rs.security.oauth2.grants.clientcred.ClientCredentialsGrantHandler;
@@ -59,32 +93,6 @@ import org.apache.meecrowave.Meecrowave;
 import org.apache.meecrowave.oauth2.data.RefreshTokenEnabledProvider;
 import org.apache.meecrowave.oauth2.provider.JCacheCodeDataProvider;
 
-import javax.annotation.PostConstruct;
-import javax.crypto.spec.SecretKeySpec;
-import javax.enterprise.context.ApplicationScoped;
-import javax.inject.Inject;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServletRequest;
-import javax.ws.rs.core.MultivaluedMap;
-import java.io.IOException;
-import java.io.StringReader;
-import java.nio.charset.StandardCharsets;
-import java.security.Principal;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Properties;
-import java.util.function.Consumer;
-
-import static java.util.Arrays.asList;
-import static java.util.Collections.emptySet;
-import static java.util.Locale.ENGLISH;
-import static java.util.Optional.ofNullable;
-import static java.util.stream.Collectors.toMap;
-import static org.apache.cxf.rs.security.oauth2.common.AuthenticationMethod.PASSWORD;
-
 @ApplicationScoped
 public class OAuth2Configurer {
     @Inject
@@ -99,28 +107,46 @@ public class OAuth2Configurer {
     @Inject
     private JCacheConfigurer jCacheConfigurer;
 
+    private OAuth2Options configuration;
+
     private Consumer<AccessTokenService> tokenServiceConsumer;
     private Consumer<RedirectionBasedGrantService> redirectionBasedGrantServiceConsumer;
     private Consumer<AbstractTokenService> abstractTokenServiceConsumer;
-    private OAuth2Options configuration;
     private Map<String, String> securityProperties;
 
     @PostConstruct // TODO: still some missing configuration for jwt etc to add/wire from OAuth2Options
     private void preCompute() {
         configuration = builder.getExtension(OAuth2Options.class);
 
+        final Function<JwtClaims, JwtClaims> customizeClaims = configuration.isUseJwtFormatForAccessTokens() ? claims -> {
+            if (claims.getIssuer() == null) {
+                claims.setIssuer(configuration.getJwtIssuer());
+            }
+            return claims;
+        } : identity();
+
         AbstractOAuthDataProvider provider;
         switch (configuration.getProvider().toLowerCase(ENGLISH)) {
             case "jpa": {
                 if (!configuration.isAuthorizationCodeSupport()) { // else use code impl
-                    final JPAOAuthDataProvider jpaProvider = new JPAOAuthDataProvider();
+                    final JPAOAuthDataProvider jpaProvider = new JPAOAuthDataProvider() {
+                        @Override
+                        protected JwtClaims createJwtAccessToken(final ServerAccessToken at) {
+                            return customizeClaims.apply(super.createJwtAccessToken(at));
+                        }
+                    };
                     jpaProvider.setEntityManagerFactory(JPAAdapter.createEntityManagerFactory(configuration));
                     provider = jpaProvider;
                     break;
                 }
             }
             case "jpa-code": {
-                final JPACodeDataProvider jpaProvider = new JPACodeDataProvider();
+                final JPACodeDataProvider jpaProvider = new JPACodeDataProvider() {
+                    @Override
+                    protected JwtClaims createJwtAccessToken(final ServerAccessToken at) {
+                        return customizeClaims.apply(super.createJwtAccessToken(at));
+                    }
+                };
                 jpaProvider.setEntityManagerFactory(JPAAdapter.createEntityManagerFactory(configuration));
                 provider = jpaProvider;
                 break;
@@ -129,7 +155,12 @@ public class OAuth2Configurer {
                 if (!configuration.isAuthorizationCodeSupport()) { // else use code impl
                     jCacheConfigurer.doSetup(configuration);
                     try {
-                        provider = new JCacheOAuthDataProvider(configuration.getJcacheConfigUri(), bus, configuration.isJcacheStoreJwtKeyOnly());
+                        provider = new JCacheOAuthDataProvider(configuration.getJcacheConfigUri(), bus, configuration.isJcacheStoreJwtKeyOnly()) {
+                            @Override
+                            protected JwtClaims createJwtAccessToken(final ServerAccessToken at) {
+                                return customizeClaims.apply(super.createJwtAccessToken(at));
+                            }
+                        };
                     } catch (final Exception e) {
                         throw new IllegalStateException(e);
                     }
@@ -138,7 +169,12 @@ public class OAuth2Configurer {
             case "jcache-code":
                 jCacheConfigurer.doSetup(configuration);
                 try {
-                    provider = new JCacheCodeDataProvider(configuration, bus);
+                    provider = new JCacheCodeDataProvider(configuration, bus) {
+                        @Override
+                        protected JwtClaims createJwtAccessToken(final ServerAccessToken at) {
+                            return customizeClaims.apply(super.createJwtAccessToken(at));
+                        }
+                    };
                 } catch (final Exception e) {
                     throw new IllegalStateException(e);
                 }
@@ -146,12 +182,22 @@ public class OAuth2Configurer {
             case "encrypted":
                 if (!configuration.isAuthorizationCodeSupport()) { // else use code impl
                     provider = new DefaultEncryptingOAuthDataProvider(
-                            new SecretKeySpec(configuration.getEncryptedKey().getBytes(StandardCharsets.UTF_8), configuration.getEncryptedAlgo()));
+                            new SecretKeySpec(configuration.getEncryptedKey().getBytes(StandardCharsets.UTF_8), configuration.getEncryptedAlgo())) {
+                        @Override
+                        protected JwtClaims createJwtAccessToken(final ServerAccessToken at) {
+                            return customizeClaims.apply(super.createJwtAccessToken(at));
+                        }
+                    };
                     break;
                 }
             case "encrypted-code":
                 provider = new DefaultEncryptingCodeDataProvider(
-                        new SecretKeySpec(configuration.getEncryptedKey().getBytes(StandardCharsets.UTF_8), configuration.getEncryptedAlgo()));
+                        new SecretKeySpec(configuration.getEncryptedKey().getBytes(StandardCharsets.UTF_8), configuration.getEncryptedAlgo())) {
+                    @Override
+                    protected JwtClaims createJwtAccessToken(final ServerAccessToken at) {
+                        return customizeClaims.apply(super.createJwtAccessToken(at));
+                    }
+                };
                 break;
             default:
                 throw new IllegalArgumentException("Unsupported oauth2 provider: " + configuration.getProvider());

Modified: openwebbeans/meecrowave/trunk/meecrowave-oauth2/src/main/java/org/apache/meecrowave/oauth2/configuration/OAuth2Options.java
URL: http://svn.apache.org/viewvc/openwebbeans/meecrowave/trunk/meecrowave-oauth2/src/main/java/org/apache/meecrowave/oauth2/configuration/OAuth2Options.java?rev=1853921&r1=1853920&r2=1853921&view=diff
==============================================================================
--- openwebbeans/meecrowave/trunk/meecrowave-oauth2/src/main/java/org/apache/meecrowave/oauth2/configuration/OAuth2Options.java (original)
+++ openwebbeans/meecrowave/trunk/meecrowave-oauth2/src/main/java/org/apache/meecrowave/oauth2/configuration/OAuth2Options.java Tue Feb 19 22:03:37 2019
@@ -76,6 +76,9 @@ public class OAuth2Options implements Cl
     @CliOption(name = "oauth2-jwt-access-token-claim-map", description = "The jwt claims configuration")
     private String jwtAccessTokenClaimMap;
 
+    @CliOption(name = "oauth2-jwt-issuer", description = "The jwt issuer (ignored if not set)")
+    private String jwtIssuer;
+
     @CliOption(name = "oauth2-provider", description = "Which provider type to use: jcache[-code], jpa[-code], encrypted[-code]")
     private String provider = "jcache";
 
@@ -536,4 +539,12 @@ public class OAuth2Options implements Cl
     public void setJcacheJmx(final boolean jcacheJmx) {
         this.jcacheJmx = jcacheJmx;
     }
+
+    public String getJwtIssuer() {
+        return jwtIssuer;
+    }
+
+    public void setJwtIssuer(final String jwtIssuer) {
+        this.jwtIssuer = jwtIssuer;
+    }
 }

Added: openwebbeans/meecrowave/trunk/meecrowave-oauth2/src/main/resources/META-INF/beans.xml
URL: http://svn.apache.org/viewvc/openwebbeans/meecrowave/trunk/meecrowave-oauth2/src/main/resources/META-INF/beans.xml?rev=1853921&view=auto
==============================================================================
--- openwebbeans/meecrowave/trunk/meecrowave-oauth2/src/main/resources/META-INF/beans.xml (added)
+++ openwebbeans/meecrowave/trunk/meecrowave-oauth2/src/main/resources/META-INF/beans.xml Tue Feb 19 22:03:37 2019
@@ -0,0 +1,28 @@
+<?xml version="1.0"?>
+<!--
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+-->
+<beans bean-discovery-mode="all" version="2.0"
+       xmlns="http://xmlns.jcp.org/xml/ns/javaee"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="
+        http://xmlns.jcp.org/xml/ns/javaee
+        http://xmlns.jcp.org/xml/ns/javaee/beans_2_0.xsd">
+
+  <trim/>
+</beans>

Modified: openwebbeans/meecrowave/trunk/meecrowave-oauth2/src/test/java/org/apache/meecrowave/oauth2/Keystores.java
URL: http://svn.apache.org/viewvc/openwebbeans/meecrowave/trunk/meecrowave-oauth2/src/test/java/org/apache/meecrowave/oauth2/Keystores.java?rev=1853921&r1=1853920&r2=1853921&view=diff
==============================================================================
--- openwebbeans/meecrowave/trunk/meecrowave-oauth2/src/test/java/org/apache/meecrowave/oauth2/Keystores.java (original)
+++ openwebbeans/meecrowave/trunk/meecrowave-oauth2/src/test/java/org/apache/meecrowave/oauth2/Keystores.java Tue Feb 19 22:03:37 2019
@@ -32,6 +32,7 @@ import java.io.OutputStream;
 import java.security.KeyStore;
 import java.security.Principal;
 import java.security.PrivateKey;
+import java.security.PublicKey;
 import java.security.cert.X509Certificate;
 
 public final class Keystores {
@@ -39,29 +40,30 @@ public final class Keystores {
         // no-op
     }
 
-    public static void create(final File keystore) throws Exception {
+    public static PublicKey create(final File keystore) throws Exception {
         CryptoUtils.installBouncyCastleProvider();
 
         final KeyStore ks = KeyStore.getInstance("JKS");
         ks.load(null, "password".toCharArray());
 
-        final CertAndKeyGen keyGen = new CertAndKeyGen("RSA", "SHA1WithRSA", null);
+        final CertAndKeyGen keyGen = new CertAndKeyGen("RSA", "SHA256WithRSA", null);
         keyGen.generate(2048);
-        PrivateKey rootPrivateKey = keyGen.getPrivateKey();
+        final PrivateKey rootPrivateKey = keyGen.getPrivateKey();
 
         X509Certificate rootCertificate = keyGen.getSelfCertificate(new X500Name("cn=root"), (long) 365 * 24 * 60 * 60);
 
-        final CertAndKeyGen keyGen1 = new CertAndKeyGen("RSA", "SHA1WithRSA", null);
+        final CertAndKeyGen keyGen1 = new CertAndKeyGen("RSA", "SHA256WithRSA", null);
         keyGen1.generate(2048);
         final PrivateKey middlePrivateKey = keyGen1.getPrivateKey();
 
         X509Certificate middleCertificate = keyGen1.getSelfCertificate(new X500Name("CN=MIDDLE"), (long) 365 * 24 * 60 * 60);
 
         //Generate leaf certificate
-        final CertAndKeyGen keyGen2 = new CertAndKeyGen("RSA", "SHA1WithRSA", null);
+        final CertAndKeyGen keyGen2 = new CertAndKeyGen("RSA", "SHA256WithRSA", null);
         keyGen2.generate(2048);
         final PrivateKey topPrivateKey = keyGen2.getPrivateKey();
 
+
         X509Certificate topCertificate = keyGen2.getSelfCertificate(new X500Name("cn=root"), (long) 365 * 24 * 60 * 60);
 
         rootCertificate = createSignedCertificate(rootCertificate, rootCertificate, rootPrivateKey);
@@ -77,6 +79,8 @@ public final class Keystores {
         try (final OutputStream os = new FileOutputStream(keystore)) {
             ks.store(os, "password".toCharArray());
         }
+
+        return keyGen2.getPublicKey();
     }
 
     private static X509Certificate createSignedCertificate(final X509Certificate cetrificate, final X509Certificate issuerCertificate,

Modified: openwebbeans/meecrowave/trunk/meecrowave-oauth2/src/test/java/org/apache/meecrowave/oauth2/OAuth2Test.java
URL: http://svn.apache.org/viewvc/openwebbeans/meecrowave/trunk/meecrowave-oauth2/src/test/java/org/apache/meecrowave/oauth2/OAuth2Test.java?rev=1853921&r1=1853920&r2=1853921&view=diff
==============================================================================
--- openwebbeans/meecrowave/trunk/meecrowave-oauth2/src/test/java/org/apache/meecrowave/oauth2/OAuth2Test.java (original)
+++ openwebbeans/meecrowave/trunk/meecrowave-oauth2/src/test/java/org/apache/meecrowave/oauth2/OAuth2Test.java Tue Feb 19 22:03:37 2019
@@ -30,10 +30,14 @@ import static org.junit.Assert.assertTru
 import static org.junit.Assert.fail;
 
 import java.io.File;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
 import java.net.URISyntaxException;
 import java.nio.charset.StandardCharsets;
+import java.security.PublicKey;
 import java.util.Base64;
 import java.util.function.BiFunction;
+import java.util.stream.Stream;
 
 import javax.cache.Cache;
 import javax.cache.CacheManager;
@@ -56,15 +60,22 @@ import org.apache.cxf.rs.security.oauth2
 import org.apache.cxf.rs.security.oauth2.common.OAuthAuthorizationData;
 import org.apache.cxf.rs.security.oauth2.provider.OAuthJSONProvider;
 import org.apache.cxf.rs.security.oauth2.utils.OAuthConstants;
+import org.apache.geronimo.microprofile.impl.jwtauth.config.GeronimoJwtAuthConfig;
+import org.apache.geronimo.microprofile.impl.jwtauth.jwt.DateValidator;
+import org.apache.geronimo.microprofile.impl.jwtauth.jwt.JwtParser;
+import org.apache.geronimo.microprofile.impl.jwtauth.jwt.KidMapper;
+import org.apache.geronimo.microprofile.impl.jwtauth.jwt.SignatureValidator;
 import org.apache.meecrowave.Meecrowave;
 import org.apache.meecrowave.junit.MeecrowaveRule;
 import org.apache.meecrowave.oauth2.provider.JCacheCodeDataProvider;
+import org.eclipse.microprofile.jwt.JsonWebToken;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
 import org.junit.Test;
 
 public class OAuth2Test {
     private static final File KEYSTORE = new File("target/OAuth2Test/keystore.jceks");
+    private static PublicKey PUBLIC_KEY;
 
     @ClassRule
     public static final MeecrowaveRule MEECROWAVE = new MeecrowaveRule(
@@ -72,6 +83,7 @@ public class OAuth2Test {
                     .user("test", "pwd").role("test", "admin")
                     // jwt requires more config
                     .property("oauth2-use-jwt-format-for-access-token", "true")
+                    .property("oauth2-jwt-issuer", "myissuer")
                     // auth code support is optional so activate it
                     .property("oauth2-authorization-code-support", "true")
                     // auth code jose setup to store the tokens
@@ -90,7 +102,7 @@ public class OAuth2Test {
 
     @BeforeClass
     public static void createKeyStore() throws Exception {
-        Keystores.create(KEYSTORE);
+        PUBLIC_KEY = Keystores.create(KEYSTORE);
     }
 
     @Test
@@ -112,6 +124,7 @@ public class OAuth2Test {
             assertEquals(3600, token.getExpiresIn());
             assertNotEquals(0, token.getIssuedAt());
             assertNotNull(token.getRefreshToken());
+            validateJwt(token);
         } finally {
             client.close();
         }
@@ -245,4 +258,59 @@ public class OAuth2Test {
             fail(e.getMessage());
         }
     }
+
+    private void validateJwt(final ClientAccessToken token) {
+        final JwtParser parser = new JwtParser();
+        final KidMapper kidMapper = new KidMapper();
+        final DateValidator dateValidator = new DateValidator();
+        final SignatureValidator signatureValidator = new SignatureValidator();
+        final GeronimoJwtAuthConfig config = (value, def) -> {
+            switch (value) {
+                case "issuer.default":
+                    return "myissuer";
+                case "jwt.header.kid.default":
+                    return "defaultkid";
+                case "public-key.default":
+                    return Base64.getEncoder().encodeToString(PUBLIC_KEY.getEncoded());
+                default:
+                    return def;
+            }
+        };
+        setField(kidMapper, "config", config);
+        setField(dateValidator, "config", config);
+        setField(parser, "config", config);
+        setField(signatureValidator, "config", config);
+        setField(parser, "kidMapper", kidMapper);
+        setField(parser, "dateValidator", dateValidator);
+        setField(parser, "signatureValidator", signatureValidator);
+        Stream.of(dateValidator, signatureValidator, kidMapper, parser).forEach(this::init);
+        final JsonWebToken jsonWebToken = parser.parse(token.getTokenKey());
+        assertNotNull(jsonWebToken);
+        assertEquals("myissuer", jsonWebToken.getIssuer());
+        assertEquals("test", JsonString.class.cast(jsonWebToken.getClaim("username")).getString());
+    }
+
+    private void init(final Object o) {
+        try {
+            final Method init = o.getClass().getDeclaredMethod("init");
+            if (!init.isAccessible()) {
+                init.setAccessible(true);
+            }
+            init.invoke(o);
+        } catch (final Exception e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    private void setField(final Object instance, final String field, final Object value) {
+        try {
+            final Field declaredField = instance.getClass().getDeclaredField(field);
+            if (!declaredField.isAccessible()) {
+                declaredField.setAccessible(true);
+            }
+            declaredField.set(instance, value);
+        } catch (final Exception e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
 }