# Authentification FranceConnect dans WebCOnsumerApp via TmsSsoClientBUndle ## TmsSsoClientBundle ### 1. Configuration #### a- TmsSsoClientBundle/Resources/config/routing.yml france_connect_login: path: /login/check-france-connect #### b- TmsSsoClientBundle/Resources/config/services.yml hwi_oauth.resource_owner.tms_france_connect.class: Tms\Bundle\SsoClientBundle\OAuth\ResourceOwner\TmsFranceConnectResourceOwner ... wi_oauth.abstract_resource_owner.tms_france_connect: class: '%hwi_oauth.resource_owner.tms_france_connect.class%' parent: hwi_oauth.abstract_resource_owner.oauth2 abstract: true ### 2. ResourceOwner #### a-TmsSsoClientBundle/OAuth/ResourceOwner/TmsFranceConnectResourceOwner.php ##### * Les options 'client_id' : identifiant fourni par France Connect, 'client_secret' : code fourni par France Connect, 'open_id_session_token' : récupere par la fonction getRandomToken(), 'open_id_session_nonce' => récupere par la fonction getRandomToken(), 'base_url' : dev => https://fcp.integ01.dev-franceconnect.fr/api/v1/a, prod => https://app.franceconnect.gouv.fr/api/v1/ 'display' => null, 'sup_scope' => null, 'scope' => null, 'csrf' => false, 'user_response_class' => 'HWI\Bundle\OAuthBundle\OAuth\Response\PathUserResponse', 'auth_with_one_url' => false, 'access_token_url' => false, 'use_bearer_authorization' => null, 'infos_url' : dev => https://fcp.integ01.dev-franceconnect.fr/api/v1/userinfo, prod => https://app.franceconnect.gouv.fr/api/v1/userinfo "certificates" : chemain vers le fichier certificates.pem 'api_token' : token générer du manager pour authoriser les signin dans SsoManager ##### * Execution: 1- getAuthorizationUrl public function getAuthorizationUrl($redirectUri, array $extraParameters = array()) { $state = "token={$this->options['open_id_session_token']}"; $scope = "openid%20".str_replace(",","%20",$this->options['sup_scope']).'%20'; $info=array("response_type"=>'code', "client_id"=> $this->options['client_id'], "scope"=>$scope, "redirect_uri"=> $redirectUri, "state"=>urlencode($state), "nonce"=> $this->options['open_id_session_nonce'] ); $url = $this->getURLforService("authorize"); foreach($info as $key=>$value){ $url.=$key."=".$value."&"; } return $url; } créer l'url de redirection vers les ervice FranceConnect. exemple : https://fcp.integ01.dev-franceconnect.fr/api/v1/authorize?client_id=123456&redirect_uri=http://aide-installation-personnel.odr.docker/&scope=openid%20profile%20email%20address%20phone%20preferred_username%20 2- Choix du service d'authentification ![](https://i.imgur.com/4ow82l0.png) 3- Se connecter ![](https://i.imgur.com/o8xX5Ya.png) 3- retour sur le site ($redirectUri qui daot etres renseigner sur https://partenaires.franceconnect.gouv.fr/) ![](https://i.imgur.com/dqFM9F7.png) 4- Recuperation du token d'accée public function getAccessToken(Request $request, $redirectUri, array $extraParameters = array()) { $parameters = array_merge(array( 'code' => $request->query->get('code'), 'grant_type' => 'authorization_code', 'client_id' => $this->options['client_id'], 'client_secret' => $this->options['client_secret'], 'redirect_uri' => $redirectUri, ), $extraParameters); $response = $this->doGetTokenRequest($this->options['access_token_url'], $parameters); $response = $this->getCurlWrapperResponseContent($response); $this->validateResponseContent($response); return $response; } Récuperation du token par la fonction "doGetTokenRequest" via l'url de l'option "access_token_url" en ajoutant comme parameters : - code : fournit par france connect - client_id - client_secret - redirect_uri protected function doGetTokenRequest($url, array $parameters = array()) { return $this->httpRequest($url, $parameters); } protected function httpRequest($url, $content = array(), $headers = array(), $method = null) { if (null === $method) { $method = null === $content ? CurlWrapper::METHOD_GET : CurlWrapper::METHOD_POST; } $curlWrapper = new CurlWrapper(); $curlWrapper->setServerCertificate($this->options['certificates']); $curlWrapper->setPostDataUrlEncode($content); $token_url = $this->options['access_token_url']; $result = $curlWrapper->get($token_url); return $result; } La fonction httpRequest signe les headers de la requette avec la certificat "certificates" 5- Récuperation du contenu de la reponse via la fonction getCurlWrapperResponseContent protected function getCurlWrapperResponseContent($content) { if (!$content) { return array(); } $response = json_decode($content, true); if (JSON_ERROR_NONE !== json_last_error()) { parse_str($content, $response); } return $response; } 6- Validation du contenu de la reponse protected function validateResponseContent($response) { if (isset($response['error_description'])) { throw new AuthenticationException(sprintf('OAuth error: "%s"', $response['error_description'])); } if (isset($response['error'])) { throw new AuthenticationException(sprintf('OAuth error: "%s"', isset($response['error']['message']) ? $response['error']['message'] : $response['error'])); } if (!isset($response['access_token'])) { throw new AuthenticationException('Not a valid access token.'); } } 7- Recuperation des infos utilisateur via la fonction getUserInformation public function getUserInformation(array $accessToken, array $extraParameters = array()) { $curlWrapper = new CurlWrapper(); $curlWrapper->setServerCertificate($this->options['certificates']); $curlWrapper->addHeader("Authorization", "Bearer ".$accessToken['access_token']); $user_info_url = $this->getURLforService("userinfo"); $result = $curlWrapper->get($user_info_url); $response = $this->getUserResponse(); $response->setResponse($result); $response->setResourceOwner($this); $response->setOAuthToken(new OAuthToken($accessToken)); return $response; } #### d- TmsSsoClientBundle/Manager/CurlWrapper.php <?php /** * Created by PhpStorm. * User: tveron * Date: 01/08/2016 * Time: 12:00 */ namespace Tms\Bundle\SsoClientBundle\Manager; class CurlWrapper { const POST_DATA_SEPARATOR = "\r\n"; private $curlHandle; private $lastError; private $postData; private $postFile; private $postFileProperties; const METHOD_OPTIONS = 'OPTIONS'; const METHOD_GET = 'GET'; const METHOD_HEAD = 'HEAD'; const METHOD_POST = 'POST'; const METHOD_PUT = 'PUT'; const METHOD_DELETE = 'DELETE'; const METHOD_PATCH = 'PATCH'; public function __construct($proxy = null) { $this->curlHandle = curl_init(); $this->setProperties(CURLOPT_RETURNTRANSFER, 1); $this->setProperties(CURLOPT_FOLLOWLOCATION, 1); $this->setProperties(CURLOPT_MAXREDIRS, 5); if(!empty($proxy)) { $this->setProperties(CURLOPT_HTTPPROXYTUNNEL, 1); $this->setProperties(CURLOPT_PROXY, $proxy); } $this->postFile = array(); $this->postData = array(); curl_setopt($this->curlHandle, CURLOPT_SSL_VERIFYPEER, true); } public function __destruct() { curl_close($this->curlHandle); } public function httpAuthentication($username, $password) { $this->setProperties(CURLOPT_USERPWD, "$username:$password"); $this->setProperties(CURLOPT_HTTPAUTH, CURLAUTH_BASIC); } public function addHeader($name, $value) { $this->setProperties(CURLOPT_HTTPHEADER, array("$name: $value")); } public function getLastError() { return $this->lastError; } private function setProperties($properties, $values) { curl_setopt($this->curlHandle, $properties, $values); } public function setAccept($format) { $curlHttpHeader[] = "Accept: $format"; $this->setProperties(CURLOPT_HTTPHEADER, $curlHttpHeader); } public function dontVerifySSLCACert() { $this->setProperties(CURLOPT_SSL_VERIFYHOST, 0); $this->setProperties(CURLOPT_SSL_VERIFYPEER, 0); } public function setServerCertificate($serverCertificate) { $this->setProperties(CURLOPT_CAINFO, $serverCertificate); $this->setProperties(CURLOPT_SSL_VERIFYPEER, 0); } public function setClientCertificate($clientCertificate, $clientKey, $clientKeyPassword) { $this->setProperties(CURLOPT_SSLCERT, $clientCertificate); $this->setProperties(CURLOPT_SSLKEY, $clientKey); $this->setProperties(CURLOPT_SSLKEYPASSWD, $clientKeyPassword); } public function get($url) { $this->setProperties(CURLOPT_URL, $url); if ($this->postData || $this->postFile) { $this->curlSetPostData(); } curl_setopt($this->curlHandle, CURLINFO_HEADER_OUT, true); $output = curl_exec($this->curlHandle); //print_r(curl_getinfo($this->curlHandle,CURLINFO_HEADER_OUT)); $this->lastError = curl_error($this->curlHandle); if ($this->lastError) { $this->lastError = "Erreur de connexion au serveur : " . $this->lastError; return false; } return $output; } public function addPostData($name, $value) { if (!isset($this->postData[$name])) { $this->postData[$name] = array(); } $this->postData[$name][] = $value; } public function setPostDataUrlEncode(array $post_data) { $pd = array(); foreach ($post_data as $k => $v) { $pd[] = "$k=$v"; } $pd = implode("&", $pd); $this->setProperties(CURLOPT_POST, true); $this->setProperties(CURLOPT_POSTFIELDS, $pd); $this->setProperties(CURLOPT_HTTPHEADER, array('Content-Type: application/x-www-form-urlencoded')); } public function addPostFile($field, $filePath, $fileName = false, $contentType = "application/octet-stream", $contentTransferEncoding = false) { if (!$fileName) { $fileName = basename($filePath); } $this->postFile[$field][$fileName] = $filePath; $this->postFileProperties[$field][$fileName] = array($contentType, $contentTransferEncoding); } private function getBoundary() { return '----------------------------' . substr(sha1('CurlWrapper' . microtime()), 0, 12); } private function curlSetPostData() { $this->setProperties(CURLOPT_POST, true); if ($this->isPostDataWithSimilarName()) { $this->curlSetPostDataWithSimilarFilename(); } else { $this->curlPostDataStandard(); } } private function isPostDataWithSimilarName() { $array = array(); foreach ($this->postData as $name => $multipleValue) { foreach ($multipleValue as $data) { if (isset($array[$name])) { return true; } $array[$name] = true; } } foreach ($this->postFile as $name => $multipleValue) { foreach ($multipleValue as $data) { if (isset($array[$name])) { return true; } $array[$name] = true; } } } private function curlPostDataStandard() { $post = array(); foreach ($this->postData as $name => $multipleValue) { foreach ($multipleValue as $value) { $post[$name] = $value; } } foreach ($this->postFile as $name => $multipleValue) { foreach ($multipleValue as $fileName => $filePath) { $post[$name] = "@$filePath;filename=$fileName"; } } curl_setopt($this->curlHandle, CURLOPT_POSTFIELDS, $post); } private function curlSetPostDataWithSimilarFilename() { $boundary = $this->getBoundary(); $body = array(); foreach ($this->postData as $name => $multipleValue) { foreach ($multipleValue as $value) { $body[] = "--$boundary"; $body[] = "Content-Disposition: form-data; name=$name"; $body[] = ''; $body[] = $value; } } foreach ($this->postFile as $name => $multipleValue) { foreach ($multipleValue as $fileName => $filePath) { $body[] = "--$boundary"; $body[] = "Content-Disposition: form-data; name=$name; filename=\"$fileName\""; $body[] = "Content-Type: {$this->postFileProperties[$name][$fileName][0]}"; if ($this->postFileProperties[$name][$fileName][1]) { $body[] = "Content-Transfer-Encoding: {$this->postFileProperties[$name][$fileName][1]}"; } $body[] = ''; $body[] = file_get_contents($filePath); } } $body[] = "--$boundary--"; $body[] = ''; $content = join(self::POST_DATA_SEPARATOR, $body); $curlHttpHeader[] = 'Content-Length: ' . strlen($content); $curlHttpHeader[] = 'Expect: 100-continue'; $curlHttpHeader[] = "Content-Type: multipart/form-data; boundary=$boundary"; $this->setProperties(CURLOPT_HTTPHEADER, $curlHttpHeader); $this->setProperties(CURLOPT_POSTFIELDS, $content); } public function getHTTPCode() { return curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE); } } #### e- TmsSsoClientBundle/Model/User.php ##### * Attributs /** * France Connect identifier * * @var string */ protected $franceConnectId = null; /** * user phone * * @var string */ protected $phone = null; /** * user birthdate * * @var string */ protected $birthdate = null; /** * user birthplace * * @var string */ protected $birthplace = null; ##### * Récuperation des info utilisateur "setFromResponse" } else if ($resourceOwner instanceof TmsFranceConnectResourceOwner) { $this->franceConnectId = $raw['sub']; $this->username = isset($raw['email']) ? $raw['email'] : null; $this->gender = isset($raw['gender']) ? (('male' == $raw['gender']) ? 1: 2) : 0; $this->firstName = isset($raw['given_name']) ? $raw['given_name'] : null; $this->lastName = isset($raw['family_name']) ? $raw['family_name'] : null; $this->phone = isset($raw['phone']) ? $raw['phone'] : null; $this->birthdate = isset($raw['birthdate']) ? $raw['birthdate'] : null; $this->birthplace = isset($raw['birthplace']) ? $raw['birthplace'] : null; } ## WebConsumerApp ### 1- Configuration #### a- WebConsumerApp/app/config/config.yml tms_france_connect: type: tms_france_connect scope: "email profile" options: display: popup base_url: "%dgafp.aipe.france_connect.base_url%" sup_scope: "%dgafp.aipe.france_connect.sup_scope%" access_token_url: "%dgafp.aipe.france_connect.access_token_url%" infos_url: "%dgafp.aipe.france_connect.infos_url%" certificates: "%dgafp.aipe.france_connect.certificates%" identity: selector: tms_web_security.identity.customer tokens: dgafp: client_id: "%dgafp.aipe.france_connect.id%" client_secret: "%dgafp.aipe.france_connect.secret%" api_token: "%dgafp.aipe.france_connect.api_token%" #### b- WebConsumerApp/app/config/parameters.yml dgafp.aipe.france_connect.id: 1553bc08000a5c65e18a2d8481e8e1b8991f3c253445331141eb4f13e3b89b5b dgafp.aipe.france_connect.secret: ea1ce2c9d09b2be59e5fae9930de7c542e5b807f18e38dc2e1f40ac6e05397c8 dgafp.aipe.france_connect.base_url: 'https://fcp.integ01.dev-franceconnect.fr/api/v1/' dgafp.aipe.france_connect.sup_scope: 'profile,email,preferred_username,birthdate,birthplace' dgafp.aipe.france_connect.access_token_url: 'https://fcp.integ01.dev-franceconnect.fr/api/v1/token' dgafp.aipe.france_connect.infos_url: 'https://fcp.integ01.dev-franceconnect.fr/api/v1/userinfo' dgafp.aipe.france_connect.certificates: /home/bouraoui/Documents/certificates/certificates.pem dgafp.aipe.france_connect.api_token: 1g89fhc3opokkggkwckg8gs48k08go840ggs848c0kgwg88ggc #### c- WebConsumerApp/app/config/routing.yml france_connect_login: path: /oauth/login/check-france-connect #### d- WebConsumerApp/app/config/security.yml oauth_secured_area: pattern: ^/ oauth: resource_owners: tms: /oauth/login/check-tms tms_facebook: /oauth/login/check-facebook tms_google: /oauth/login/check-google tms_france_connect: /oauth/login/check-france-connect external: /oauth/login/check-external check_path: tms_login login_path: tms_websecurity_connect_login failure_path: tms_websecurity_connect_login success_handler: tms_web_portal.listener.login oauth_user_provider: service: da_oauth_client.user_provider.memory logout: success_handler: tms_web_portal.listener.logout anonymous: ~ #### e-WebConsumerApp/src/Tms/WebPortalBundle/Resources/themes/dgafp.aipe/views/User/login.html.twig <a href="{{ host ~ path("hwi_oauth_service_redirect", {service: "tms_france_connect"}) }}" title="Se connecter avec FranceConnect" class="button social france-connect "></a>