@@ -11,7 +11,9 @@ class Client
1111 * Default Portier broker origin.
1212 * @var string
1313 */
14- const DEFAULT_BROKER = 'https://broker.portier.io ' ;
14+ public const DEFAULT_BROKER = 'https://broker.portier.io ' ;
15+
16+ private const REQUIRED_CLAIMS = ['iss ' , 'aud ' , 'exp ' , 'iat ' , 'email ' , 'nonce ' ];
1517
1618 private $ store ;
1719 private $ redirectUri ;
@@ -50,19 +52,79 @@ public function __construct(StoreInterface $store, string $redirectUri)
5052 * `authenticate`, normalization is already part of the authentication
5153 * process.
5254 *
53- * This is currently implemented by making an HTTP call to Portier, without
54- * cache.
55+ * For PHP 7.3 with the intl extension, this function can process the email
56+ * list locally. Otherwise, note that this function makes an HTTP call to
57+ * the Portier broker, without result caching.
58+ *
59+ * Use `hasNormalizeLocal` to check if local normalization is available at
60+ * run-time, or directly use `normalizeLocal` to force-or-fail local
61+ * normalization.
5562 *
5663 * @param string[] $emails Email addresses to normalize.
5764 * @return string[] Normalized email addresses, empty strings for invalid.
5865 */
5966 public function normalize (array $ emails ): array
6067 {
61- $ res = $ this ->store ->guzzle ->post (
62- $ this ->broker . '/normalize ' ,
63- ['body ' => implode ("\n" , $ emails )]
68+ if (self ::hasNormalizeLocal ()) {
69+ return array_map ([self ::class, 'normalizeLocal ' ], $ emails );
70+ } else {
71+ $ res = $ this ->store ->guzzle ->post (
72+ $ this ->broker . '/normalize ' ,
73+ ['body ' => implode ("\n" , $ emails )]
74+ );
75+ return explode ("\n" , (string ) $ res ->getBody ());
76+ }
77+ }
78+
79+ /**
80+ * Normalize an email address. (Pure-PHP version)
81+ *
82+ * This method is useful when comparing user input to an email address
83+ * returned in a Portier token. It is not necessary to call this before
84+ * `authenticate`, normalization is already part of the authentication
85+ * process.
86+ *
87+ * This function requires PHP 7.3 with the intl extension.
88+ */
89+ public static function normalizeLocal (string $ email ): string
90+ {
91+ // Repeat these checks here, so PHPStan understands.
92+ assert (defined ('MB_CASE_FOLD ' ) && function_exists ('idn_to_ascii ' ));
93+
94+ $ localEnd = strrpos ($ email , '@ ' );
95+ if ($ localEnd === false ) {
96+ return '' ;
97+ }
98+
99+ $ local = mb_convert_case (
100+ substr ($ email , 0 , $ localEnd ),
101+ MB_CASE_FOLD
102+ );
103+ if (empty ($ local )) {
104+ return '' ;
105+ }
106+
107+ $ host = idn_to_ascii (
108+ substr ($ email , $ localEnd + 1 ),
109+ IDNA_USE_STD3_RULES | IDNA_CHECK_BIDI ,
110+ INTL_IDNA_VARIANT_UTS46
64111 );
65- return explode ("\n" , (string ) $ res ->getBody ());
112+ if (empty ($ host ) || $ host [0 ] === '[ ' ||
113+ filter_var ($ host , FILTER_VALIDATE_IP ) !== false ) {
114+ return '' ;
115+ }
116+
117+ return sprintf ('%s@%s ' , $ local , $ host );
118+ }
119+
120+ /**
121+ * Check whether `normalizeLocal` can be used on this PHP installation.
122+ *
123+ * The `normalizeLocal` function requires PHP 7.3 with the intl extension.
124+ */
125+ public static function hasNormalizeLocal (): bool
126+ {
127+ return defined ('MB_CASE_FOLD ' ) && function_exists ('idn_to_ascii ' );
66128 }
67129
68130 /**
@@ -129,6 +191,14 @@ public function verify(string $token): string
129191 throw new \Exception ('Token signature did not validate ' );
130192 }
131193
194+ // Check that the required token claims are set.
195+ $ missing = array_filter (self ::REQUIRED_CLAIMS , function (string $ name ) use ($ token ) {
196+ return !$ token ->hasClaim ($ name );
197+ });
198+ if (!empty ($ missing )) {
199+ throw new \Exception (sprintf ('Token is missing claims: %s ' , implode (', ' , $ missing )));
200+ }
201+
132202 // Validate the token claims.
133203 $ vdata = new \Lcobucci \JWT \ValidationData ();
134204 $ vdata ->setIssuer ($ this ->broker );
@@ -137,11 +207,13 @@ public function verify(string $token): string
137207 throw new \Exception ('Token claims did not validate ' );
138208 }
139209
140- // Get the email and consume the nonce.
210+ // Consume the nonce.
141211 $ nonce = $ token ->getClaim ('nonce ' );
142- $ email = $ token ->getClaim ('sub ' );
143- $ this ->store ->consumeNonce ($ nonce , $ email );
212+ $ email = $ token ->getClaim ('email ' );
213+ $ emailOriginal = $ token ->getClaim ('email_original ' , $ email );
214+ $ this ->store ->consumeNonce ($ nonce , $ emailOriginal );
144215
216+ // Return the normalized email.
145217 return $ email ;
146218 }
147219
0 commit comments