001/*******************************************************************************
002 * Copyright 2017 The MIT Internet Trust Consortium
003 *
004 * Portions copyright 2011-2013 The MITRE Corporation
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License");
007 * you may not use this file except in compliance with the License.
008 * You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 *******************************************************************************/
018/**
019 *
020 */
021package org.mitre.oauth2.service.impl;
022
023import static org.mitre.openid.connect.request.ConnectRequestParameters.CODE_CHALLENGE;
024import static org.mitre.openid.connect.request.ConnectRequestParameters.CODE_CHALLENGE_METHOD;
025import static org.mitre.openid.connect.request.ConnectRequestParameters.CODE_VERIFIER;
026
027import java.nio.charset.StandardCharsets;
028import java.security.MessageDigest;
029import java.security.NoSuchAlgorithmException;
030import java.util.Collection;
031import java.util.Date;
032import java.util.HashSet;
033import java.util.List;
034import java.util.Set;
035import java.util.UUID;
036
037import org.mitre.data.AbstractPageOperationTemplate;
038import org.mitre.data.DefaultPageCriteria;
039import org.mitre.oauth2.model.AuthenticationHolderEntity;
040import org.mitre.oauth2.model.ClientDetailsEntity;
041import org.mitre.oauth2.model.OAuth2AccessTokenEntity;
042import org.mitre.oauth2.model.OAuth2RefreshTokenEntity;
043import org.mitre.oauth2.model.PKCEAlgorithm;
044import org.mitre.oauth2.model.SystemScope;
045import org.mitre.oauth2.repository.AuthenticationHolderRepository;
046import org.mitre.oauth2.repository.OAuth2TokenRepository;
047import org.mitre.oauth2.service.ClientDetailsEntityService;
048import org.mitre.oauth2.service.OAuth2TokenEntityService;
049import org.mitre.oauth2.service.SystemScopeService;
050import org.mitre.openid.connect.model.ApprovedSite;
051import org.mitre.openid.connect.service.ApprovedSiteService;
052import org.slf4j.Logger;
053import org.slf4j.LoggerFactory;
054import org.springframework.beans.factory.annotation.Autowired;
055import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
056import org.springframework.security.core.AuthenticationException;
057import org.springframework.security.oauth2.common.exceptions.InvalidClientException;
058import org.springframework.security.oauth2.common.exceptions.InvalidRequestException;
059import org.springframework.security.oauth2.common.exceptions.InvalidScopeException;
060import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
061import org.springframework.security.oauth2.provider.OAuth2Authentication;
062import org.springframework.security.oauth2.provider.OAuth2Request;
063import org.springframework.security.oauth2.provider.TokenRequest;
064import org.springframework.security.oauth2.provider.token.TokenEnhancer;
065import org.springframework.stereotype.Service;
066import org.springframework.transaction.annotation.Transactional;
067
068import com.google.common.base.Strings;
069import com.google.common.collect.Sets;
070import com.nimbusds.jose.util.Base64URL;
071import com.nimbusds.jwt.JWTClaimsSet;
072import com.nimbusds.jwt.PlainJWT;
073
074
075/**
076 * @author jricher
077 *
078 */
079@Service("defaultOAuth2ProviderTokenService")
080public class DefaultOAuth2ProviderTokenService implements OAuth2TokenEntityService {
081
082        /**
083         * Logger for this class
084         */
085        private static final Logger logger = LoggerFactory.getLogger(DefaultOAuth2ProviderTokenService.class);
086
087        @Autowired
088        private OAuth2TokenRepository tokenRepository;
089
090        @Autowired
091        private AuthenticationHolderRepository authenticationHolderRepository;
092
093        @Autowired
094        private ClientDetailsEntityService clientDetailsService;
095
096        @Autowired
097        private TokenEnhancer tokenEnhancer;
098
099        @Autowired
100        private SystemScopeService scopeService;
101
102        @Autowired
103        private ApprovedSiteService approvedSiteService;
104
105
106        @Override
107        public Set<OAuth2AccessTokenEntity> getAllAccessTokensForUser(String id) {
108
109                Set<OAuth2AccessTokenEntity> all = tokenRepository.getAllAccessTokens();
110                Set<OAuth2AccessTokenEntity> results = Sets.newLinkedHashSet();
111
112                for (OAuth2AccessTokenEntity token : all) {
113                        if (clearExpiredAccessToken(token) != null && token.getAuthenticationHolder().getAuthentication().getName().equals(id)) {
114                                results.add(token);
115                        }
116                }
117
118                return results;
119        }
120
121
122        @Override
123        public Set<OAuth2RefreshTokenEntity> getAllRefreshTokensForUser(String id) {
124                Set<OAuth2RefreshTokenEntity> all = tokenRepository.getAllRefreshTokens();
125                Set<OAuth2RefreshTokenEntity> results = Sets.newLinkedHashSet();
126
127                for (OAuth2RefreshTokenEntity token : all) {
128                        if (clearExpiredRefreshToken(token) != null && token.getAuthenticationHolder().getAuthentication().getName().equals(id)) {
129                                results.add(token);
130                        }
131                }
132
133                return results;
134        }
135
136        @Override
137        public OAuth2AccessTokenEntity getAccessTokenById(Long id) {
138                return clearExpiredAccessToken(tokenRepository.getAccessTokenById(id));
139        }
140
141        @Override
142        public OAuth2RefreshTokenEntity getRefreshTokenById(Long id) {
143                return clearExpiredRefreshToken(tokenRepository.getRefreshTokenById(id));
144        }
145
146        /**
147         * Utility function to delete an access token that's expired before returning it.
148         * @param token the token to check
149         * @return null if the token is null or expired, the input token (unchanged) if it hasn't
150         */
151        private OAuth2AccessTokenEntity clearExpiredAccessToken(OAuth2AccessTokenEntity token) {
152                if (token == null) {
153                        return null;
154                } else if (token.isExpired()) {
155                        // immediately revoke expired token
156                        logger.debug("Clearing expired access token: " + token.getValue());
157                        revokeAccessToken(token);
158                        return null;
159                } else {
160                        return token;
161                }
162        }
163
164        /**
165         * Utility function to delete a refresh token that's expired before returning it.
166         * @param token the token to check
167         * @return null if the token is null or expired, the input token (unchanged) if it hasn't
168         */
169        private OAuth2RefreshTokenEntity clearExpiredRefreshToken(OAuth2RefreshTokenEntity token) {
170                if (token == null) {
171                        return null;
172                } else if (token.isExpired()) {
173                        // immediately revoke expired token
174                        logger.debug("Clearing expired refresh token: " + token.getValue());
175                        revokeRefreshToken(token);
176                        return null;
177                } else {
178                        return token;
179                }
180        }
181
182        @Override
183        @Transactional(value="defaultTransactionManager")
184        public OAuth2AccessTokenEntity createAccessToken(OAuth2Authentication authentication) throws AuthenticationException, InvalidClientException {
185                if (authentication != null && authentication.getOAuth2Request() != null) {
186                        // look up our client
187                        OAuth2Request request = authentication.getOAuth2Request();
188
189                        ClientDetailsEntity client = clientDetailsService.loadClientByClientId(request.getClientId());
190
191                        if (client == null) {
192                                throw new InvalidClientException("Client not found: " + request.getClientId());
193                        }
194
195
196                        // handle the PKCE code challenge if present
197                        if (request.getExtensions().containsKey(CODE_CHALLENGE)) {
198                                String challenge = (String) request.getExtensions().get(CODE_CHALLENGE);
199                                PKCEAlgorithm alg = PKCEAlgorithm.parse((String) request.getExtensions().get(CODE_CHALLENGE_METHOD));
200
201                                String verifier = request.getRequestParameters().get(CODE_VERIFIER);
202
203                                if (alg.equals(PKCEAlgorithm.plain)) {
204                                        // do a direct string comparison
205                                        if (!challenge.equals(verifier)) {
206                                                throw new InvalidRequestException("Code challenge and verifier do not match");
207                                        }
208                                } else if (alg.equals(PKCEAlgorithm.S256)) {
209                                        // hash the verifier
210                                        try {
211                                                MessageDigest digest = MessageDigest.getInstance("SHA-256");
212                                                String hash = Base64URL.encode(digest.digest(verifier.getBytes(StandardCharsets.US_ASCII))).toString();
213                                                if (!challenge.equals(hash)) {
214                                                        throw new InvalidRequestException("Code challenge and verifier do not match");
215                                                }
216                                        } catch (NoSuchAlgorithmException e) {
217                                                logger.error("Unknown algorithm for PKCE digest", e);
218                                        }
219                                }
220
221                        }
222
223
224                        OAuth2AccessTokenEntity token = new OAuth2AccessTokenEntity();//accessTokenFactory.createNewAccessToken();
225
226                        // attach the client
227                        token.setClient(client);
228
229                        // inherit the scope from the auth, but make a new set so it is
230                        //not unmodifiable. Unmodifiables don't play nicely with Eclipselink, which
231                        //wants to use the clone operation.
232                        Set<SystemScope> scopes = scopeService.fromStrings(request.getScope());
233
234                        // remove any of the special system scopes
235                        scopes = scopeService.removeReservedScopes(scopes);
236
237                        token.setScope(scopeService.toStrings(scopes));
238
239                        // make it expire if necessary
240                        if (client.getAccessTokenValiditySeconds() != null && client.getAccessTokenValiditySeconds() > 0) {
241                                Date expiration = new Date(System.currentTimeMillis() + (client.getAccessTokenValiditySeconds() * 1000L));
242                                token.setExpiration(expiration);
243                        }
244
245                        // attach the authorization so that we can look it up later
246                        AuthenticationHolderEntity authHolder = new AuthenticationHolderEntity();
247                        authHolder.setAuthentication(authentication);
248                        authHolder = authenticationHolderRepository.save(authHolder);
249
250                        token.setAuthenticationHolder(authHolder);
251
252                        // attach a refresh token, if this client is allowed to request them and the user gets the offline scope
253                        if (client.isAllowRefresh() && token.getScope().contains(SystemScopeService.OFFLINE_ACCESS)) {
254                                OAuth2RefreshTokenEntity savedRefreshToken = createRefreshToken(client, authHolder);
255
256                                token.setRefreshToken(savedRefreshToken);
257                        }
258
259                        //Add approved site reference, if any
260                        OAuth2Request originalAuthRequest = authHolder.getAuthentication().getOAuth2Request();
261
262                        if (originalAuthRequest.getExtensions() != null && originalAuthRequest.getExtensions().containsKey("approved_site")) {
263
264                                Long apId = Long.parseLong((String) originalAuthRequest.getExtensions().get("approved_site"));
265                                ApprovedSite ap = approvedSiteService.getById(apId);
266
267                                token.setApprovedSite(ap);
268                        }
269
270                        OAuth2AccessTokenEntity enhancedToken = (OAuth2AccessTokenEntity) tokenEnhancer.enhance(token, authentication);
271
272                        OAuth2AccessTokenEntity savedToken = saveAccessToken(enhancedToken);
273
274                        if (savedToken.getRefreshToken() != null) {
275                                tokenRepository.saveRefreshToken(savedToken.getRefreshToken()); // make sure we save any changes that might have been enhanced
276                        }
277
278                        return savedToken;
279                }
280
281                throw new AuthenticationCredentialsNotFoundException("No authentication credentials found");
282        }
283
284
285        private OAuth2RefreshTokenEntity createRefreshToken(ClientDetailsEntity client, AuthenticationHolderEntity authHolder) {
286                OAuth2RefreshTokenEntity refreshToken = new OAuth2RefreshTokenEntity(); //refreshTokenFactory.createNewRefreshToken();
287                JWTClaimsSet.Builder refreshClaims = new JWTClaimsSet.Builder();
288
289
290                // make it expire if necessary
291                if (client.getRefreshTokenValiditySeconds() != null) {
292                        Date expiration = new Date(System.currentTimeMillis() + (client.getRefreshTokenValiditySeconds() * 1000L));
293                        refreshToken.setExpiration(expiration);
294                        refreshClaims.expirationTime(expiration);
295                }
296
297                // set a random identifier
298                refreshClaims.jwtID(UUID.randomUUID().toString());
299
300                // TODO: add issuer fields, signature to JWT
301
302                PlainJWT refreshJwt = new PlainJWT(refreshClaims.build());
303                refreshToken.setJwt(refreshJwt);
304
305                //Add the authentication
306                refreshToken.setAuthenticationHolder(authHolder);
307                refreshToken.setClient(client);
308
309
310
311                // save the token first so that we can set it to a member of the access token (NOTE: is this step necessary?)
312                OAuth2RefreshTokenEntity savedRefreshToken = tokenRepository.saveRefreshToken(refreshToken);
313                return savedRefreshToken;
314        }
315
316        @Override
317        @Transactional(value="defaultTransactionManager")
318        public OAuth2AccessTokenEntity refreshAccessToken(String refreshTokenValue, TokenRequest authRequest) throws AuthenticationException {
319                
320                if (Strings.isNullOrEmpty(refreshTokenValue)) {
321                        // throw an invalid token exception if there's no refresh token value at all
322                        throw new InvalidTokenException("Invalid refresh token: " + refreshTokenValue);
323                }
324
325                OAuth2RefreshTokenEntity refreshToken = clearExpiredRefreshToken(tokenRepository.getRefreshTokenByValue(refreshTokenValue));
326
327                if (refreshToken == null) {
328                        // throw an invalid token exception if we couldn't find the token
329                        throw new InvalidTokenException("Invalid refresh token: " + refreshTokenValue);
330                }
331
332                ClientDetailsEntity client = refreshToken.getClient();
333
334                AuthenticationHolderEntity authHolder = refreshToken.getAuthenticationHolder();
335
336                // make sure that the client requesting the token is the one who owns the refresh token
337                ClientDetailsEntity requestingClient = clientDetailsService.loadClientByClientId(authRequest.getClientId());
338                if (!client.getClientId().equals(requestingClient.getClientId())) {
339                        tokenRepository.removeRefreshToken(refreshToken);
340                        throw new InvalidClientException("Client does not own the presented refresh token");
341                }
342
343                //Make sure this client allows access token refreshing
344                if (!client.isAllowRefresh()) {
345                        throw new InvalidClientException("Client does not allow refreshing access token!");
346                }
347
348                // clear out any access tokens
349                if (client.isClearAccessTokensOnRefresh()) {
350                        tokenRepository.clearAccessTokensForRefreshToken(refreshToken);
351                }
352
353                if (refreshToken.isExpired()) {
354                        tokenRepository.removeRefreshToken(refreshToken);
355                        throw new InvalidTokenException("Expired refresh token: " + refreshTokenValue);
356                }
357
358                OAuth2AccessTokenEntity token = new OAuth2AccessTokenEntity();
359
360                // get the stored scopes from the authentication holder's authorization request; these are the scopes associated with the refresh token
361                Set<String> refreshScopesRequested = new HashSet<>(refreshToken.getAuthenticationHolder().getAuthentication().getOAuth2Request().getScope());
362                Set<SystemScope> refreshScopes = scopeService.fromStrings(refreshScopesRequested);
363                // remove any of the special system scopes
364                refreshScopes = scopeService.removeReservedScopes(refreshScopes);
365
366                Set<String> scopeRequested = authRequest.getScope() == null ? new HashSet<String>() : new HashSet<>(authRequest.getScope());
367                Set<SystemScope> scope = scopeService.fromStrings(scopeRequested);
368
369                // remove any of the special system scopes
370                scope = scopeService.removeReservedScopes(scope);
371
372                if (scope != null && !scope.isEmpty()) {
373                        // ensure a proper subset of scopes
374                        if (refreshScopes != null && refreshScopes.containsAll(scope)) {
375                                // set the scope of the new access token if requested
376                                token.setScope(scopeService.toStrings(scope));
377                        } else {
378                                String errorMsg = "Up-scoping is not allowed.";
379                                logger.error(errorMsg);
380                                throw new InvalidScopeException(errorMsg);
381                        }
382                } else {
383                        // otherwise inherit the scope of the refresh token (if it's there -- this can return a null scope set)
384                        token.setScope(scopeService.toStrings(refreshScopes));
385                }
386
387                token.setClient(client);
388
389                if (client.getAccessTokenValiditySeconds() != null) {
390                        Date expiration = new Date(System.currentTimeMillis() + (client.getAccessTokenValiditySeconds() * 1000L));
391                        token.setExpiration(expiration);
392                }
393
394                if (client.isReuseRefreshToken()) {
395                        // if the client re-uses refresh tokens, do that
396                        token.setRefreshToken(refreshToken);
397                } else {
398                        // otherwise, make a new refresh token
399                        OAuth2RefreshTokenEntity newRefresh = createRefreshToken(client, authHolder);
400                        token.setRefreshToken(newRefresh);
401
402                        // clean up the old refresh token
403                        tokenRepository.removeRefreshToken(refreshToken);
404                }
405
406                token.setAuthenticationHolder(authHolder);
407
408                tokenEnhancer.enhance(token, authHolder.getAuthentication());
409
410                tokenRepository.saveAccessToken(token);
411
412                return token;
413
414        }
415
416        @Override
417        public OAuth2Authentication loadAuthentication(String accessTokenValue) throws AuthenticationException {
418
419                OAuth2AccessTokenEntity accessToken = clearExpiredAccessToken(tokenRepository.getAccessTokenByValue(accessTokenValue));
420
421                if (accessToken == null) {
422                        throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
423                } else {
424                        return accessToken.getAuthenticationHolder().getAuthentication();
425                }
426        }
427
428
429        /**
430         * Get an access token from its token value.
431         */
432        @Override
433        public OAuth2AccessTokenEntity readAccessToken(String accessTokenValue) throws AuthenticationException {
434                OAuth2AccessTokenEntity accessToken = clearExpiredAccessToken(tokenRepository.getAccessTokenByValue(accessTokenValue));
435                if (accessToken == null) {
436                        throw new InvalidTokenException("Access token for value " + accessTokenValue + " was not found");
437                } else {
438                        return accessToken;
439                }
440        }
441
442        /**
443         * Get an access token by its authentication object.
444         */
445        @Override
446        public OAuth2AccessTokenEntity getAccessToken(OAuth2Authentication authentication) {
447                // TODO: implement this against the new service (#825)
448                throw new UnsupportedOperationException("Unable to look up access token from authentication object.");
449        }
450
451        /**
452         * Get a refresh token by its token value.
453         */
454        @Override
455        public OAuth2RefreshTokenEntity getRefreshToken(String refreshTokenValue) throws AuthenticationException {
456                OAuth2RefreshTokenEntity refreshToken = tokenRepository.getRefreshTokenByValue(refreshTokenValue);
457                if (refreshToken == null) {
458                        throw new InvalidTokenException("Refresh token for value " + refreshTokenValue + " was not found");
459                }
460                else {
461                        return refreshToken;
462                }
463        }
464
465        /**
466         * Revoke a refresh token and all access tokens issued to it.
467         */
468        @Override
469        @Transactional(value="defaultTransactionManager")
470        public void revokeRefreshToken(OAuth2RefreshTokenEntity refreshToken) {
471                tokenRepository.clearAccessTokensForRefreshToken(refreshToken);
472                tokenRepository.removeRefreshToken(refreshToken);
473        }
474
475        /**
476         * Revoke an access token.
477         */
478        @Override
479        @Transactional(value="defaultTransactionManager")
480        public void revokeAccessToken(OAuth2AccessTokenEntity accessToken) {
481                tokenRepository.removeAccessToken(accessToken);
482        }
483
484
485        /* (non-Javadoc)
486         * @see org.mitre.oauth2.service.OAuth2TokenEntityService#getAccessTokensForClient(org.mitre.oauth2.model.ClientDetailsEntity)
487         */
488        @Override
489        public List<OAuth2AccessTokenEntity> getAccessTokensForClient(ClientDetailsEntity client) {
490                return tokenRepository.getAccessTokensForClient(client);
491        }
492
493        /* (non-Javadoc)
494         * @see org.mitre.oauth2.service.OAuth2TokenEntityService#getRefreshTokensForClient(org.mitre.oauth2.model.ClientDetailsEntity)
495         */
496        @Override
497        public List<OAuth2RefreshTokenEntity> getRefreshTokensForClient(ClientDetailsEntity client) {
498                return tokenRepository.getRefreshTokensForClient(client);
499        }
500
501        /**
502         * Clears out expired tokens and any abandoned authentication objects
503         */
504        @Override
505        public void clearExpiredTokens() {
506                logger.debug("Cleaning out all expired tokens");
507
508                new AbstractPageOperationTemplate<OAuth2AccessTokenEntity>("clearExpiredAccessTokens") {
509                        @Override
510                        public Collection<OAuth2AccessTokenEntity> fetchPage() {
511                                return tokenRepository.getAllExpiredAccessTokens(new DefaultPageCriteria());
512                        }
513
514                        @Override
515                        public void doOperation(OAuth2AccessTokenEntity item) {
516                                revokeAccessToken(item);
517                        }
518                }.execute();
519
520                new AbstractPageOperationTemplate<OAuth2RefreshTokenEntity>("clearExpiredRefreshTokens") {
521                        @Override
522                        public Collection<OAuth2RefreshTokenEntity> fetchPage() {
523                                return tokenRepository.getAllExpiredRefreshTokens(new DefaultPageCriteria());
524                        }
525
526                        @Override
527                        public void doOperation(OAuth2RefreshTokenEntity item) {
528                                revokeRefreshToken(item);
529                        }
530                }.execute();
531
532                new AbstractPageOperationTemplate<AuthenticationHolderEntity>("clearExpiredAuthenticationHolders") {
533                        @Override
534                        public Collection<AuthenticationHolderEntity> fetchPage() {
535                                return authenticationHolderRepository.getOrphanedAuthenticationHolders(new DefaultPageCriteria());
536                        }
537
538                        @Override
539                        public void doOperation(AuthenticationHolderEntity item) {
540                                authenticationHolderRepository.remove(item);
541                        }
542                }.execute();
543        }
544
545        /* (non-Javadoc)
546         * @see org.mitre.oauth2.service.OAuth2TokenEntityService#saveAccessToken(org.mitre.oauth2.model.OAuth2AccessTokenEntity)
547         */
548        @Override
549        @Transactional(value="defaultTransactionManager")
550        public OAuth2AccessTokenEntity saveAccessToken(OAuth2AccessTokenEntity accessToken) {
551                OAuth2AccessTokenEntity newToken = tokenRepository.saveAccessToken(accessToken);
552
553                // if the old token has any additional information for the return from the token endpoint, carry it through here after save
554                if (accessToken.getAdditionalInformation() != null && !accessToken.getAdditionalInformation().isEmpty()) {
555                        newToken.getAdditionalInformation().putAll(accessToken.getAdditionalInformation());
556                }
557
558                return newToken;
559        }
560
561        /* (non-Javadoc)
562         * @see org.mitre.oauth2.service.OAuth2TokenEntityService#saveRefreshToken(org.mitre.oauth2.model.OAuth2RefreshTokenEntity)
563         */
564        @Override
565        @Transactional(value="defaultTransactionManager")
566        public OAuth2RefreshTokenEntity saveRefreshToken(OAuth2RefreshTokenEntity refreshToken) {
567                return tokenRepository.saveRefreshToken(refreshToken);
568        }
569
570        /**
571         * @return the tokenEnhancer
572         */
573        public TokenEnhancer getTokenEnhancer() {
574                return tokenEnhancer;
575        }
576
577        /**
578         * @param tokenEnhancer the tokenEnhancer to set
579         */
580        public void setTokenEnhancer(TokenEnhancer tokenEnhancer) {
581                this.tokenEnhancer = tokenEnhancer;
582        }
583
584        @Override
585        public OAuth2AccessTokenEntity getRegistrationAccessTokenForClient(ClientDetailsEntity client) {
586                List<OAuth2AccessTokenEntity> allTokens = getAccessTokensForClient(client);
587
588                for (OAuth2AccessTokenEntity token : allTokens) {
589                        if ((token.getScope().contains(SystemScopeService.REGISTRATION_TOKEN_SCOPE) || token.getScope().contains(SystemScopeService.RESOURCE_TOKEN_SCOPE))
590                                        && token.getScope().size() == 1) {
591                                // if it only has the registration scope, then it's a registration token
592                                return token;
593                        }
594                }
595
596                return null;
597        }
598
599
600
601}