11<?php
22
3+ /*
4+ * Copyright (c) 2018 Adil Kachbat and contributors
5+ * Copyright (c) 2023-2026 Gecka
6+ *
7+ * For the full copyright and license notice, please view the LICENSE
8+ * file that was distributed with this source code.
9+ */
10+
311namespace SocialiteProviders \NcConnect ;
412
513use GuzzleHttp \RequestOptions ;
614use Illuminate \Support \Arr ;
715use Illuminate \Support \Str ;
816use Laravel \Socialite \Two \InvalidStateException ;
917use SocialiteProviders \Manager \OAuth2 \AbstractProvider ;
18+ use UnexpectedValueException ;
1019
11- /**
12- * @see Provider's documentation: https://docs.google.com/document/d/13zo1E1eVMFUmbV6ECw2YvTiM-uPBL0DBuWh5wLtzCpA/edit?pli=1
13- */
1420class Provider extends AbstractProvider
1521{
16- /**
17- * API URLs.
18- */
19- public const PROD_BASE_URL = 'https://connect.gouv.nc/v2/ ' ;
22+ public const PROD_BASE_URL = 'https://connect.gouv.nc/v3/realms/nc-connect/ ' ;
2023
21- public const TEST_BASE_URL = 'https://connect-dev.gouv.nc/v2 / ' ;
24+ public const TEST_BASE_URL = 'https://connect-dev.gouv.nc/v3/realms/nc-connect / ' ;
2225
2326 public const IDENTIFIER = 'NCCONNECT ' ;
2427
25- /**
26- * The scopes being requested.
27- *
28- * @var array
29- */
3028 protected $ scopes = [
3129 'openid ' ,
3230 'identite_pivot ' ,
3331 'profile ' ,
3432 'email ' ,
3533 ];
3634
37- /**
38- * {@inheritdoc}
39- */
4035 protected $ scopeSeparator = ' ' ;
4136
42- /**
43- * Return API Base URL.
44- *
45- * @return string
46- */
47- protected function getBaseUrl ()
37+ protected function getBaseUrl (): string
4838 {
49- return config ('app.env ' ) === 'production ' && ! $ this ->getConfig ('force_dev ' ) ? self ::PROD_BASE_URL : self ::TEST_BASE_URL ;
39+ return app ()->environment ('production ' ) && ! $ this ->getConfig ('force_dev ' )
40+ ? self ::PROD_BASE_URL
41+ : self ::TEST_BASE_URL ;
5042 }
5143
52- /**
53- * {@inheritdoc}
54- */
55- public static function additionalConfigKeys ()
44+ public static function additionalConfigKeys (): array
5645 {
57- return ['logout_redirect ' , 'force_dev ' ];
46+ return ['logout_redirect ' , 'force_dev ' , ' auth_method ' ];
5847 }
5948
60- /**
61- * {@inheritdoc}
62- */
63- protected function getAuthUrl ($ state )
49+ protected function getAuthUrl ($ state ): string
6450 {
65- //It is used to prevent replay attacks
66- $ this ->parameters ['nonce ' ] = Str::random (20 );
51+ $ nonce = Str::random (40 );
52+ $ this ->parameters ['nonce ' ] = $ nonce ;
53+ $ this ->request ->session ()->put ('ncconnect_nonce ' , $ nonce );
54+
55+ return $ this ->buildAuthUrlFromBase (
56+ $ this ->getBaseUrl ().'protocol/openid-connect/auth ' ,
57+ $ state
58+ );
59+ }
6760
68- return $ this ->buildAuthUrlFromBase ($ this ->getBaseUrl ().'/authorize ' , $ state );
61+ protected function getTokenUrl (): string
62+ {
63+ return $ this ->getBaseUrl ().'protocol/openid-connect/token ' ;
6964 }
7065
7166 /**
72- * {@inheritdoc}
67+ * Build HTTP request options with the configured auth method.
7368 */
74- protected function getTokenUrl ()
69+ protected function buildTokenRequestOptions ( array $ params ): array
7570 {
76- return $ this ->getBaseUrl ().'/token ' ;
71+ if ($ this ->getConfig ('auth_method ' ) === 'client_secret_post ' ) {
72+ $ params ['client_id ' ] = $ this ->clientId ;
73+ $ params ['client_secret ' ] = $ this ->clientSecret ;
74+
75+ return [RequestOptions::FORM_PARAMS => $ params ];
76+ }
77+
78+ return [
79+ RequestOptions::HEADERS => [
80+ 'Authorization ' => 'Basic ' .base64_encode ($ this ->clientId .': ' .$ this ->clientSecret ),
81+ ],
82+ RequestOptions::FORM_PARAMS => $ params ,
83+ ];
7784 }
7885
79- /**
80- * {@inheritdoc}
81- */
82- public function getAccessTokenResponse ($ code )
86+ public function getAccessTokenResponse ($ code ): array
8387 {
84- $ response = $ this ->getHttpClient ()->post ($ this ->getBaseUrl ().'/token ' , [
85- RequestOptions::HEADERS => ['Authorization ' => 'Basic ' .base64_encode ($ this ->clientId .': ' .$ this ->clientSecret )],
86- RequestOptions::FORM_PARAMS => $ this ->getTokenFields ($ code ),
87- ]);
88+ $ params = [
89+ 'grant_type ' => 'authorization_code ' ,
90+ 'code ' => $ code ,
91+ 'redirect_uri ' => $ this ->redirectUrl ,
92+ ];
93+
94+ $ response = $ this ->getHttpClient ()->post (
95+ $ this ->getTokenUrl (),
96+ $ this ->buildTokenRequestOptions ($ params )
97+ );
8898
8999 return json_decode ((string ) $ response ->getBody (), true );
90100 }
@@ -94,70 +104,121 @@ public function getAccessTokenResponse($code)
94104 */
95105 public function user ()
96106 {
97- if ($ this ->hasInvalidState ()) {
98- throw new InvalidStateException ();
99- }
107+ $ user = parent ::user ();
100108
101- $ response = $ this -> getAccessTokenResponse ($ this ->getCode () );
109+ $ idToken = Arr:: get ($ this ->credentialsResponseBody , ' id_token ' );
102110
103- $ user = $ this ->mapUserToObject ($ this ->getUserByToken (
104- $ token = Arr::get ($ response , 'access_token ' )
105- ));
111+ $ this ->validateNonce ($ idToken );
106112
107- //store tokenId session for logout url generation
108- $ this ->request ->session ()->put ('fc_token_id ' , Arr::get ($ response , 'id_token ' ));
113+ if ($ user instanceof User) {
114+ $ user ->setTokenId ($ idToken );
115+ }
109116
110- return $ user ->setTokenId (Arr::get ($ response , 'id_token ' ))
111- ->setToken ($ token )
112- ->setRefreshToken (Arr::get ($ response , 'refresh_token ' ))
113- ->setExpiresIn (Arr::get ($ response , 'expires_in ' ));
117+ return $ user ;
114118 }
115119
116120 /**
117- * {@inheritdoc}
121+ * Validate the nonce claim in the id_token against the session value.
122+ *
123+ * @throws InvalidStateException
118124 */
119- protected function getUserByToken ( $ token )
125+ protected function validateNonce (? string $ idToken ): void
120126 {
121- $ response = $ this ->getHttpClient ()->get ($ this ->getBaseUrl ().'/userinfo ' , [
122- RequestOptions::HEADERS => [
123- 'Authorization ' => 'Bearer ' .$ token ,
124- ],
125- ]);
127+ $ expectedNonce = $ this ->request ->session ()->pull ('ncconnect_nonce ' );
128+
129+ if (! $ idToken || ! $ expectedNonce ) {
130+ throw new InvalidStateException ('Missing nonce or id_token. ' );
131+ }
132+
133+ $ parts = explode ('. ' , $ idToken );
134+
135+ if (count ($ parts ) !== 3 ) {
136+ throw new InvalidStateException ('Invalid id_token format. ' );
137+ }
138+
139+ $ payload = json_decode (base64_decode (strtr ($ parts [1 ], '-_ ' , '+/ ' )), true );
140+
141+ if (! is_array ($ payload ) || ($ payload ['nonce ' ] ?? null ) !== $ expectedNonce ) {
142+ throw new InvalidStateException ('Invalid nonce in id_token. ' );
143+ }
144+ }
145+
146+ protected function getUserByToken ($ token ): array
147+ {
148+ $ response = $ this ->getHttpClient ()->get (
149+ $ this ->getBaseUrl ().'protocol/openid-connect/userinfo ' ,
150+ [RequestOptions::HEADERS => ['Authorization ' => 'Bearer ' .$ token ]]
151+ );
126152
127153 return json_decode ((string ) $ response ->getBody (), true );
128154 }
129155
130- /**
131- * {@inheritdoc}
132- */
133- protected function mapUserToObject (array $ user )
156+ protected function mapUserToObject (array $ user ): User
134157 {
135- return (new User ())->setRaw ($ user )->map ([
136- 'id ' => $ user ['sub ' ],
137- 'email ' => $ user ['email ' ],
138- 'email_verified ' => $ user ['email_verified ' ],
139- 'verified ' => $ user ['verified ' ] ?? 0 ,
140- 'preferred_username ' => $ user ['preferred_username ' ] ?? '' ,
158+ if (empty ($ user ['sub ' ])) {
159+ throw new UnexpectedValueException ('Missing "sub" claim in userinfo response. ' );
160+ }
141161
142- 'given_name ' => $ user ['given_name ' ] ?? '' ,
143- 'family_name ' => $ user ['family_name ' ] ?? '' ,
144- 'birthdate ' => $ user ['birthdate ' ] ?? '' ,
145- 'gender ' => $ user ['gender ' ] ?? '' ,
146- 'birthplace ' => $ user ['birthplace ' ] ?? '' ,
147- 'birthcountry ' => $ user ['birthcountry ' ] ?? '' ,
162+ return (new User )->setRaw ($ user )->map ([
163+ 'id ' => $ user ['sub ' ],
164+ 'email ' => $ user ['email ' ] ?? null ,
165+ 'email_verified ' => $ user ['email_verified ' ] ?? false ,
166+ 'verified ' => $ user ['verified ' ] ?? 0 ,
167+ 'preferred_username ' => $ user ['preferred_username ' ] ?? '' ,
168+ 'given_name ' => $ user ['given_name ' ] ?? '' ,
169+ 'first_name ' => $ user ['first_name ' ] ?? '' ,
170+ 'family_name ' => $ user ['family_name ' ] ?? '' ,
171+ 'birthdate ' => $ user ['birthdate ' ] ?? '' ,
172+ 'gender ' => $ user ['gender ' ] ?? '' ,
173+ 'birthplace ' => $ user ['birthplace ' ] ?? '' ,
148174 ]);
149175 }
150176
151177 /**
152- * Generate logout URL for redirection to NcConnect.
178+ * {@inheritdoc}
153179 */
154- public function generateLogoutURL ()
180+ protected function getRefreshTokenResponse ( $ refreshToken ): array
155181 {
156182 $ params = [
157- 'post_logout_redirect_uri ' => $ this -> getConfig ( ' logout_redirect ' ) ,
158- 'id_token_hint ' => $ this -> request -> session ()-> get ( ' fc_token_id ' ) ,
183+ 'grant_type ' => ' refresh_token ' ,
184+ 'refresh_token ' => $ refreshToken ,
159185 ];
160186
161- return $ this ->getBaseUrl ().'/logout? ' .http_build_query ($ params );
187+ $ response = $ this ->getHttpClient ()->post (
188+ $ this ->getTokenUrl (),
189+ $ this ->buildTokenRequestOptions ($ params )
190+ );
191+
192+ return json_decode ((string ) $ response ->getBody (), true );
193+ }
194+
195+ /**
196+ * Generate logout URL for redirection to NcConnect.
197+ *
198+ * Without arguments, returns the bare logout endpoint.
199+ * With an id_token_hint and/or redirect URI, builds a full RP-Initiated Logout URL.
200+ */
201+ public function generateLogoutURL (?string $ idTokenHint = null , ?string $ postLogoutRedirectUri = null ): string
202+ {
203+ $ logoutUrl = $ this ->getBaseUrl ().'protocol/openid-connect/logout ' ;
204+
205+ $ redirectUri = $ postLogoutRedirectUri ?? $ this ->getConfig ('logout_redirect ' );
206+
207+ if ($ redirectUri === null && $ idTokenHint === null ) {
208+ return $ logoutUrl ;
209+ }
210+
211+ $ params = [];
212+
213+ if ($ redirectUri !== null ) {
214+ $ params ['client_id ' ] = $ this ->clientId ;
215+ $ params ['post_logout_redirect_uri ' ] = $ redirectUri ;
216+ }
217+
218+ if ($ idTokenHint !== null ) {
219+ $ params ['id_token_hint ' ] = $ idTokenHint ;
220+ }
221+
222+ return $ logoutUrl .'? ' .http_build_query ($ params );
162223 }
163224}
0 commit comments