From a48d9472f9f8ea874208601e0f0248e096c8a702 Mon Sep 17 00:00:00 2001 From: iamdadmin Date: Mon, 12 Jan 2026 14:33:43 +0000 Subject: [PATCH 1/6] fix(auth): replace deprecated library --- composer.json | 2 +- packages/auth/composer.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 70449dbeb..4d1bba882 100644 --- a/composer.json +++ b/composer.json @@ -51,7 +51,7 @@ "azure-oss/storage-blob-flysystem": "^1.2", "brianium/paratest": "^7.14", "carthage-software/mago": "1.0.0-beta.28", - "depotwarehouse/oauth2-twitch": "^1.3", + "vertisan/oauth2-twitch-helix": "^2.0", "guzzlehttp/psr7": "^2.6.1", "league/flysystem-aws-s3-v3": "^3.25.1", "league/flysystem-ftp": "^3.25.1", diff --git a/packages/auth/composer.json b/packages/auth/composer.json index 687412af4..4a83377c2 100644 --- a/packages/auth/composer.json +++ b/packages/auth/composer.json @@ -22,7 +22,7 @@ "adam-paterson/oauth2-slack": "^1.1", "wohali/oauth2-discord-new": "^1.2", "smolblog/oauth2-twitter": "^1.0", - "depotwarehouse/oauth2-twitch": "^1.3" + "vertisan/oauth2-twitch-helix": "^2.0" }, "autoload": { "psr-4": { From 318f842c6a0b27df3b6af84c52599ac3514285ed Mon Sep 17 00:00:00 2001 From: iamdadmin Date: Mon, 12 Jan 2026 14:35:51 +0000 Subject: [PATCH 2/6] docs(auth): adding Twitch oauth provider --- docs/2-features/17-oauth.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/2-features/17-oauth.md b/docs/2-features/17-oauth.md index 75ac6dc59..902869ce9 100644 --- a/docs/2-features/17-oauth.md +++ b/docs/2-features/17-oauth.md @@ -220,6 +220,7 @@ Tempest provides a different configuration object for each OAuth provider. Below - **Microsoft** authentication using {b`Tempest\Auth\OAuth\Config\MicrosoftOAuthConfig`}, - **Slack** authentication using {b`Tempest\Auth\OAuth\Config\SlackOAuthConfig`}, - **Apple** authentication using {b`Tempest\Auth\OAuth\Config\AppleOAuthConfig`}, +- **Twitch** authentication using {b`Tempest\Auth\OAuth\Config\TwitchOAuthConfig`}, - Any other OAuth platform using {b`Tempest\Auth\OAuth\Config\GenericOAuthConfig`}. ## Testing From 13e5e7e0562906cf70efeefe953184aced2a5824 Mon Sep 17 00:00:00 2001 From: iamdadmin Date: Mon, 19 Jan 2026 15:29:38 +0000 Subject: [PATCH 3/6] feat(auth): TwitchHelix implemented, and changed from Twitch to TwitchHelix naming --- docs/2-features/17-oauth.md | 2 +- .../oauth/twitchhelix.config.stub.php | 15 ++++ .../OAuth/Config/TwitchHelixOAuthConfig.php | 73 +++++++++++++++++++ .../auth/src/OAuth/SupportedOAuthProvider.php | 3 + 4 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 packages/auth/src/Installer/oauth/twitchhelix.config.stub.php create mode 100644 packages/auth/src/OAuth/Config/TwitchHelixOAuthConfig.php diff --git a/docs/2-features/17-oauth.md b/docs/2-features/17-oauth.md index 902869ce9..f2ca38203 100644 --- a/docs/2-features/17-oauth.md +++ b/docs/2-features/17-oauth.md @@ -220,7 +220,7 @@ Tempest provides a different configuration object for each OAuth provider. Below - **Microsoft** authentication using {b`Tempest\Auth\OAuth\Config\MicrosoftOAuthConfig`}, - **Slack** authentication using {b`Tempest\Auth\OAuth\Config\SlackOAuthConfig`}, - **Apple** authentication using {b`Tempest\Auth\OAuth\Config\AppleOAuthConfig`}, -- **Twitch** authentication using {b`Tempest\Auth\OAuth\Config\TwitchOAuthConfig`}, +- **Twitch** authentication using {b`Tempest\Auth\OAuth\Config\TwitchHelixOAuthConfig`}, - Any other OAuth platform using {b`Tempest\Auth\OAuth\Config\GenericOAuthConfig`}. ## Testing diff --git a/packages/auth/src/Installer/oauth/twitchhelix.config.stub.php b/packages/auth/src/Installer/oauth/twitchhelix.config.stub.php new file mode 100644 index 000000000..1e97cc72a --- /dev/null +++ b/packages/auth/src/Installer/oauth/twitchhelix.config.stub.php @@ -0,0 +1,15 @@ + $this->clientId, + 'clientSecret' => $this->clientSecret, + 'redirectUri' => $this->redirectTo, + ]); + } + + /** + * @param TwitchHelixResourceOwner $resourceOwner + */ + public function mapUser(ObjectFactory $factory, ResourceOwnerInterface $resourceOwner): OAuthUser + { + return $factory->withData([ + 'id' => (string) $resourceOwner->getId(), + 'email' => $resourceOwner->getEmail(), + 'name' => $resourceOwner->getDisplayName(), + 'nickname' => $resourceOwner->getDisplayName(), + 'avatar' => $resourceOwner->getProfileImageUrl(), + 'provider' => $this->provider, + 'raw' => $resourceOwner->toArray(), + ])->to(OAuthUser::class); + } +} diff --git a/packages/auth/src/OAuth/SupportedOAuthProvider.php b/packages/auth/src/OAuth/SupportedOAuthProvider.php index 1346a942d..94d260a95 100644 --- a/packages/auth/src/OAuth/SupportedOAuthProvider.php +++ b/packages/auth/src/OAuth/SupportedOAuthProvider.php @@ -11,6 +11,7 @@ use League\OAuth2\Client\Provider\Instagram; use League\OAuth2\Client\Provider\LinkedIn; use Stevenmaguire\OAuth2\Client\Provider\Microsoft; +use Vertisan\OAuth2\Client\Provider\TwitchHelix; use Wohali\OAuth2\Client\Provider\Discord; enum SupportedOAuthProvider: string @@ -25,6 +26,7 @@ enum SupportedOAuthProvider: string case LINKEDIN = LinkedIn::class; case MICROSOFT = Microsoft::class; case SLACK = Slack::class; + case TWITCHHELIX = TwitchHelix::class; public function composerPackage(): ?string { @@ -39,6 +41,7 @@ public function composerPackage(): ?string self::LINKEDIN => 'league/oauth2-linkedin', self::MICROSOFT => 'stevenmaguire/oauth2-microsoft', self::SLACK => 'adam-paterson/oauth2-slack', + self::TWITCHHELIX => 'vertisan/oauth2-twitch-helix', }; } } From c8dc4ccf39370471cc20c4542cec0fbd99120278 Mon Sep 17 00:00:00 2001 From: iamdadmin Date: Mon, 19 Jan 2026 15:37:55 +0000 Subject: [PATCH 4/6] feat(auth): Updated test --- composer.json | 2 +- tests/Integration/Auth/Installer/OAuthInstallerTest.php | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 4d1bba882..5146e035b 100644 --- a/composer.json +++ b/composer.json @@ -51,7 +51,6 @@ "azure-oss/storage-blob-flysystem": "^1.2", "brianium/paratest": "^7.14", "carthage-software/mago": "1.0.0-beta.28", - "vertisan/oauth2-twitch-helix": "^2.0", "guzzlehttp/psr7": "^2.6.1", "league/flysystem-aws-s3-v3": "^3.25.1", "league/flysystem-ftp": "^3.25.1", @@ -88,6 +87,7 @@ "tempest/blade": "dev-main", "thenetworg/oauth2-azure": "^2.2", "twig/twig": "^3.16", + "vertisan/oauth2-twitch-helix": "^2.0", "wohali/oauth2-discord-new": "^1.2" }, "replace": { diff --git a/tests/Integration/Auth/Installer/OAuthInstallerTest.php b/tests/Integration/Auth/Installer/OAuthInstallerTest.php index 22e754b60..7fd0a9b6a 100644 --- a/tests/Integration/Auth/Installer/OAuthInstallerTest.php +++ b/tests/Integration/Auth/Installer/OAuthInstallerTest.php @@ -121,6 +121,11 @@ public static function oauthProvider(): array 'expectedConfigPath' => 'App/Authentication/OAuth/slack.config.php', 'expectedControllerPath' => 'App/Authentication/OAuth/SlackController.php', ], + 'twitchhelix' => [ + 'provider' => SupportedOAuthProvider::TWITCHHELIX, + 'expectedConfigPath' => 'App/Authentication/OAuth/twitchhelix.config.php', + 'expectedControllerPath' => 'App/Authentication/OAuth/TwitchHelixController.php', + ], ]; } } From f98bb0cad875c6a29959c61f61a0ce7a8dbd0bf4 Mon Sep 17 00:00:00 2001 From: iamdadmin Date: Mon, 19 Jan 2026 21:49:13 +0000 Subject: [PATCH 5/6] feat(auth): Closes #1888 various improvements to auth/oauth installers --- .../src/Installer/AuthenticationInstaller.php | 19 +++++- .../auth/src/Installer/OAuthInstaller.php | 2 - .../basic-user/LoginController.stub.php | 59 +++++++++++++++++++ .../basic-user/MustBeAuthenticated.stub.php | 43 ++++++++++++++ .../oauth/CreateUsersTableMigration.stub.php | 28 +++++++++ .../Installer/oauth/OAuthControllerStub.php | 18 ++++-- .../src/Installer/oauth/UserModel.stub.php | 27 +++++++++ .../Authentication/CreateUsersTable.php | 26 ++++++++ 8 files changed, 213 insertions(+), 9 deletions(-) create mode 100644 packages/auth/src/Installer/basic-user/LoginController.stub.php create mode 100644 packages/auth/src/Installer/basic-user/MustBeAuthenticated.stub.php create mode 100644 packages/auth/src/Installer/oauth/CreateUsersTableMigration.stub.php create mode 100644 packages/auth/src/Installer/oauth/UserModel.stub.php create mode 100644 src/Tempest/Framework/Authentication/CreateUsersTable.php diff --git a/packages/auth/src/Installer/AuthenticationInstaller.php b/packages/auth/src/Installer/AuthenticationInstaller.php index d80395421..efb237f1c 100644 --- a/packages/auth/src/Installer/AuthenticationInstaller.php +++ b/packages/auth/src/Installer/AuthenticationInstaller.php @@ -20,6 +20,8 @@ final class AuthenticationInstaller implements Installer { use PublishesFiles; + public bool $installOAuth = false; + private(set) string $name = 'auth'; public function __construct( @@ -31,17 +33,28 @@ public function __construct( public function install(): void { - $migration = $this->publish(__DIR__ . '/basic-user/CreateUsersTableMigration.stub.php', src_path('Authentication/CreateUsersTable.php')); - $this->publish(__DIR__ . '/basic-user/UserModel.stub.php', src_path('Authentication/User.php')); + // First question, ask whether to also install OAuth, as it changes the stubs to publish + $this->installOAuth = $this->shouldInstallOAuth(); + + // Get the appropriate stubs + $stubPath = $this->installOAuth ? 'oauth' : 'basic-user'; + + // Publish the stubs + $migration = $this->publish(__DIR__ . "/{$stubPath}/CreateUsersTableMigration.stub.php", src_path('Authentication/CreateUsersTable.php')); + $this->publish(__DIR__ . "/{$stubPath}/UserModel.stub.php", src_path('Authentication/User.php')); + $this->publish(__DIR__ . '/basic-user/MustBeAuthenticated.stub.php', src_path('Authentication/MustBeAuthenticated.php')); + $this->publish(__DIR__ . '/basic-user/LoginController.stub.php', src_path('Authentication/LoginController.php')); $this->publishImports(); + // Offer to migrate if ($migration && $this->shouldMigrate()) { $this->migrationManager->executeUp( migration: $this->container->get(to_fqcn($migration, root: root_path())), ); } - if ($this->shouldInstallOAuth()) { + // Run the OAuth installer now + if ($this->installOAuth) { $this->container->get(OAuthInstaller::class)->install(); } } diff --git a/packages/auth/src/Installer/OAuthInstaller.php b/packages/auth/src/Installer/OAuthInstaller.php index 0ee15ec40..1ff9ca0d4 100644 --- a/packages/auth/src/Installer/OAuthInstaller.php +++ b/packages/auth/src/Installer/OAuthInstaller.php @@ -115,14 +115,12 @@ private function publishController(SupportedOAuthProvider $provider): void 'redirect-route', 'callback-route', "'user-model-fqcn'", - 'provider_db_column', ], replace: [ "\\{$providerFqcn}::{$provider->name}", "/auth/{$name}", "/auth/{$name}/callback", "\\{$userModelFqcn}::class", - "{$name}_id", ], ), ); diff --git a/packages/auth/src/Installer/basic-user/LoginController.stub.php b/packages/auth/src/Installer/basic-user/LoginController.stub.php new file mode 100644 index 000000000..805fa3273 --- /dev/null +++ b/packages/auth/src/Installer/basic-user/LoginController.stub.php @@ -0,0 +1,59 @@ +authenticator->authenticate($user); + + // Get the intended URL and redirect there, or default to home + // getIntended() automatically consumes/removes the stored URL + $intendedUrl = $this->previousUrl->getIntended('/dashboard'); + + return new Redirect($intendedUrl) + ->flash('success', 'Logged in successfully'); + } + + #[Post('/auth/logout')] + public function logout(): Redirect + { + $this->authenticator->deauthenticate(); + + return new Redirect('/') + ->flash('success', 'You have been logged out'); + } +} diff --git a/packages/auth/src/Installer/basic-user/MustBeAuthenticated.stub.php b/packages/auth/src/Installer/basic-user/MustBeAuthenticated.stub.php new file mode 100644 index 000000000..f4f70c50c --- /dev/null +++ b/packages/auth/src/Installer/basic-user/MustBeAuthenticated.stub.php @@ -0,0 +1,43 @@ +authenticator->current(); + + if ($user === null) { + // Store the intended URL + $this->previousUrl->setIntended($request->path); + + // Redirect to login if not authenticated + return new Redirect('/auth/login') + ->flash('error', 'You must be logged in to access this page'); + } + + // User is authenticated, continue with request + return $next($request); + } +} diff --git a/packages/auth/src/Installer/oauth/CreateUsersTableMigration.stub.php b/packages/auth/src/Installer/oauth/CreateUsersTableMigration.stub.php new file mode 100644 index 000000000..c5f5537d8 --- /dev/null +++ b/packages/auth/src/Installer/oauth/CreateUsersTableMigration.stub.php @@ -0,0 +1,28 @@ +primary() + ->string('email') + ->string('password', nullable: true) + ->string('name', nullable: true) + ->string('nickname', nullable: true) + ->string('avatar', nullable: true) + ->string('oauth_id', nullable: true) + ->string('oauth_raw', nullable: true) + ->string('oauth_provider', nullable: true); + } +} diff --git a/packages/auth/src/Installer/oauth/OAuthControllerStub.php b/packages/auth/src/Installer/oauth/OAuthControllerStub.php index 293ab874f..f9d9c7693 100644 --- a/packages/auth/src/Installer/oauth/OAuthControllerStub.php +++ b/packages/auth/src/Installer/oauth/OAuthControllerStub.php @@ -11,6 +11,7 @@ use Tempest\Discovery\SkipDiscovery; use Tempest\Http\Request; use Tempest\Http\Responses\Redirect; +use Tempest\Http\Session\PreviousUrl; use Tempest\Router\Get; use function Tempest\Database\query; @@ -21,6 +22,7 @@ public function __construct( #[Tag('tag_name')] private OAuthClient $oauth, + private PreviousUrl $previousUrl, ) {} #[Get('redirect-route')] @@ -32,19 +34,27 @@ public function redirect(): Redirect #[Get('callback-route')] public function callback(Request $request): Redirect { - // TODO: implement, the code below is an example + // TODO: implement, the code below is an example, customise to suit your application $this->oauth->authenticate( request: $request, map: fn (OAuthUser $user): Authenticatable => query('user-model-fqcn')->updateOrCreate([ - 'provider_db_column' => $user->id, + 'oauth_id' => $user->id, ], [ - 'provider_db_column' => $user->id, + 'oauth_id' => $user->id, 'username' => $user->nickname, 'email' => $user->email, + 'name' => $user->name, + 'nickname' => $user->nickname, + 'avatar' => $user->avatar, + 'oauth_raw' => $user->raw, + 'oauth_provider' => 'tag_name', ]), ); - return new Redirect('/'); + $intendedUrl = $this->previousUrl->getIntended('/dashboard'); + + return new Redirect($intendedUrl) + ->flash('success', 'Logged in successfully'); } } diff --git a/packages/auth/src/Installer/oauth/UserModel.stub.php b/packages/auth/src/Installer/oauth/UserModel.stub.php new file mode 100644 index 000000000..b8a28fceb --- /dev/null +++ b/packages/auth/src/Installer/oauth/UserModel.stub.php @@ -0,0 +1,27 @@ +primary() + ->string('email') + ->string('password', nullable: true) + ->string('name', nullable: true) + ->string('nickname', nullable: true) + ->string('avatar', nullable: true) + ->string('oauth_id', nullable: true) + ->string('oauth_raw', nullable: true) + ->string('oauth_provider', nullable: true); + } +} From 24e2a55537c84fd77f5a26514a2851c6b405ee8c Mon Sep 17 00:00:00 2001 From: iamdadmin Date: Tue, 20 Jan 2026 05:33:13 +0000 Subject: [PATCH 6/6] feat(auth): Update LoginController stub with example code from docs/04-authentication.md --- .../src/Installer/basic-user/LoginController.stub.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/auth/src/Installer/basic-user/LoginController.stub.php b/packages/auth/src/Installer/basic-user/LoginController.stub.php index 805fa3273..3eb2325af 100644 --- a/packages/auth/src/Installer/basic-user/LoginController.stub.php +++ b/packages/auth/src/Installer/basic-user/LoginController.stub.php @@ -35,8 +35,15 @@ public function showLoginForm(): View public function login(LoginRequest $request): Redirect { // TODO: implement, the code below is an example, customise to suit your application - - // Database query here to check for your user // + + $user = query(User::class) + ->select() + ->where('email', $request->email) + ->first(); + + if (! $user || ! $this->passwordHasher->verify($request->password, $user->password)) { + return new Redirect('/login')->flash('error', 'Invalid credentials'); + } $this->authenticator->authenticate($user);