Skip to content

Commit cdb25fb

Browse files
authored
feat: support first_screen, extra_params and direct_sign_in params (#13)
1 parent bfbfe74 commit cdb25fb

File tree

7 files changed

+321
-23
lines changed

7 files changed

+321
-23
lines changed

samples/index.php

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,25 @@
1111
use Logto\Sdk\LogtoClient;
1212
use Logto\Sdk\LogtoConfig;
1313
use Logto\Sdk\Constants\UserScope;
14+
use Logto\Sdk\InteractionMode;
15+
use Logto\Sdk\Models\DirectSignInOptions;
16+
use Logto\Sdk\Constants\DirectSignInMethod;
17+
use Logto\Sdk\Constants\FirstScreen;
18+
use Logto\Sdk\Constants\AuthenticationIdentifier;
19+
use Logto\Sdk\Oidc\OidcCore;
1420

1521
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/../');
1622
$dotenv->load();
1723

24+
// Set the SSL verification options for PHP before creating the LogtoClient
25+
$contextOptions = [
26+
'ssl' => [
27+
'verify_peer' => false,
28+
'verify_peer_name' => false,
29+
],
30+
];
31+
stream_context_set_default($contextOptions);
32+
1833
$resources = ['https://default.logto.app/api', 'https://shopping.api'];
1934
$client = new LogtoClient(
2035
new LogtoConfig(
@@ -30,7 +45,14 @@
3045
case '/':
3146
case null:
3247
if (!$client->isAuthenticated()) {
33-
echo '<a href="/sign-in">Sign in</a>';
48+
// show different sign in options
49+
echo '<h3>Sign In Options:</h3>';
50+
echo '<ul>';
51+
echo '<li><a href="/sign-in">Normal Sign In</a></li>';
52+
echo '<li><a href="/sign-in/sign-up">Sign In (Sign Up First)</a></li>';
53+
echo '<li><a href="/sign-in/social">Sign In with GitHub</a></li>';
54+
echo '<li><a href="/sign-in/email-and-username">Sign In with Email and Username</a></li>';
55+
echo '</ul>';
3456
break;
3557
}
3658

@@ -64,6 +86,31 @@
6486
header('Location: ' . $client->signIn("http://localhost:8080/sign-in-callback"));
6587
exit();
6688

89+
case '/sign-in/sign-up':
90+
header('Location: ' . $client->signIn(
91+
"http://localhost:8080/sign-in-callback",
92+
interactionMode: InteractionMode::signUp
93+
));
94+
exit();
95+
96+
case '/sign-in/social':
97+
header('Location: ' . $client->signIn(
98+
"http://localhost:8080/sign-in-callback",
99+
directSignIn: new DirectSignInOptions(
100+
method: DirectSignInMethod::social,
101+
target: 'github'
102+
)
103+
));
104+
exit();
105+
106+
case '/sign-in/email-and-username':
107+
header('Location: ' . $client->signIn(
108+
"http://localhost:8080/sign-in-callback",
109+
firstScreen: FirstScreen::signIn,
110+
identifiers: [AuthenticationIdentifier::email, AuthenticationIdentifier::username]
111+
));
112+
exit();
113+
67114
case '/sign-in-callback':
68115
$client->handleSignInCallback();
69116
header('Location: /');
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php declare(strict_types=1);
2+
namespace Logto\Sdk\Constants;
3+
4+
/** The identifier type for sign-in. */
5+
enum AuthenticationIdentifier: string
6+
{
7+
case email = 'email';
8+
case phone = 'phone';
9+
case username = 'username';
10+
}

src/Constants/DirectSignInMethod.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php declare(strict_types=1);
2+
namespace Logto\Sdk\Constants;
3+
4+
/** The method to be used for direct sign-in. */
5+
enum DirectSignInMethod: string
6+
{
7+
case social = 'social';
8+
case sso = 'sso';
9+
}

src/Constants/FirstScreen.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php declare(strict_types=1);
2+
namespace Logto\Sdk\Constants;
3+
4+
/** The first screen to show in the sign-in experience. */
5+
enum FirstScreen: string
6+
{
7+
case resetPassword = 'reset_password';
8+
case signIn = 'identifier:sign_in';
9+
case register = 'identifier:register';
10+
case singleSignOn = 'single_sign_on';
11+
}

src/LogtoClient.php

Lines changed: 111 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
use Logto\Sdk\Storage\SessionStorage;
1111
use Logto\Sdk\Storage\Storage;
1212
use Logto\Sdk\Storage\StorageKey;
13+
use Logto\Sdk\Models\DirectSignInOptions;
14+
use Logto\Sdk\Constants\FirstScreen;
15+
use Logto\Sdk\Constants\AuthenticationIdentifier;
1316

1417
/**
1518
* The sign-in session that stores the information for the sign-in callback.
@@ -165,21 +168,71 @@ function getRefreshToken(): ?string
165168
* Returns the sign-in URL for the given redirect URI. You should redirect the user
166169
* to the returned URL to sign in.
167170
*
168-
* By specifying the interaction mode, you can control whether the user will be
169-
* prompted for sign-in or sign-up on the first screen. If the interaction mode is
170-
* not specified, the default one will be used.
171-
*
172-
* @example
171+
* @param string $redirectUri The URI to redirect to after sign-in
172+
* @param ?InteractionMode $interactionMode Controls whether to show sign-in or sign-up UI first
173+
* @param ?DirectSignInOptions $directSignIn Direct sign-in configuration for social or SSO, see details at https://docs.logto.io/docs/references/openid-connect/authentication-parameters/#direct-sign-in
174+
* @param ?FirstScreen $firstScreen Controls which screen to show first (sign-in or register), see details at https://docs.logto.io/docs/references/openid-connect/authentication-parameters/#first-screen
175+
* @param ?array $identifiers Array of authentication identifiers (email, phone, username) to enable, this parameter MUST work with `firstScreen` parameter
176+
* @param ?array $extraParams Additional query parameters to include in the sign-in URL
177+
*
178+
* @example Basic sign-in
173179
* ```php
174180
* header('Location: ' . $client->signIn("https://example.com/callback"));
175181
* ```
182+
*
183+
* @example Sign-in with social provider
184+
* ```php
185+
* $directSignIn = new DirectSignInOptions(
186+
* method: DirectSignInMethod::social,
187+
* target: 'github'
188+
* );
189+
* header('Location: ' . $client->signIn(
190+
* "https://example.com/callback",
191+
* directSignIn: $directSignIn
192+
* ));
193+
* ```
194+
*
195+
* @example Sign-in with specific identifiers
196+
* ```php
197+
* header('Location: ' . $client->signIn(
198+
* "https://example.com/callback",
199+
* firstScreen: FirstScreen::signIn,
200+
* identifiers: [AuthenticationIdentifier::email, AuthenticationIdentifier::username]
201+
* ));
202+
* ```
203+
*
204+
* @example Sign-in with additional parameters
205+
* ```php
206+
* header('Location: ' . $client->signIn(
207+
* "https://example.com/callback",
208+
* extraParams: [
209+
* 'foo' => 'bar',
210+
* 'baz' => 'qux'
211+
* ]
212+
* ));
213+
* ```
176214
*/
177-
function signIn(string $redirectUri, ?InteractionMode $interactionMode = null): string
178-
{
215+
function signIn(
216+
string $redirectUri,
217+
?InteractionMode $interactionMode = null,
218+
?DirectSignInOptions $directSignIn = null,
219+
?FirstScreen $firstScreen = null,
220+
?array $identifiers = null,
221+
?array $extraParams = null
222+
): string {
179223
$codeVerifier = $this->oidcCore::generateCodeVerifier();
180224
$codeChallenge = $this->oidcCore::generateCodeChallenge($codeVerifier);
181225
$state = $this->oidcCore::generateState();
182-
$signInUrl = $this->buildSignInUrl($redirectUri, $codeChallenge, $state, $interactionMode);
226+
$signInUrl = $this->buildSignInUrl(
227+
$redirectUri,
228+
$codeChallenge,
229+
$state,
230+
$interactionMode,
231+
$directSignIn,
232+
$firstScreen,
233+
$identifiers,
234+
$extraParams
235+
);
183236

184237
foreach (StorageKey::cases() as $key) {
185238
$this->storage->delete($key);
@@ -293,11 +346,20 @@ public function fetchUserInfo(): UserInfoResponse
293346
return $this->oidcCore->fetchUserInfo($accessToken);
294347
}
295348

296-
protected function buildSignInUrl(string $redirectUri, string $codeChallenge, string $state, ?InteractionMode $interactionMode): string
297-
{
349+
protected function buildSignInUrl(
350+
string $redirectUri,
351+
string $codeChallenge,
352+
string $state,
353+
?InteractionMode $interactionMode,
354+
?DirectSignInOptions $directSignIn = null,
355+
?FirstScreen $firstScreen = null,
356+
?array $identifiers = null,
357+
?array $extraParams = null
358+
): string {
298359
$pickValue = function (string|\BackedEnum $value): string {
299360
return $value instanceof \BackedEnum ? $value->value : $value;
300361
};
362+
301363
$config = $this->config;
302364
$scopes = array_unique(
303365
array_map($pickValue, array_merge($config->scopes ?: [], $this->oidcCore::DEFAULT_SCOPES))
@@ -308,7 +370,8 @@ protected function buildSignInUrl(string $redirectUri, string $codeChallenge, st
308370
: ($config->resources ?: [])
309371
);
310372

311-
$query = http_build_query([
373+
// Build the base query parameters
374+
$queryParams = [
312375
'client_id' => $config->appId,
313376
'redirect_uri' => $redirectUri,
314377
'response_type' => 'code',
@@ -317,18 +380,44 @@ protected function buildSignInUrl(string $redirectUri, string $codeChallenge, st
317380
'code_challenge' => $codeChallenge,
318381
'code_challenge_method' => 'S256',
319382
'state' => $state,
320-
'interaction_mode' => $interactionMode?->value,
321-
]);
383+
];
384+
385+
// Add optional parameters
386+
if ($interactionMode !== null) {
387+
$queryParams['interaction_mode'] = $interactionMode->value;
388+
}
389+
390+
if ($firstScreen !== null) {
391+
$queryParams['first_screen'] = $firstScreen->value;
392+
}
393+
394+
// Handle the `identifiers` array parameter
395+
if ($identifiers !== null && count($identifiers) > 0) {
396+
$queryParams['identifier'] = implode(' ', array_map($pickValue, $identifiers));
397+
}
398+
399+
// Handle the `direct_sign_in` parameter
400+
if ($directSignIn !== null) {
401+
$queryParams['direct_sign_in'] = $directSignIn->method->value . ':' . $directSignIn->target;
402+
}
403+
404+
// Merge the extra query parameters
405+
if ($extraParams !== null) {
406+
$queryParams = array_merge($queryParams, $extraParams);
407+
}
408+
409+
// Build the base URL
410+
$url = $this->oidcCore->metadata->authorization_endpoint . '?' . http_build_query($queryParams);
411+
412+
// Add the `resource` parameters
413+
if (count($resources) > 0) {
414+
$url .= '&' . implode('&', array_map(
415+
fn($resource) => "resource=" . urlencode($resource),
416+
$resources
417+
));
418+
}
322419

323-
return $this->oidcCore->metadata->authorization_endpoint .
324-
'?' .
325-
$query .
326-
(
327-
count($resources) > 0 ?
328-
# Resources need to use the same key name as the query string
329-
'&' . implode('&', array_map(fn($resource) => "resource=" . urlencode($resource), $resources)) :
330-
''
331-
);
420+
return $url;
332421
}
333422

334423
protected function setSignInSession(SignInSession $data): void

src/Models/DirectSignInOptions.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php declare(strict_types=1);
2+
namespace Logto\Sdk\Models;
3+
4+
use Logto\Sdk\Constants\DirectSignInMethod;
5+
6+
/** Options for direct sign-in. */
7+
class DirectSignInOptions extends JsonModel
8+
{
9+
public function __construct(
10+
/** The method to be used for the direct sign-in. */
11+
public DirectSignInMethod $method,
12+
/**
13+
* The target to be used for the direct sign-in.
14+
* For `method: 'social'`, it should be the social connector target.
15+
*/
16+
public string $target,
17+
) {
18+
}
19+
}

0 commit comments

Comments
 (0)