-
Notifications
You must be signed in to change notification settings - Fork 34
Expand file tree
/
Copy pathFacebookVerifier.php
More file actions
134 lines (116 loc) · 5.06 KB
/
Copy pathFacebookVerifier.php
File metadata and controls
134 lines (116 loc) · 5.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
<?php
namespace Fleetbase\Auth;
use GuzzleHttp\Client as GuzzleClient;
/**
* Server-side verifier for Facebook Login access tokens.
*
* The Facebook JavaScript / native SDKs hand a short-lived `accessToken`
* to the client after a successful login. The server MUST validate that
* token before trusting any identity claim — Facebook's official guidance
* is to call the Graph API `/debug_token` endpoint authenticated with
* an app access token (`{app_id}|{app_secret}`) and confirm:
*
* 1. The returned `data.app_id` matches our configured `services.facebook.app_id`
* 2. The token is `data.is_valid === true`
* 3. The `data.user_id` is non-empty
*
* Then a follow-up GET to `/me?fields=id,email,name` returns the profile
* fields needed for find-or-create user logic.
*
* The previous storefront implementation (`CustomerController::loginWithFacebook`)
* accepted `facebookUserId` from the request body without server-side
* verification — anyone could POST a forged identifier and impersonate
* another Facebook user. This class closes that gap for the Console OAuth
* flow and should be backported to the storefront controller separately.
*
* Config keys required:
* - services.facebook.app_id — public, also accepted as request param
* - services.facebook.app_secret — server-side only
*/
class FacebookVerifier
{
private const GRAPH_API_BASE = 'https://graph.facebook.com/v18.0';
/**
* Verify a Facebook access token and return the verified profile.
*
* @return array|null `['user_id', 'email', 'name']` when valid, `null` otherwise
*/
public static function verifyAccessToken(string $accessToken, ?string $clientAppId = null): ?array
{
$configuredAppId = config('services.facebook.app_id');
$appSecret = config('services.facebook.app_secret');
if (!$configuredAppId || !$appSecret) {
logger()->error('Facebook OAuth not configured — services.facebook.app_id / app_secret missing');
return null;
}
// Defence-in-depth: if the client supplied an app_id, refuse to verify
// a token claiming a different app. Stops a token issued for another
// Facebook app from logging into this one even if Graph API debug_token
// were ever to misreport.
if ($clientAppId !== null && $clientAppId !== $configuredAppId) {
logger()->warning('Facebook OAuth client app_id mismatch', [
'expected' => $configuredAppId,
'received' => $clientAppId,
]);
return null;
}
$http = self::httpClient();
$appToken = $configuredAppId . '|' . $appSecret;
try {
// Step 1: debug the user's access token using the app token.
$debugResp = $http->get(self::GRAPH_API_BASE . '/debug_token', [
'query' => [
'input_token' => $accessToken,
'access_token' => $appToken,
],
]);
$debugBody = json_decode((string) $debugResp->getBody(), true);
$data = data_get($debugBody, 'data');
if (!is_array($data)) {
logger()->warning('Facebook debug_token returned no data', ['body' => $debugBody]);
return null;
}
if (data_get($data, 'is_valid') !== true) {
logger()->info('Facebook access token reported invalid', ['data' => $data]);
return null;
}
if (data_get($data, 'app_id') !== $configuredAppId) {
logger()->warning('Facebook token issued for a different app', [
'expected' => $configuredAppId,
'actual' => data_get($data, 'app_id'),
]);
return null;
}
$userId = data_get($data, 'user_id');
if (!$userId) {
return null;
}
// Step 2: pull the profile (email, name) using the user's access token.
$meResp = $http->get(self::GRAPH_API_BASE . '/me', [
'query' => [
'fields' => 'id,email,name',
'access_token' => $accessToken,
],
]);
$me = json_decode((string) $meResp->getBody(), true);
return [
'user_id' => (string) $userId,
'email' => data_get($me, 'email'),
'name' => data_get($me, 'name'),
];
} catch (\Throwable $e) {
logger()->error('Facebook token verification failed: ' . $e->getMessage());
return null;
}
}
private static function httpClient(): GuzzleClient
{
return new GuzzleClient([
'timeout' => 8.0,
'connect_timeout' => 4.0,
// In local development we routinely run behind self-signed certs
// (mirrors the GoogleVerifier::verifyIdToken pattern).
'verify' => config('app.debug') !== true && app()->environment('production'),
]);
}
}