Skip to content

Commit 466d7e3

Browse files
jasonvargaclaude
andauthored
[6.x] OAuth Improvements (#14899)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent a28f5b3 commit 466d7e3

24 files changed

Lines changed: 1475 additions & 43 deletions

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"fakerphp/faker": "~1.10",
4747
"google/cloud-translate": "^1.6",
4848
"laravel/pint": "1.16.0",
49+
"laravel/socialite": "^5.28",
4950
"mockery/mockery": "^1.6.10",
5051
"orchestra/testbench": "^10.8 || ^11.0",
5152
"phpunit/phpunit": "^11.5.3",

config/oauth.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
'routes' => [
1414
'login' => 'oauth/{provider}',
1515
'callback' => 'oauth/{provider}/callback',
16+
'disconnect' => 'oauth/{provider}/disconnect',
1617
],
1718

1819
/*

lang/en/messages.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,12 @@
188188
'navigation_documentation_instructions' => 'Learn about building, configuring, and rendering navigations',
189189
'navigation_link_to_entry_instructions' => 'Add a link to an entry. Enable linking to additional collections in the config area.',
190190
'navigation_link_to_url_instructions' => 'Add a link to any internal or external URL. Enable linking to entries in the config area.',
191+
'oauth_email_exists' => 'An account with this email address already exists. Sign in and connect this provider from your account settings.',
192+
'oauth_already_connected' => 'Your :provider account is already connected.',
193+
'oauth_belongs_to_another_user' => 'This :provider account is already connected to a different user.',
194+
'oauth_connect_unsupported' => 'This provider does not support connecting accounts.',
195+
'oauth_connected' => 'Connected your :provider account.',
196+
'oauth_disconnected' => 'Disconnected your :provider account.',
191197
'outpost_error_422' => 'Error communicating with statamic.com.',
192198
'outpost_error_429' => 'Too many requests to statamic.com.',
193199
'outpost_issue_try_later' => 'There was an issue communicating with statamic.com. Please try again later.',

resources/js/components/users/PublishForm.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
<DropdownMenu>
2020
<DropdownItem :text="__('Edit Blueprint')" icon="blueprint-edit" v-if="canEditBlueprint" :href="actions.editBlueprint" />
2121
<DropdownItem :text="__('Passkeys')" icon="key" :href="cp_url('passkeys')" />
22+
<DropdownItem v-if="oauthEnabled" :text="__('Sign-in Providers')" icon="sign-in" :href="cp_url('oauth')" />
2223
<DropdownSeparator v-if="canEditBlueprint && itemActions.length" />
2324
<DropdownItem
2425
v-for="action in itemActions"
@@ -120,6 +121,7 @@ export default {
120121
method: String,
121122
canEditPassword: Boolean,
122123
canEditBlueprint: Boolean,
124+
oauthEnabled: Boolean,
123125
requiresCurrentPassword: Boolean,
124126
twoFactor: Object,
125127
},

resources/js/pages/users/Edit.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ defineProps([
1111
'meta',
1212
'canEditPassword',
1313
'canEditBlueprint',
14+
'oauthEnabled',
1415
'requiresCurrentPassword',
1516
'itemActions',
1617
'itemActionUrl',
@@ -33,6 +34,7 @@ defineProps([
3334
:initial-meta="meta"
3435
:can-edit-password="canEditPassword"
3536
:can-edit-blueprint="canEditBlueprint"
37+
:oauth-enabled="oauthEnabled"
3638
:requires-current-password="requiresCurrentPassword"
3739
:initial-item-actions="itemActions"
3840
:item-action-url="itemActionUrl"

resources/js/pages/users/OAuth.vue

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<script setup>
2+
import { router } from '@inertiajs/vue3';
3+
import axios from 'axios';
4+
import Head from '@/pages/layout/Head.vue';
5+
import { Header, Button, Listing } from '@ui';
6+
import { toast } from '@api';
7+
import { requireElevatedSession } from '@/components/elevated-sessions';
8+
9+
defineProps(['providers']);
10+
11+
const columns = [
12+
{ label: __('Provider'), field: 'label' },
13+
{ label: '', field: 'actions' },
14+
];
15+
16+
function connect(provider) {
17+
requireElevatedSession()
18+
.then(() => (window.location = provider.connectUrl))
19+
.catch(() => toast.error(__('statamic::messages.elevated_session_required')));
20+
}
21+
22+
function disconnect(provider) {
23+
requireElevatedSession()
24+
.then(() => performDisconnect(provider))
25+
.catch(() => toast.error(__('statamic::messages.elevated_session_required')));
26+
}
27+
28+
function performDisconnect(provider) {
29+
axios.delete(provider.disconnectUrl).then(() => {
30+
toast.success(__('statamic::messages.oauth_disconnected', { provider: provider.label }));
31+
router.reload();
32+
});
33+
}
34+
</script>
35+
36+
<template>
37+
<Head :title="__('Sign-in Providers')" />
38+
39+
<div class="max-w-5xl 3xl:max-w-6xl mx-auto" data-max-width-wrapper>
40+
<Header :title="__('Sign-in Providers')" icon="sign-in" />
41+
42+
<Listing
43+
:items="providers"
44+
:columns
45+
:allow-search="false"
46+
:allow-customizing-columns="false"
47+
>
48+
<template #cell-label="{ row }">
49+
<div class="flex items-center gap-2">
50+
<span v-if="row.icon" class="flex size-4 items-center [&_svg]:size-4" v-html="row.icon" />
51+
<span>{{ row.label }}</span>
52+
</div>
53+
</template>
54+
55+
<template #cell-actions="{ row }">
56+
<div class="text-right">
57+
<Button
58+
v-if="row.connected"
59+
size="xs"
60+
:text="__('Disconnect')"
61+
@click="disconnect(row)"
62+
/>
63+
<Button
64+
v-else
65+
size="xs"
66+
:text="__('Connect')"
67+
@click="connect(row)"
68+
/>
69+
</div>
70+
</template>
71+
</Listing>
72+
</div>
73+
</template>

resources/svg/icons/sign-in.svg

Lines changed: 1 addition & 1 deletion
Loading

routes/cp.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php
22

33
use Illuminate\Support\Facades\Route;
4+
use Statamic\Facades\OAuth;
45
use Statamic\Facades\TwoFactor;
56
use Statamic\Facades\Utility;
67
use Statamic\Http\Controllers\CP\Addons\AddonsController;
@@ -23,6 +24,7 @@
2324
use Statamic\Http\Controllers\CP\Auth\ForgotPasswordController;
2425
use Statamic\Http\Controllers\CP\Auth\ImpersonationController;
2526
use Statamic\Http\Controllers\CP\Auth\LoginController;
27+
use Statamic\Http\Controllers\CP\Auth\OAuthController;
2628
use Statamic\Http\Controllers\CP\Auth\PasskeyController;
2729
use Statamic\Http\Controllers\CP\Auth\PasskeyLoginController;
2830
use Statamic\Http\Controllers\CP\Auth\ResetPasswordController;
@@ -113,6 +115,7 @@
113115
use Statamic\Http\Controllers\CP\Users\UsersController;
114116
use Statamic\Http\Controllers\CP\Users\UserWizardController;
115117
use Statamic\Http\Controllers\CP\Utilities\UtilitiesController;
118+
use Statamic\Http\Controllers\OAuthController as FrontendOAuthController;
116119
use Statamic\Http\Controllers\User\TwoFactorRecoveryCodesController;
117120
use Statamic\Http\Middleware\CP\RedirectIfTwoFactorSetupIncomplete;
118121
use Statamic\Http\Middleware\CP\RequireElevatedSession;
@@ -436,6 +439,11 @@
436439
Route::delete('{id}', [PasskeyController::class, 'destroy'])->name('passkeys.destroy');
437440
});
438441

442+
if (OAuth::enabled()) {
443+
Route::get('oauth', [OAuthController::class, 'index'])->name('oauth');
444+
Route::delete('oauth/{provider}/disconnect', [FrontendOAuthController::class, 'disconnect'])->middleware(RequireElevatedSession::class)->name('oauth.disconnect');
445+
}
446+
439447
Route::get('themes', [ThemeController::class, 'index']);
440448
Route::get('themes/refresh', [ThemeController::class, 'refresh']);
441449
Route::post('themes/share', ShareThemeController::class);

routes/web.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@
114114
Route::match(['get', 'post'], config('statamic.oauth.routes.callback'), [OAuthController::class, 'handleProviderCallback'])
115115
->withoutMiddleware(['App\Http\Middleware\VerifyCsrfToken', 'Illuminate\Foundation\Http\Middleware\VerifyCsrfToken'])
116116
->name('oauth.callback');
117+
Route::delete(config('statamic.oauth.routes.disconnect', 'oauth/{provider}/disconnect'), [OAuthController::class, 'disconnect'])
118+
->middleware([AuthGuard::class, 'auth', RequireElevatedSession::class])
119+
->name('oauth.disconnect');
117120
}
118121
});
119122

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Statamic\Exceptions;
4+
5+
use Exception;
6+
7+
class OAuthEmailExistsException extends Exception
8+
{
9+
public function __construct()
10+
{
11+
parent::__construct('A user already exists with the OAuth email.');
12+
}
13+
}

0 commit comments

Comments
 (0)