Skip to content

feat: support first_screen, extra_params and direct_sign_in params #13

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion samples/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,25 @@
use Logto\Sdk\LogtoClient;
use Logto\Sdk\LogtoConfig;
use Logto\Sdk\Constants\UserScope;
use Logto\Sdk\InteractionMode;
use Logto\Sdk\Models\DirectSignInOptions;
use Logto\Sdk\Constants\DirectSignInMethod;
use Logto\Sdk\Constants\FirstScreen;
use Logto\Sdk\Constants\AuthenticationIdentifier;
use Logto\Sdk\Oidc\OidcCore;

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

// Set the SSL verification options for PHP before creating the LogtoClient
$contextOptions = [
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
],
];
stream_context_set_default($contextOptions);

$resources = ['https://default.logto.app/api', 'https://shopping.api'];
$client = new LogtoClient(
new LogtoConfig(
Expand All @@ -30,7 +45,14 @@
case '/':
case null:
if (!$client->isAuthenticated()) {
echo '<a href="/sign-in">Sign in</a>';
// show different sign in options
echo '<h3>Sign In Options:</h3>';
echo '<ul>';
echo '<li><a href="/sign-in">Normal Sign In</a></li>';
echo '<li><a href="/sign-in/sign-up">Sign In (Sign Up First)</a></li>';
echo '<li><a href="/sign-in/social">Sign In with GitHub</a></li>';
echo '<li><a href="/sign-in/email-and-username">Sign In with Email and Username</a></li>';
echo '</ul>';
break;
}

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

case '/sign-in/sign-up':
header('Location: ' . $client->signIn(
"http://localhost:8080/sign-in-callback",
interactionMode: InteractionMode::signUp
));
exit();

case '/sign-in/social':
header('Location: ' . $client->signIn(
"http://localhost:8080/sign-in-callback",
directSignIn: new DirectSignInOptions(
method: DirectSignInMethod::social,
target: 'github'
)
));
exit();

case '/sign-in/email-and-username':
header('Location: ' . $client->signIn(
"http://localhost:8080/sign-in-callback",
firstScreen: FirstScreen::signIn,
identifiers: [AuthenticationIdentifier::email, AuthenticationIdentifier::username]
));
exit();

case '/sign-in-callback':
$client->handleSignInCallback();
header('Location: /');
Expand Down
10 changes: 10 additions & 0 deletions src/Constants/AuthenticationIdentifier.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace Logto\Sdk\Constants;

/** The identifier type for sign-in. */
enum AuthenticationIdentifier: string
{
case email = 'email';
case phone = 'phone';
case username = 'username';
}
9 changes: 9 additions & 0 deletions src/Constants/DirectSignInMethod.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php declare(strict_types=1);
namespace Logto\Sdk\Constants;

/** The method to be used for direct sign-in. */
enum DirectSignInMethod: string
{
case social = 'social';
case sso = 'sso';
}
11 changes: 11 additions & 0 deletions src/Constants/FirstScreen.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php declare(strict_types=1);
namespace Logto\Sdk\Constants;

/** The first screen to show in the sign-in experience. */
enum FirstScreen: string
{
case resetPassword = 'reset_password';
case signIn = 'identifier:sign_in';
case register = 'identifier:register';
case singleSignOn = 'single_sign_on';
}
133 changes: 111 additions & 22 deletions src/LogtoClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
use Logto\Sdk\Storage\SessionStorage;
use Logto\Sdk\Storage\Storage;
use Logto\Sdk\Storage\StorageKey;
use Logto\Sdk\Models\DirectSignInOptions;
use Logto\Sdk\Constants\FirstScreen;
use Logto\Sdk\Constants\AuthenticationIdentifier;

/**
* The sign-in session that stores the information for the sign-in callback.
Expand Down Expand Up @@ -165,21 +168,71 @@ function getRefreshToken(): ?string
* Returns the sign-in URL for the given redirect URI. You should redirect the user
* to the returned URL to sign in.
*
* By specifying the interaction mode, you can control whether the user will be
* prompted for sign-in or sign-up on the first screen. If the interaction mode is
* not specified, the default one will be used.
*
* @example
* @param string $redirectUri The URI to redirect to after sign-in
* @param ?InteractionMode $interactionMode Controls whether to show sign-in or sign-up UI first
* @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
* @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
* @param ?array $identifiers Array of authentication identifiers (email, phone, username) to enable, this parameter MUST work with `firstScreen` parameter
* @param ?array $extraParams Additional query parameters to include in the sign-in URL
*
* @example Basic sign-in
* ```php
* header('Location: ' . $client->signIn("https://example.com/callback"));
* ```
*
* @example Sign-in with social provider
* ```php
* $directSignIn = new DirectSignInOptions(
* method: DirectSignInMethod::social,
* target: 'github'
* );
* header('Location: ' . $client->signIn(
* "https://example.com/callback",
* directSignIn: $directSignIn
* ));
* ```
*
* @example Sign-in with specific identifiers
* ```php
* header('Location: ' . $client->signIn(
* "https://example.com/callback",
* firstScreen: FirstScreen::signIn,
* identifiers: [AuthenticationIdentifier::email, AuthenticationIdentifier::username]
* ));
* ```
*
* @example Sign-in with additional parameters
* ```php
* header('Location: ' . $client->signIn(
* "https://example.com/callback",
* extraParams: [
* 'foo' => 'bar',
* 'baz' => 'qux'
* ]
* ));
* ```
*/
function signIn(string $redirectUri, ?InteractionMode $interactionMode = null): string
{
function signIn(
string $redirectUri,
?InteractionMode $interactionMode = null,
?DirectSignInOptions $directSignIn = null,
?FirstScreen $firstScreen = null,
?array $identifiers = null,
?array $extraParams = null
): string {
$codeVerifier = $this->oidcCore::generateCodeVerifier();
$codeChallenge = $this->oidcCore::generateCodeChallenge($codeVerifier);
$state = $this->oidcCore::generateState();
$signInUrl = $this->buildSignInUrl($redirectUri, $codeChallenge, $state, $interactionMode);
$signInUrl = $this->buildSignInUrl(
$redirectUri,
$codeChallenge,
$state,
$interactionMode,
$directSignIn,
$firstScreen,
$identifiers,
$extraParams
);

foreach (StorageKey::cases() as $key) {
$this->storage->delete($key);
Expand Down Expand Up @@ -293,11 +346,20 @@ public function fetchUserInfo(): UserInfoResponse
return $this->oidcCore->fetchUserInfo($accessToken);
}

protected function buildSignInUrl(string $redirectUri, string $codeChallenge, string $state, ?InteractionMode $interactionMode): string
{
protected function buildSignInUrl(
string $redirectUri,
string $codeChallenge,
string $state,
?InteractionMode $interactionMode,
?DirectSignInOptions $directSignIn = null,
?FirstScreen $firstScreen = null,
?array $identifiers = null,
?array $extraParams = null
): string {
$pickValue = function (string|\BackedEnum $value): string {
return $value instanceof \BackedEnum ? $value->value : $value;
};

$config = $this->config;
$scopes = array_unique(
array_map($pickValue, array_merge($config->scopes ?: [], $this->oidcCore::DEFAULT_SCOPES))
Expand All @@ -308,7 +370,8 @@ protected function buildSignInUrl(string $redirectUri, string $codeChallenge, st
: ($config->resources ?: [])
);

$query = http_build_query([
// Build the base query parameters
$queryParams = [
'client_id' => $config->appId,
'redirect_uri' => $redirectUri,
'response_type' => 'code',
Expand All @@ -317,18 +380,44 @@ protected function buildSignInUrl(string $redirectUri, string $codeChallenge, st
'code_challenge' => $codeChallenge,
'code_challenge_method' => 'S256',
'state' => $state,
'interaction_mode' => $interactionMode?->value,
]);
];

// Add optional parameters
if ($interactionMode !== null) {
$queryParams['interaction_mode'] = $interactionMode->value;
}

if ($firstScreen !== null) {
$queryParams['first_screen'] = $firstScreen->value;
}

// Handle the `identifiers` array parameter
if ($identifiers !== null && count($identifiers) > 0) {
$queryParams['identifier'] = implode(' ', array_map($pickValue, $identifiers));
}

// Handle the `direct_sign_in` parameter
if ($directSignIn !== null) {
$queryParams['direct_sign_in'] = $directSignIn->method->value . ':' . $directSignIn->target;
}

// Merge the extra query parameters
if ($extraParams !== null) {
$queryParams = array_merge($queryParams, $extraParams);
}

// Build the base URL
$url = $this->oidcCore->metadata->authorization_endpoint . '?' . http_build_query($queryParams);

// Add the `resource` parameters
if (count($resources) > 0) {
$url .= '&' . implode('&', array_map(
fn($resource) => "resource=" . urlencode($resource),
$resources
));
}

return $this->oidcCore->metadata->authorization_endpoint .
'?' .
$query .
(
count($resources) > 0 ?
# Resources need to use the same key name as the query string
'&' . implode('&', array_map(fn($resource) => "resource=" . urlencode($resource), $resources)) :
''
);
return $url;
}

protected function setSignInSession(SignInSession $data): void
Expand Down
19 changes: 19 additions & 0 deletions src/Models/DirectSignInOptions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php declare(strict_types=1);
namespace Logto\Sdk\Models;

use Logto\Sdk\Constants\DirectSignInMethod;

/** Options for direct sign-in. */
class DirectSignInOptions extends JsonModel
{
public function __construct(
/** The method to be used for the direct sign-in. */
public DirectSignInMethod $method,
/**
* The target to be used for the direct sign-in.
* For `method: 'social'`, it should be the social connector target.
*/
public string $target,
) {
}
}
Loading