Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a894386
avoiding finding by email as its not secure enough
jasonvarga Jun 26, 2026
f8d7d2c
avoid creating user if already exists
jasonvarga Jun 26, 2026
7eeff59
hitting routes while being logged in links the account
jasonvarga Jun 26, 2026
a44d4cf
tests
jasonvarga Jun 26, 2026
6c6ad78
unlinking
jasonvarga Jun 26, 2026
382d594
cp page
jasonvarga Jun 26, 2026
7a6a2c9
avoid rendering stateless providers as they cant be used for linking.…
jasonvarga Jun 26, 2026
094e458
Merge branch '6.x' into improve-oauth
jasonvarga Jun 29, 2026
8330558
bust opcache
jasonvarga Jun 29, 2026
dd85be8
oauth loop tag and unlink_form tag
jasonvarga Jun 29, 2026
cb35267
more tests
jasonvarga Jun 29, 2026
3e1588c
2fa
jasonvarga Jun 29, 2026
5590e2d
test for the cp page
jasonvarga Jun 29, 2026
53b924c
clean up
jasonvarga Jun 29, 2026
5fd443c
consistent naming
jasonvarga Jun 29, 2026
88f4219
handle already-published oauth config files
jasonvarga Jun 29, 2026
a3bba36
fix sizing / stroke / etc to match sign-out.svg
jasonvarga Jun 29, 2026
3b733fa
add trans import
jasonvarga Jun 30, 2026
12b7db4
remember oauth provider through the two-factor challenge
jasonvarga Jun 30, 2026
93a3f9e
use separate disconnect routes for the front-end and control panel
jasonvarga Jun 30, 2026
90dcfe1
require socialite in dev and add some missing tests
jasonvarga Jun 30, 2026
0065c65
hook up elevated sessions
jasonvarga Jun 30, 2026
b00ec16
ensure elevated when clicking disconnect
jasonvarga Jun 30, 2026
68c7a6b
elevate with js for connect button
jasonvarga Jun 30, 2026
3ab524f
make sure callback route requires elevation, and store the redirect i…
jasonvarga Jul 1, 2026
6176434
we dont need the email in the exception. unnecessary pii
jasonvarga Jul 1, 2026
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
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"fakerphp/faker": "~1.10",
"google/cloud-translate": "^1.6",
"laravel/pint": "1.16.0",
"laravel/socialite": "^5.28",
"mockery/mockery": "^1.6.10",
"orchestra/testbench": "^10.8 || ^11.0",
"phpunit/phpunit": "^11.5.3",
Expand Down
1 change: 1 addition & 0 deletions config/oauth.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
'routes' => [
'login' => 'oauth/{provider}',
'callback' => 'oauth/{provider}/callback',
'disconnect' => 'oauth/{provider}/disconnect',
],

