@Override protected FacebookProfile retrieveUserProfileFromToken(final Token accessToken) { String body = sendRequestForData(accessToken, getProfileUrl(accessToken)); if (body == null) { throw new HttpCommunicationException("Not data found for accessToken : " + accessToken); } final FacebookProfile profile = extractUserProfile(body); addAccessTokenToProfile(profile, accessToken); if (profile != null && this.requiresExtendedToken) { String url = CommonHelper.addParameter(EXCHANGE_TOKEN_URL, OAuthConstants.CLIENT_ID, this.key); url = CommonHelper.addParameter(url, OAuthConstants.CLIENT_SECRET, this.secret); url = addExchangeToken(url, accessToken); final ProxyOAuthRequest request = createProxyRequest(url); final long t0 = System.currentTimeMillis(); final Response response = request.send(); final int code = response.getCode(); body = response.getBody(); final long t1 = System.currentTimeMillis(); logger.debug("Request took : " + (t1 - t0) + " ms for : " + url); logger.debug("response code : {} / response body : {}", code, body); if (code == 200) { logger.debug("Retrieve extended token from : {}", body); final Token extendedAccessToken = this.api20.getAccessTokenExtractor().extract(body); logger.debug("Extended token : {}", extendedAccessToken); addAccessTokenToProfile(profile, extendedAccessToken); } else { logger.error("Cannot get extended token : {} / {}", code, body); } } return profile; }
@Override protected void internalInit() { extractor = new BasicAuthExtractor(headerName, prefixHeader, getName()); super.internalInit(); CommonHelper.assertNotBlank("callbackUrl", this.callbackUrl); CommonHelper.assertNotBlank("realmName", this.realmName); }
@Override protected void internalInit(final WebContext context) { CommonHelper.assertNotBlank("callbackUrl", this.callbackUrl); if (CommonHelper.isBlank(this.casLoginUrl) && CommonHelper.isBlank(this.casPrefixUrl)) { throw new TechnicalException("casLoginUrl and casPrefixUrl cannot be both blank"); } initializeClientConfiguration(); initializeLogoutHandler(context); if (this.casProtocol == CasProtocol.CAS10) { initializeCas10Protocol(); } else if (this.casProtocol == CasProtocol.CAS20) { initializeCas20Protocol(context); } else if (this.casProtocol == CasProtocol.CAS20_PROXY) { initializeCas20ProxyProtocol(context); } else if (this.casProtocol == CasProtocol.CAS30) { initializeCas30Protocol(context); } else if (this.casProtocol == CasProtocol.CAS30_PROXY) { initializeCas30ProxyProtocol(context); } else if (this.casProtocol == CasProtocol.SAML) { initializeSAMLProtocol(); } addAuthorizationGenerator(new DefaultCasAuthorizationGenerator<CasProfile>()); }
/** * Extracts digest Authorization header components. As per RFC 2617 : username is the user's name * in the specified realm qop is quality of protection uri is the request uri response is the * client response nonce is a server-specified data string which should be uniquely generated each * time a 401 response is made cnonce is the client nonce nc is the nonce count If in the * Authorization header it is not specified a username and response, we throw CredentialsException * because the client uses an username and a password to authenticate. response is just a MD5 * encoded value based on user provided password and RFC 2617 digest authentication encoding rules * * @param context the current web context * @return the Digest credentials */ @Override public DigestCredentials extract(WebContext context) throws HttpAction { final TokenCredentials credentials = this.extractor.extract(context); if (credentials == null) { return null; } String token = credentials.getToken(); Map<String, String> valueMap = parseTokenValue(token); String username = valueMap.get("username"); String response = valueMap.get("response"); if (CommonHelper.isBlank(username) || CommonHelper.isBlank(response)) { throw new CredentialsException("Bad format of the digest auth header"); } String realm = valueMap.get("realm"); String nonce = valueMap.get("nonce"); String uri = valueMap.get("uri"); String cnonce = valueMap.get("cnonce"); String nc = valueMap.get("nc"); String qop = valueMap.get("qop"); String method = context.getRequestMethod(); return new DigestCredentials( response, method, clientName, username, realm, nonce, uri, cnonce, nc, qop); }
public Result callback() { final PlayWebContext context = new PlayWebContext(ctx(), config.getSessionStore()); CommonHelper.assertNotNull("config", config); CommonHelper.assertNotNull("config.httpActionAdapter", config.getHttpActionAdapter()); final Clients clients = config.getClients(); CommonHelper.assertNotNull("clients", clients); final Client client = clients.findClient(context); logger.debug("client: {}", client); CommonHelper.assertNotNull("client", client); CommonHelper.assertTrue( client instanceof IndirectClient, "only indirect clients are allowed on the callback url"); final Credentials credentials; try { credentials = client.getCredentials(context); } catch (final RequiresHttpAction e) { return (Result) config.getHttpActionAdapter().adapt(e.getCode(), context); } logger.debug("credentials: {}", credentials); final UserProfile profile = client.getUserProfile(credentials, context); logger.debug("profile: {}", profile); saveUserProfile(context, profile); return redirectToOriginallyRequestedUrl(context); }
protected void initializeClientConfiguration() { if (this.casPrefixUrl != null && !this.casPrefixUrl.endsWith("/")) { this.casPrefixUrl += "/"; } if (CommonHelper.isBlank(this.casPrefixUrl)) { this.casPrefixUrl = this.casLoginUrl.replaceFirst("/login$", "/"); } else if (CommonHelper.isBlank(this.casLoginUrl)) { this.casLoginUrl = this.casPrefixUrl + "login"; } }
public String convert(final Object attribute) { if (attribute != null && attribute instanceof String) { final String s = (String) attribute; if (CommonHelper.isNotBlank(s) && CommonHelper.isNotBlank(this.regex) && CommonHelper.isNotBlank(this.replacement)) { return s.replaceAll(this.regex, this.replacement); } } return null; }
protected void initializeClientConfiguration(final WebContext context) { if (this.casPrefixUrl != null && !this.casPrefixUrl.endsWith("/")) { this.casPrefixUrl += "/"; } if (CommonHelper.isBlank(this.casPrefixUrl)) { this.casPrefixUrl = this.casLoginUrl.replaceFirst("/login$", "/"); } else if (CommonHelper.isBlank(this.casLoginUrl)) { this.casLoginUrl = this.casPrefixUrl + "login"; } this.casPrefixUrl = callbackUrlResolver.compute(this.casPrefixUrl, context); this.casLoginUrl = callbackUrlResolver.compute(this.casLoginUrl, context); }
@Override public boolean isAuthorized( final WebContext context, final UserProfile profile, final String authorizerName, final Map<String, Authorizer> authorizersMap) { final List<Authorizer> authorizers = new ArrayList<>(); // if we have an authorizer name (which may be a list of authorizer names) if (CommonHelper.isNotBlank(authorizerName)) { final String[] names = authorizerName.split(Pac4jConstants.ELEMENT_SEPRATOR); final int nb = names.length; for (int i = 0; i < nb; i++) { final String name = names[i]; if ("hsts".equalsIgnoreCase(name)) { authorizers.add(STRICT_TRANSPORT_SECURITY_HEADER); } else if ("nosniff".equalsIgnoreCase(name)) { authorizers.add(X_CONTENT_TYPE_OPTIONS_HEADER); } else if ("noframe".equalsIgnoreCase(name)) { authorizers.add(X_FRAME_OPTIONS_HEADER); } else if ("xssprotection".equalsIgnoreCase(name)) { authorizers.add(XSS_PROTECTION_HEADER); } else if ("nocache".equalsIgnoreCase(name)) { authorizers.add(CACHE_CONTROL_HEADER); } else if ("securityheaders".equalsIgnoreCase(name)) { authorizers.add(CACHE_CONTROL_HEADER); authorizers.add(X_CONTENT_TYPE_OPTIONS_HEADER); authorizers.add(STRICT_TRANSPORT_SECURITY_HEADER); authorizers.add(X_FRAME_OPTIONS_HEADER); authorizers.add(XSS_PROTECTION_HEADER); } else if ("csrfToken".equalsIgnoreCase(name)) { authorizers.add(CSRF_TOKEN_GENERATOR_AUTHORIZER); } else if ("csrfCheck".equalsIgnoreCase(name)) { authorizers.add(CSRF_AUTHORIZER); } else if ("csrf".equalsIgnoreCase(name)) { authorizers.add(CSRF_TOKEN_GENERATOR_AUTHORIZER); authorizers.add(CSRF_AUTHORIZER); } else { // we must have authorizers CommonHelper.assertNotNull("authorizersMap", authorizersMap); final Authorizer result = authorizersMap.get(name); // we must have an authorizer defined for this name CommonHelper.assertNotNull("authorizersMap['" + name + "']", result); authorizers.add(result); } } } return isAuthorized(context, profile, authorizers); }
@Override public String getName() { if (CommonHelper.isBlank(this.name)) { return this.getClass().getSimpleName(); } return this.name; }
@Override protected void verifyProfile(UserProfile userProfile) { final WordPressProfile profile = (WordPressProfile) userProfile; logger.debug("userProfile : {}", profile); assertEquals("35944437", profile.getId()); assertEquals( WordPressProfile.class.getName() + UserProfile.SEPARATOR + "35944437", profile.getTypedId()); assertTrue(ProfileHelper.isTypedIdOf(profile.getTypedId(), WordPressProfile.class)); assertTrue(CommonHelper.isNotBlank(profile.getAccessToken())); assertCommonProfile( userProfile, "*****@*****.**", null, null, "testscribeup", "testscribeup", Gender.UNSPECIFIED, null, "https://0.gravatar.com/avatar/67c3844a672979889c1e3abbd8c4eb22?s=96&d=identicon&r=G", "http://en.gravatar.com/testscribeup", null); assertEquals(36224958, profile.getPrimaryBlog().intValue()); final WordPressLinks links = profile.getLinks(); assertEquals("https://public-api.wordpress.com/rest/v1/me", links.getSelf()); assertEquals("https://public-api.wordpress.com/rest/v1/me/help", links.getHelp()); assertEquals("https://public-api.wordpress.com/rest/v1/sites/36224958", links.getSite()); assertEquals(8, profile.getAttributes().size()); }
@Override public String toString() { return CommonHelper.toString( this.getClass(), "callbackUrl", this.callbackUrl, "casLoginUrl", this.casLoginUrl, "casPrefixUrl", this.casPrefixUrl, "casProtocol", this.casProtocol, "renew", this.renew, "gateway", this.gateway, "logoutHandler", this.logoutHandler, "acceptAnyProxy", this.acceptAnyProxy, "allowedProxyChains", this.allowedProxyChains, "casProxyReceptor", this.casProxyReceptor); }
@Override public boolean isAuthorized(final WebContext context, final UserProfile profile) { CommonHelper.assertNotNull("pattern", pattern); final String ip = context.getRemoteAddr(); return this.pattern.matcher(ip).matches(); }
@Override protected void internalInit() { super.internalInit(); CommonHelper.assertNotBlank("scope", this.scope); CommonHelper.assertNotBlank("fields", this.fields); StateApi20 api20 = new LinkedInApi20(); this.service = new LinkedInOAuth20ServiceImpl( api20, new OAuthConfig( this.key, this.secret, this.callbackUrl, SignatureType.Header, this.scope, null), this.connectTimeout, this.readTimeout, this.proxyHost, this.proxyPort); }
@Override protected void verifyProfile(UserProfile userProfile) { final Google2Profile profile = (Google2Profile) userProfile; assertEquals("113675986756217860428", profile.getId()); assertEquals( Google2Profile.class.getName() + UserProfile.SEPARATOR + "113675986756217860428", profile.getTypedId()); assertTrue(ProfileHelper.isTypedIdOf(profile.getTypedId(), Google2Profile.class)); assertTrue(CommonHelper.isNotBlank(profile.getAccessToken())); assertCommonProfile( userProfile, "*****@*****.**", "Jérôme", "ScribeUP", "Jérôme ScribeUP", null, Gender.MALE, Locale.ENGLISH, "https://lh4.googleusercontent.com/-fFUNeYqT6bk/AAAAAAAAAAI/AAAAAAAAAAA/5gBL6csVWio/photo.jpg", "https://plus.google.com/113675986756217860428", null); assertNull(profile.getBirthday()); assertTrue(profile.getEmails() != null && profile.getEmails().size() == 1); assertEquals(9, profile.getAttributes().size()); }
@Override protected void verifyProfile(CommonProfile userProfile) { final PayPalProfile profile = (PayPalProfile) userProfile; assertEquals("YAxf5WKSFn4BG_l3wqcBJUSObQTG1Aww5FY0EDf_ccw", profile.getId()); assertEquals( PayPalProfile.class.getName() + CommonProfile.SEPARATOR + "YAxf5WKSFn4BG_l3wqcBJUSObQTG1Aww5FY0EDf_ccw", profile.getTypedId()); assertTrue(ProfileHelper.isTypedIdOf(profile.getTypedId(), PayPalProfile.class)); assertTrue(CommonHelper.isNotBlank(profile.getAccessToken())); assertCommonProfile( userProfile, "*****@*****.**", "Test", "ScribeUP", "Test ScribeUP", null, Gender.UNSPECIFIED, Locale.FRANCE, null, null, "Europe/Berlin"); final PayPalAddress address = profile.getAddress(); assertEquals("FR", address.getCountry()); assertEquals("Paris", address.getLocality()); assertEquals("75001", address.getPostalCode()); assertEquals("Adr1", address.getStreetAddress()); final Locale language = profile.getLanguage(); assertEquals(Locale.FRANCE, language); assertEquals(9, profile.getAttributes().size()); }
@Override protected void verifyProfile(UserProfile userProfile) { final TwitterProfile profile = (TwitterProfile) userProfile; assertEquals("488358057", profile.getId()); assertEquals( TwitterProfile.class.getName() + UserProfile.SEPARATOR + "488358057", profile.getTypedId()); assertTrue(ProfileHelper.isTypedIdOf(profile.getTypedId(), TwitterProfile.class)); assertTrue(CommonHelper.isNotBlank(profile.getAccessToken())); assertCommonProfile( userProfile, null, null, null, "test scribeUP", "testscribeUP", Gender.UNSPECIFIED, Locale.UK, ".twimg.com/sticky/default_profile_images/default_profile_5_normal.png", "http://t.co/fNjYqp7wZ8", "New York"); assertFalse(profile.getContributorsEnabled()); assertEquals( TestsHelper.getFormattedDate(1328872224000L, "EEE MMM dd HH:mm:ss Z yyyy", Locale.US), profile.getCreatedAt().toString()); assertTrue(profile.getDefaultProfile()); assertTrue(profile.getDefaultProfileImage()); assertEquals("biographie", profile.getDescription()); assertEquals(0, profile.getFavouritesCount().intValue()); assertFalse(profile.getFollowRequestSent()); assertEquals(0, profile.getFollowersCount().intValue()); assertFalse(profile.getFollowing()); assertEquals(0, profile.getFriendsCount().intValue()); assertFalse(profile.getGeoEnabled()); assertFalse(profile.getIsTranslator()); assertEquals(0, profile.getListedCount().intValue()); assertFalse(profile.getNotifications()); assertTrue(profile.getProfileBackgroundColor() instanceof Color); assertTrue( profile.getProfileBackgroundImageUrl().contains(".twimg.com/images/themes/theme1/bg.png")); assertTrue( profile.getProfileBackgroundImageUrlHttps().endsWith("/images/themes/theme1/bg.png")); assertFalse(profile.getProfileBackgroundTile()); assertTrue( profile .getProfileImageUrlHttps() .endsWith("/sticky/default_profile_images/default_profile_5_normal.png")); assertTrue(profile.getProfileLinkColor() instanceof Color); assertTrue(profile.getProfileSidebarBorderColor() instanceof Color); assertTrue(profile.getProfileSidebarFillColor() instanceof Color); assertTrue(profile.getProfileTextColor() instanceof Color); assertTrue(profile.getProfileUseBackgroundImage()); assertTrue(profile.getProtected()); assertNull(profile.getShowAllInlineMedia()); assertEquals(0, profile.getStatusesCount().intValue()); assertEquals("Amsterdam", profile.getTimeZone()); assertEquals(3600, profile.getUtcOffset().intValue()); assertFalse(profile.getVerified()); assertNotNull(profile.getAccessSecret()); assertEquals(37, profile.getAttributes().size()); }
@Override protected void internalInit(final WebContext context) { super.internalInit(context); CommonHelper.assertNotNull("scope", this.scope); if (this.scope == Google2Scope.EMAIL) { this.scopeValue = this.EMAIL_SCOPE; } else if (this.scope == Google2Scope.PROFILE) { this.scopeValue = this.PROFILE_SCOPE; } else { this.scopeValue = this.PROFILE_SCOPE + " " + this.EMAIL_SCOPE; } this.service = new StateOAuth20ServiceImpl( new GoogleApi20(), new OAuthConfig( this.key, this.secret, computeFinalCallbackUrl(context), SignatureType.Header, this.scopeValue, null), this.connectTimeout, this.readTimeout, this.proxyHost, this.proxyPort, false, true); }
@Override protected boolean hasBeenCancelled(final WebContext context) { final String denied = context.getRequestParameter("denied"); if (CommonHelper.isNotBlank(denied)) { return true; } else { return false; } }
public CredentialProvider( final String name, final String storePasswd, final String privateKeyPasswd) { InputStream inputStream = CommonHelper.getInputStreamFromName(name); KeyStore keyStore = loadKeyStore(inputStream, storePasswd); this.privateKey = getPrivateKeyAlias(keyStore); Map<String, String> passwords = new HashMap<String, String>(); passwords.put(this.privateKey, privateKeyPasswd); this.credentialResolver = new KeyStoreCredentialResolver(keyStore, passwords); }
protected Result redirectToOriginallyRequestedUrl(final WebContext context) { final String requestedUrl = (String) context.getSessionAttribute(Pac4jConstants.REQUESTED_URL); logger.debug("requestedUrl: {}", requestedUrl); if (CommonHelper.isNotBlank(requestedUrl)) { context.setSessionAttribute(Pac4jConstants.REQUESTED_URL, null); return redirect(requestedUrl); } else { return redirect(this.defaultUrl); } }
private String buildCallbackUrlForAuthorizationCodeResponseType( final Authentication authentication, final Service service, final String redirectUri) { final OAuthCode code = this.oAuthCodeFactory.create(service, authentication); logger.debug("Generated OAuth code: {}", code); this.ticketRegistry.addTicket(code); final String state = authentication.getAttributes().get(OAuthConstants.STATE).toString(); final String nonce = authentication.getAttributes().get(OAuthConstants.NONCE).toString(); String callbackUrl = redirectUri; callbackUrl = CommonHelper.addParameter(callbackUrl, OAuthConstants.CODE, code.getId()); if (StringUtils.isNotBlank(state)) { callbackUrl = CommonHelper.addParameter(callbackUrl, OAuthConstants.STATE, state); } if (StringUtils.isNotBlank(nonce)) { callbackUrl = CommonHelper.addParameter(callbackUrl, OAuthConstants.NONCE, nonce); } return callbackUrl; }
@Override public String toString() { return CommonHelper.toString( this.getClass(), "requestToken", this.requestToken, "token", this.token, "verifier", this.verifier, "clientName", getClientName()); }
@Override public void validate(final TokenCredentials credentials) throws HttpAction { if (credentials == null) { throw new CredentialsException("credentials must not be null"); } if (CommonHelper.isBlank(credentials.getToken())) { throw new CredentialsException("token must not be blank"); } final String token = credentials.getToken(); final CommonProfile profile = new CommonProfile(); profile.setId(token); credentials.setUserProfile(profile); }
/** * The code in this method is based on this blog post: * https://www.sammyk.me/the-single-most-important-way-to-make-your-facebook-app-more-secure and * this answer: * https://stackoverflow.com/questions/7124735/hmac-sha256-algorithm-for-signature-calculation * * @param url the URL to which we're adding the proof * @param token the application token we pass back and forth * @return URL with the appsecret_proof parameter added */ protected String computeAppSecretProof(String url, Token token) { try { Mac sha256_HMAC = Mac.getInstance("HmacSHA256"); SecretKeySpec secret_key = new SecretKeySpec(this.secret.getBytes("UTF-8"), "HmacSHA256"); sha256_HMAC.init(secret_key); String proof = org.apache.commons.codec.binary.Hex.encodeHexString( sha256_HMAC.doFinal(token.getToken().getBytes("UTF-8"))); url = CommonHelper.addParameter(url, APPSECRET_PARAMETER, proof); return url; } catch (Exception e) { throw new TechnicalException("Unable to compute appsecret_proof", e); } }
@Override public boolean isAuthorized( final WebContext context, final UserProfile profile, final List<Authorizer> authorizers) { // authorizations check comes after authentication and profile must not be null CommonHelper.assertNotNull("profile", profile); if (authorizers != null && authorizers.size() > 0) { // check authorizations using authorizers: all must be satisfied for (Authorizer authorizer : authorizers) { if (!authorizer.isAuthorized(context, profile)) { return false; } } } return true; }
@Override public String toString() { return CommonHelper.toString( this.getClass(), "callbackUrl", this.callbackUrl, "name", getName(), "realmName", this.realmName, "headerName", this.headerName, "prefixHeader", this.prefixHeader, "authenticator", getAuthenticator(), "profileCreator", getProfileCreator()); }
@Override public String toString() { return CommonHelper.toString( this.getClass(), "name", getName(), "callbackUrl", getCallbackUrl(), "callbackUrlResolver", getCallbackUrlResolver(), "ajaxRequestResolver", getAjaxRequestResolver(), "redirectActionBuilder", getRedirectActionBuilder(), "credentialsExtractor", getCredentialsExtractor(), "authenticator", getAuthenticator(), "profileCreator", getProfileCreator(), "configuration", this.configuration); }
@Override protected void internalInit(final WebContext context) { super.internalInit(context); CommonHelper.assertNotBlank("fields", this.fields); this.api20 = new ExtendedFacebookApi(); if (StringUtils.isNotBlank(this.scope)) { this.service = new StateOAuth20ServiceImpl( this.api20, new OAuthConfig( this.key, this.secret, computeFinalCallbackUrl(context), SignatureType.Header, this.scope, null), this.connectTimeout, this.readTimeout, this.proxyHost, this.proxyPort); } else { this.service = new StateOAuth20ServiceImpl( this.api20, new OAuthConfig( this.key, this.secret, computeFinalCallbackUrl(context), SignatureType.Header, null, null), this.connectTimeout, this.readTimeout, this.proxyHost, this.proxyPort); } }
@Override protected void internalInit() { CommonHelper.assertNotBlank("callbackUrl", this.callbackUrl); CommonHelper.assertNotNull("logoutHandler", this.logoutHandler); if (CommonHelper.isBlank(this.casLoginUrl) && CommonHelper.isBlank(this.casPrefixUrl)) { throw new TechnicalException("casLoginUrl and casPrefixUrl cannot be both blank"); } if (this.casPrefixUrl != null && !this.casPrefixUrl.endsWith("/")) { this.casPrefixUrl += "/"; } if (CommonHelper.isBlank(this.casPrefixUrl)) { this.casPrefixUrl = this.casLoginUrl.replaceFirst("/login", "/"); } else if (CommonHelper.isBlank(this.casLoginUrl)) { this.casLoginUrl = this.casPrefixUrl + "login"; } if (this.casProtocol == CasProtocol.CAS10) { this.ticketValidator = new Cas10TicketValidator(this.casPrefixUrl); } else if (this.casProtocol == CasProtocol.CAS20) { this.ticketValidator = new Cas20ServiceTicketValidator(this.casPrefixUrl); if (this.casProxyReceptor != null) { final Cas20ServiceTicketValidator cas20ServiceTicketValidator = (Cas20ServiceTicketValidator) this.ticketValidator; cas20ServiceTicketValidator.setProxyCallbackUrl(this.casProxyReceptor.getCallbackUrl()); cas20ServiceTicketValidator.setProxyGrantingTicketStorage( this.casProxyReceptor.getProxyGrantingTicketStorage()); } } else if (this.casProtocol == CasProtocol.CAS20_PROXY) { this.ticketValidator = new Cas20ProxyTicketValidator(this.casPrefixUrl); final Cas20ProxyTicketValidator cas20ProxyTicketValidator = (Cas20ProxyTicketValidator) this.ticketValidator; cas20ProxyTicketValidator.setAcceptAnyProxy(this.acceptAnyProxy); cas20ProxyTicketValidator.setAllowedProxyChains(this.allowedProxyChains); if (this.casProxyReceptor != null) { cas20ProxyTicketValidator.setProxyCallbackUrl(this.casProxyReceptor.getCallbackUrl()); cas20ProxyTicketValidator.setProxyGrantingTicketStorage( this.casProxyReceptor.getProxyGrantingTicketStorage()); } } else if (this.casProtocol == CasProtocol.SAML) { this.ticketValidator = new Saml11TicketValidator(this.casPrefixUrl); } }