Skip to content

Commit

Permalink
feat: support first_screen, extra_params and direct_sign_in params (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
darcyYe authored Nov 28, 2024
1 parent bfbfe74 commit cdb25fb
Show file tree
Hide file tree
Showing 7 changed files with 321 additions and 23 deletions.
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

0 comments on commit cdb25fb

Please sign in to comment.