001/* 002 * Copyright 2023 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.plugins.captchetat.captcha; 017 018import java.io.UnsupportedEncodingException; 019import java.nio.charset.StandardCharsets; 020import java.time.Duration; 021import java.util.ArrayList; 022import java.util.HashMap; 023import java.util.List; 024import java.util.Map; 025 026import org.apache.avalon.framework.activity.Initializable; 027import org.apache.avalon.framework.component.Component; 028import org.apache.avalon.framework.service.ServiceException; 029import org.apache.avalon.framework.service.ServiceManager; 030import org.apache.avalon.framework.service.Serviceable; 031import org.apache.commons.lang3.BooleanUtils; 032import org.apache.commons.lang3.tuple.Pair; 033import org.apache.http.HttpEntity; 034import org.apache.http.NameValuePair; 035import org.apache.http.client.config.RequestConfig; 036import org.apache.http.client.entity.UrlEncodedFormEntity; 037import org.apache.http.client.methods.CloseableHttpResponse; 038import org.apache.http.client.methods.HttpPost; 039import org.apache.http.entity.ContentType; 040import org.apache.http.entity.StringEntity; 041import org.apache.http.impl.client.CloseableHttpClient; 042import org.apache.http.impl.client.HttpClientBuilder; 043import org.apache.http.message.BasicNameValuePair; 044import org.apache.http.util.EntityUtils; 045 046import org.ametys.core.cache.AbstractCacheManager; 047import org.ametys.core.cache.Cache; 048import org.ametys.core.util.JSONUtils; 049import org.ametys.runtime.config.Config; 050import org.ametys.runtime.i18n.I18nizableText; 051import org.ametys.runtime.plugin.component.AbstractLogEnabled; 052 053 054/** 055 * Captcha implementation with images 056 */ 057public class CaptchEtatHelper extends AbstractLogEnabled implements Component, Initializable, Serviceable 058{ 059 060 /** Role */ 061 public static final String ROLE = CaptchEtatHelper.class.getName(); 062 063 /** id of the token cache */ 064 public static final String CAPCHETAT_TOKEN_CACHE = CaptchetatReader.class.getName(); 065 066 067 private static final String CAPTCHA_CLIENT_ID = "captchetat.client_id"; 068 private static final String CAPTCHA_CLIENT_SECRET = "captchetat.client_secret"; 069 private static final String CAPTCHA_ENDPOINT = "captchetat.endpoint"; 070 071 private static final Map<String, String> CAPTCHA_ENDPOINT_AUTH = Map.of( 072 "production", "https://oauth.piste.gouv.fr/api/oauth/token", 073 "sandbox", "https://sandbox-oauth.piste.gouv.fr/api/oauth/token" 074 ); 075 private static final Map<String, String> CAPTCHA_ENDPOINT_PISTE = Map.of( 076 "production", "https://api.piste.gouv.fr/piste/captcha/", 077 "sandbox", "https://sandbox-api.piste.gouv.fr/piste/captcha/" 078 ); 079 080 private AbstractCacheManager _cacheManager; 081 private CloseableHttpClient _httpClient; 082 private JSONUtils _jsonUtils; 083 084 @Override 085 public void service(ServiceManager smanager) throws ServiceException 086 { 087 _jsonUtils = (JSONUtils) smanager.lookup(JSONUtils.ROLE); 088 _cacheManager = (AbstractCacheManager) smanager.lookup(AbstractCacheManager.ROLE); 089 } 090 091 /** 092 * Initialize 093 */ 094 @Override 095 public void initialize() 096 { 097 // A little bit less than an hour, so the token is still valid after user submit the captcha 098 Duration duration = Duration.ofMinutes(50); 099 if (!_cacheManager.hasCache(CAPCHETAT_TOKEN_CACHE)) 100 { 101 _cacheManager.createMemoryCache(CAPCHETAT_TOKEN_CACHE, 102 new I18nizableText("plugin.captchetat", "PLUGINS_CAPTCHETAT_CACHE_TOKEN_LABEL"), 103 new I18nizableText("plugin.captchetat", "PLUGINS_CAPTCHETAT_CACHE_TOKEN_DESCRIPTION"), 104 true, 105 duration); 106 } 107 108 RequestConfig config = RequestConfig.custom() 109 .setConnectTimeout(20000) 110 .setConnectionRequestTimeout(20000) 111 .setSocketTimeout(20000).build(); 112 113 _httpClient = HttpClientBuilder.create() 114 .setDefaultRequestConfig(config) 115 .disableRedirectHandling() 116 .useSystemProperties() 117 .build(); 118 119 120 } 121 122 private Cache<Pair<String, String>, String> _getCache() 123 { 124 return _cacheManager.get(CAPCHETAT_TOKEN_CACHE); 125 } 126 127 private String _getToken(Pair<String, String> key) 128 { 129 return _getCache().get(key, this::_computeToken); 130 } 131 132 private String _computeToken(Pair<String, String> key) 133 { 134 135 String url = CAPTCHA_ENDPOINT_AUTH.get(Config.getInstance().getValue(CAPTCHA_ENDPOINT)); 136 HttpPost httpPost = new HttpPost(url); 137 List<NameValuePair> pairs = new ArrayList<>(); 138 pairs.add(new BasicNameValuePair("grant_type", "client_credentials")); 139 pairs.add(new BasicNameValuePair("client_id", key.getLeft())); 140 pairs.add(new BasicNameValuePair("client_secret", key.getRight())); 141 pairs.add(new BasicNameValuePair("scope", "piste.captchetat")); 142 143 try 144 { 145 httpPost.setEntity(new UrlEncodedFormEntity(pairs)); 146 } 147 catch (UnsupportedEncodingException e) 148 { 149 throw new RuntimeException("Error while building request, can't compute token", e); 150 } 151 152 try (CloseableHttpResponse response = _httpClient.execute(httpPost)) 153 { 154 switch (response.getStatusLine().getStatusCode()) 155 { 156 case 200: 157 break; 158 159 case 403: 160 throw new IllegalStateException("The CMS back-office refused the connection"); 161 162 case 500: 163 default: 164 throw new IllegalStateException("The captchEtat token generator server returned an error"); 165 } 166 HttpEntity entity = response.getEntity(); 167 String responseString = EntityUtils.toString(entity, "UTF-8"); 168 Map<String, Object> result = _jsonUtils.convertJsonToMap(responseString); 169 return (String) result.get("access_token"); 170 } 171 catch (Exception e) 172 { 173 throw new RuntimeException("Error during request, can't compute token", e); 174 } 175 } 176 177 /** 178 * get Token 179 * @return token 180 */ 181 public String getToken() 182 { 183 return _getToken(Pair.of(Config.getInstance().getValue(CAPTCHA_CLIENT_ID), Config.getInstance().getValue(CAPTCHA_CLIENT_SECRET))); 184 } 185 186 /** 187 * get Token 188 * @return token 189 */ 190 public String getEndpoint() 191 { 192 return CAPTCHA_ENDPOINT_PISTE.get(Config.getInstance().getValue(CAPTCHA_ENDPOINT)); 193 } 194 195 /** 196 * Check if the captcha is correct 197 * @param key the key 198 * @param value the value 199 * @return true if correct 200 */ 201 public boolean checkAndInvalidateCaptcha(String key, String value) 202 { 203 String url = getEndpoint() + "valider-captcha"; 204 HttpPost httpPost = new HttpPost(url); 205 List<NameValuePair> pairs = new ArrayList<>(); 206 pairs.add(new BasicNameValuePair("id", key)); 207 pairs.add(new BasicNameValuePair("code", value)); 208 String token = getToken(); 209 httpPost.addHeader("Authorization", "Bearer " + token); 210 httpPost.addHeader("accept", "application/json"); 211 httpPost.addHeader("Content-Type", "application/json"); 212 213 Map<String, Object> parameters = new HashMap<>(); 214 parameters.put("id", key); 215 parameters.put("code", value); 216 String json = _jsonUtils.convertObjectToJson(parameters); 217 HttpEntity jsonEntity = new StringEntity(json, ContentType.create("application/json", StandardCharsets.UTF_8)); 218 219 httpPost.setEntity(jsonEntity); 220 221 try (CloseableHttpResponse response = _httpClient.execute(httpPost)) 222 { 223 switch (response.getStatusLine().getStatusCode()) 224 { 225 case 200: 226 break; 227 case 403: 228 throw new IllegalStateException("The CMS back-office refused the connection"); 229 case 500: 230 default: 231 throw new IllegalStateException("The captchEtat verification server returned an error"); 232 } 233 HttpEntity entity = response.getEntity(); 234 String responseString = EntityUtils.toString(entity, "UTF-8"); 235 return BooleanUtils.toBoolean(responseString); 236 } 237 catch (Exception e) 238 { 239 getLogger().error("Error during request, can't verify captcha", e); 240 } 241 242 return false; 243 } 244 245}