/*
Expand Down
6 changes: 6 additions & 0 deletions lang/en/messages.php
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,12 @@
'navigation_documentation_instructions' => 'Learn about building, configuring, and rendering navigations',
'navigation_link_to_entry_instructions' => 'Add a link to an entry. Enable linking to additional collections in the config area.',
'navigation_link_to_url_instructions' => 'Add a link to any internal or external URL. Enable linking to entries in the config area.',
'oauth_email_exists' => 'An account with this email address already exists. Sign in and connect this provider from your account settings.',
'oauth_already_connected' => 'Your :provider account is already connected.',
'oauth_belongs_to_another_user' => 'This :provider account is already connected to a different user.',
'oauth_connect_unsupported' => 'This provider does not support connecting accounts.',
'oauth_connected' => 'Connected your :provider account.',
'oauth_disconnected' => 'Disconnected your :provider account.',
'outpost_error_422' => 'Error communicating with statamic.com.',
'outpost_error_429' => 'Too many requests to statamic.com.',
'outpost_issue_try_later' => 'There was an issue communicating with statamic.com. Please try again later.',
Expand Down
2 changes: 2 additions & 0 deletions resources/js/components/users/PublishForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<DropdownMenu>
<DropdownItem :text="__('Edit Blueprint')" icon="blueprint-edit" v-if="canEditBlueprint" :href="actions.editBlueprint" />
<DropdownItem :text="__('Passkeys')" icon="key" :href="cp_url('passkeys')" />
<DropdownItem v-if="oauthEnabled" :text="__('Sign-in Providers')" icon="sign-in" :href="cp_url('oauth')" />
<DropdownSeparator v-if="canEditBlueprint && itemActions.length" />
<DropdownItem
v-for="action in itemActions"
Expand Down Expand Up @@ -120,6 +121,7 @@ export default {
method: String,
canEditPassword: Boolean,
canEditBlueprint: Boolean,
oauthEnabled: Boolean,
requiresCurrentPassword: Boolean,
twoFactor: Object,
},
Expand Down
2 changes: 2 additions & 0 deletions resources/js/pages/users/Edit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ defineProps([
'meta',
'canEditPassword',
'canEditBlueprint',
'oauthEnabled',
'requiresCurrentPassword',
'itemActions',
'itemActionUrl',
Expand All @@ -33,6 +34,7 @@ defineProps([
:initial-meta="meta"
:can-edit-password="canEditPassword"
:can-edit-blueprint="canEditBlueprint"
:oauth-enabled="oauthEnabled"
:requires-current-password="requiresCurrentPassword"
:initial-item-actions="itemActions"
:item-action-url="itemActionUrl"
Expand Down
73 changes: 73 additions & 0 deletions resources/js/pages/users/OAuth.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<script setup>
import { router } from '@inertiajs/vue3';
import axios from 'axios';
import Head from '@/pages/layout/Head.vue';
import { Header, Button, Listing } from '@ui';
import { toast } from '@api';
import { requireElevatedSession } from '@/components/elevated-sessions';

defineProps(['providers']);

const columns = [
{ label: __('Provider'), field: 'label' },
{ label: '', field: 'actions' },
];

function connect(provider) {
requireElevatedSession()
.then(() => (window.location = provider.connectUrl))
.catch(() => toast.error(__('statamic::messages.elevated_session_required')));
}

function disconnect(provider) {
requireElevatedSession()
.then(() => performDisconnect(provider))
.catch(() => toast.error(__('statamic::messages.elevated_session_required')));
}

function performDisconnect(provider) {
axios.delete(provider.disconnectUrl).then(() => {
toast.success(__('statamic::messages.oauth_disconnected', { provider: provider.label }));
router.reload();
});
}
</script>

<template>
<Head :title="__('Sign-in Providers')" />

<div class="max-w-5xl 3xl:max-w-6xl mx-auto" data-max-width-wrapper>
<Header :title="__('Sign-in Providers')" icon="sign-in" />

<Listing
:items="providers"
:columns
:allow-search="false"
:allow-customizing-columns="false"
>
<template #cell-label="{ row }">
<div class="flex items-center gap-2">
<span v-if="row.icon" class="flex size-4 items-center [&_svg]:size-4" v-html="row.icon" />
<span>{{ row.label }}</span>
</div>
</template>

<template #cell-actions="{ row }">
<div class="text-right">
<Button
v-if="row.connected"
size="xs"
:text="__('Disconnect')"
@click="disconnect(row)"
/>
<Button
v-else
size="xs"
:text="__('Connect')"
@click="connect(row)"
/>
</div>
</template>
</Listing>
</div>
</template>
2 changes: 1 addition & 1 deletion resources/svg/icons/sign-in.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions routes/cp.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php

use Illuminate\Support\Facades\Route;
use Statamic\Facades\OAuth;
use Statamic\Facades\TwoFactor;
use Statamic\Facades\Utility;
use Statamic\Http\Controllers\CP\Addons\AddonsController;
Expand All @@ -23,6 +24,7 @@
use Statamic\Http\Controllers\CP\Auth\ForgotPasswordController;
use Statamic\Http\Controllers\CP\Auth\ImpersonationController;
use Statamic\Http\Controllers\CP\Auth\LoginController;
use Statamic\Http\Controllers\CP\Auth\OAuthController;
use Statamic\Http\Controllers\CP\Auth\PasskeyController;
use Statamic\Http\Controllers\CP\Auth\PasskeyLoginController;
use Statamic\Http\Controllers\CP\Auth\ResetPasswordController;
Expand Down Expand Up @@ -113,6 +115,7 @@
use Statamic\Http\Controllers\CP\Users\UsersController;
use Statamic\Http\Controllers\CP\Users\UserWizardController;
use Statamic\Http\Controllers\CP\Utilities\UtilitiesController;
use Statamic\Http\Controllers\OAuthController as FrontendOAuthController;
use Statamic\Http\Controllers\User\TwoFactorRecoveryCodesController;
use Statamic\Http\Middleware\CP\RedirectIfTwoFactorSetupIncomplete;
use Statamic\Http\Middleware\CP\RequireElevatedSession;
Expand Down Expand Up @@ -436,6 +439,11 @@
Route::delete('{id}', [PasskeyController::class, 'destroy'])->name('passkeys.destroy');
});

if (OAuth::enabled()) {
Route::get('oauth', [OAuthController::class, 'index'])->name('oauth');
Route::delete('oauth/{provider}/disconnect', [FrontendOAuthController::class, 'disconnect'])->middleware(RequireElevatedSession::class)->name('oauth.disconnect');
}

Route::get('themes', [ThemeController::class, 'index']);
Route::get('themes/refresh', [ThemeController::class, 'refresh']);
Route::post('themes/share', ShareThemeController::class);
Expand Down
3 changes: 3 additions & 0 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@
Route::match(['get', 'post'], config('statamic.oauth.routes.callback'), [OAuthController::class, 'handleProviderCallback'])
->withoutMiddleware(['App\Http\Middleware\VerifyCsrfToken', 'Illuminate\Foundation\Http\Middleware\VerifyCsrfToken'])
->name('oauth.callback');
Route::delete(config('statamic.oauth.routes.disconnect', 'oauth/{provider}/disconnect'), [OAuthController::class, 'disconnect'])
->middleware([AuthGuard::class, 'auth', RequireElevatedSession::class])
->name('oauth.disconnect');
}
});

Expand Down
13 changes: 13 additions & 0 deletions src/Exceptions/OAuthEmailExistsException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Statamic\Exceptions;

use Exception;

class OAuthEmailExistsException extends Exception
{
public function __construct()
{
parent::__construct('A user already exists with the OAuth email.');
}
}
34 changes: 34 additions & 0 deletions src/Http/Controllers/CP/Auth/OAuthController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace Statamic\Http\Controllers\CP\Auth;

use Inertia\Inertia;
use Statamic\Facades\OAuth;
use Statamic\Facades\User;
use Statamic\Http\Controllers\CP\CpController;
use Statamic\OAuth\Provider;
use Statamic\Statamic;

class OAuthController extends CpController
{
public function index()
{
abort_unless(OAuth::enabled(), 404);

$user = User::current();
$redirect = parse_url(cp_route('oauth'))['path'];

return Inertia::render('users/OAuth', [
'providers' => OAuth::providers()
->reject(fn (Provider $provider) => $provider->isStateless())
->map(fn (Provider $provider) => [
'name' => $provider->name(),
'label' => $provider->label(),
'icon' => Statamic::svg('oauth/'.$provider->name()),
'connected' => $provider->isConnectedTo($user),
'connectUrl' => $provider->loginUrl().'?redirect='.$redirect,
'disconnectUrl' => cp_route('oauth.disconnect', $provider->name()),
])->values(),
]);
}
}
2 changes: 2 additions & 0 deletions src/Http/Controllers/CP/Users/UsersController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Statamic\Exceptions\NotFoundHttpException;
use Statamic\Facades\Action;
use Statamic\Facades\CP\Toast;
use Statamic\Facades\OAuth;
use Statamic\Facades\Scope;
use Statamic\Facades\Search;
use Statamic\Facades\TwoFactor;
Expand Down Expand Up @@ -279,6 +280,7 @@ public function edit(Request $request, $user)
'editBlueprint' => cp_route('blueprints.users.edit'),
],
'canEditBlueprint' => User::current()->can('configure fields'),
'oauthEnabled' => OAuth::enabled(),
'canEditPassword' => User::fromUser($request->user())->can('editPassword', $user),
'requiresCurrentPassword' => $isCurrentUser = $request->user()->id === $user->id(),
'itemActions' => Action::for($user, ['view' => 'form']),
Expand Down
Loading
Loading