Skip to content

Commit 284cda2

Browse files
committed
feat: add DDLess Passport workaround to generate tokens without HTTP requests
1 parent bf28d5d commit 284cda2

1 file changed

Lines changed: 233 additions & 0 deletions

File tree

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
<?php
2+
/**
3+
* DDLess Passport Workaround
4+
*
5+
* When running under DDLess (PHP CLI), Laravel Passport's PersonalAccessTokenFactory
6+
* makes an HTTP request to /oauth/token using Guzzle. This fails because there's
7+
* no HTTP server running in the CLI process.
8+
*
9+
* This workaround replaces PersonalAccessTokenFactory with a version that
10+
* generates tokens directly without making HTTP requests.
11+
*/
12+
13+
namespace DDLess\Workarounds;
14+
15+
use Illuminate\Contracts\Foundation\Application;
16+
17+
/**
18+
* Check if we should apply Passport workarounds
19+
*/
20+
function shouldApplyPassportWorkaround(): bool
21+
{
22+
return php_sapi_name() === 'cli' || getenv('DDLESS_DEBUG_MODE') === 'true';
23+
}
24+
25+
/**
26+
* Register Passport workarounds with the Laravel application
27+
*/
28+
function registerPassportWorkaround(Application $app): void
29+
{
30+
if (!shouldApplyPassportWorkaround()) {
31+
return;
32+
}
33+
34+
// Check if Passport is installed
35+
if (!class_exists('Laravel\Passport\Passport')) {
36+
return;
37+
}
38+
39+
fwrite(STDERR, "[ddless] Passport detected - replacing PersonalAccessTokenFactory...\n");
40+
41+
try {
42+
// Replace the PersonalAccessTokenFactory with our direct implementation
43+
$app->singleton(\Laravel\Passport\PersonalAccessTokenFactory::class, function ($app) {
44+
fwrite(STDERR, "[ddless] Creating DirectPersonalAccessTokenFactory...\n");
45+
return new DirectPersonalAccessTokenFactory(
46+
$app->make(\Laravel\Passport\ClientRepository::class)
47+
);
48+
});
49+
50+
fwrite(STDERR, "[ddless] Passport PersonalAccessTokenFactory replaced successfully.\n");
51+
} catch (\Throwable $e) {
52+
fwrite(STDERR, "[ddless] Failed to replace PersonalAccessTokenFactory: " . $e->getMessage() . "\n");
53+
fwrite(STDERR, "[ddless] Stack: " . $e->getTraceAsString() . "\n");
54+
}
55+
}
56+
57+
/**
58+
* Direct token factory that doesn't make HTTP requests
59+
*
60+
* This replaces Laravel\Passport\PersonalAccessTokenFactory
61+
*/
62+
class DirectPersonalAccessTokenFactory
63+
{
64+
protected $clients;
65+
66+
public function __construct(\Laravel\Passport\ClientRepository $clients)
67+
{
68+
$this->clients = $clients;
69+
}
70+
71+
/**
72+
* Create a new personal access token.
73+
*
74+
* @param mixed $userId
75+
* @param string $name
76+
* @param array $scopes
77+
* @return \Laravel\Passport\PersonalAccessTokenResult
78+
*/
79+
public function make($userId, $name, array $scopes = [])
80+
{
81+
fwrite(STDERR, "[ddless] DirectPersonalAccessTokenFactory::make() for user {$userId}, name: {$name}\n");
82+
83+
$client = $this->clients->personalAccessClient();
84+
85+
if (!$client) {
86+
throw new \RuntimeException(
87+
'[ddless] Personal access client not found. Run: php artisan passport:client --personal'
88+
);
89+
}
90+
91+
// Generate token ID
92+
$tokenId = hash('sha256', bin2hex(random_bytes(40)));
93+
94+
// Calculate expiration
95+
$expiresAt = now()->addYear();
96+
if (class_exists('\Laravel\Passport\Passport')) {
97+
$expiration = \Laravel\Passport\Passport::personalAccessTokensExpireIn();
98+
if ($expiration) {
99+
$expiresAt = now()->add($expiration);
100+
}
101+
}
102+
103+
// Create the token record directly using Eloquent
104+
$tokenModel = config('passport.token_model', \Laravel\Passport\Token::class);
105+
$token = new $tokenModel();
106+
$token->id = $tokenId;
107+
$token->user_id = $userId;
108+
$token->client_id = $client->getKey();
109+
$token->name = $name;
110+
$token->scopes = $scopes;
111+
$token->revoked = false;
112+
$token->created_at = now();
113+
$token->updated_at = now();
114+
$token->expires_at = $expiresAt;
115+
$token->save();
116+
117+
// Generate the JWT access token
118+
$accessToken = $this->generateAccessToken($token, $client, $userId, $scopes, $expiresAt);
119+
120+
fwrite(STDERR, "[ddless] Token created successfully with ID: " . substr($tokenId, 0, 8) . "...\n");
121+
122+
return new \Laravel\Passport\PersonalAccessTokenResult(
123+
$accessToken,
124+
$token
125+
);
126+
}
127+
128+
/**
129+
* Generate a JWT access token string
130+
*/
131+
protected function generateAccessToken($token, $client, $userId, array $scopes, $expiresAt): string
132+
{
133+
// Try to use Lcobucci JWT library (Passport's default)
134+
if (class_exists('\Lcobucci\JWT\Configuration')) {
135+
return $this->generateJwtToken($token, $client, $userId, $scopes, $expiresAt);
136+
}
137+
138+
// Fallback: generate a simple bearer token
139+
fwrite(STDERR, "[ddless] JWT library not found, using simple token\n");
140+
return $this->generateSimpleToken($token);
141+
}
142+
143+
/**
144+
* Generate JWT token using Lcobucci library
145+
*/
146+
protected function generateJwtToken($token, $client, $userId, array $scopes, $expiresAt): string
147+
{
148+
try {
149+
$privateKeyPath = \Laravel\Passport\Passport::keyPath('oauth-private.key');
150+
151+
if (!file_exists($privateKeyPath)) {
152+
fwrite(STDERR, "[ddless] WARNING: Private key not found at {$privateKeyPath}\n");
153+
fwrite(STDERR, "[ddless] Run: php artisan passport:keys\n");
154+
return $this->generateSimpleToken($token);
155+
}
156+
157+
$privateKey = file_get_contents($privateKeyPath);
158+
159+
return $this->generateJwtV4($token, $client, $userId, $scopes, $expiresAt, $privateKey);
160+
} catch (\Throwable $e) {
161+
fwrite(STDERR, "[ddless] JWT generation failed: " . $e->getMessage() . "\n");
162+
return $this->generateSimpleToken($token);
163+
}
164+
}
165+
166+
/**
167+
* Generate JWT using Lcobucci 4.x/5.x
168+
*/
169+
protected function generateJwtV4($token, $client, $userId, array $scopes, $expiresAt, string $privateKey): string
170+
{
171+
$config = \Lcobucci\JWT\Configuration::forAsymmetricSigner(
172+
new \Lcobucci\JWT\Signer\Rsa\Sha256(),
173+
\Lcobucci\JWT\Signer\Key\InMemory::plainText($privateKey),
174+
\Lcobucci\JWT\Signer\Key\InMemory::plainText('') // Public key not needed for signing
175+
);
176+
177+
$now = new \DateTimeImmutable();
178+
$expiresAtImmutable = $expiresAt instanceof \DateTimeImmutable
179+
? $expiresAt
180+
: \DateTimeImmutable::createFromMutable(
181+
$expiresAt instanceof \DateTime ? $expiresAt : new \DateTime($expiresAt->format('Y-m-d H:i:s'))
182+
);
183+
184+
$jwtToken = $config->builder()
185+
->issuedBy(config('app.url', 'http://localhost'))
186+
->permittedFor((string) $client->getKey())
187+
->identifiedBy($token->id)
188+
->issuedAt($now)
189+
->canOnlyBeUsedAfter($now)
190+
->expiresAt($expiresAtImmutable)
191+
->relatedTo((string) $userId)
192+
->withClaim('scopes', $scopes)
193+
->getToken($config->signer(), $config->signingKey());
194+
195+
return $jwtToken->toString();
196+
}
197+
198+
/**
199+
* Generate a simple token (fallback when JWT is not available)
200+
*/
201+
protected function generateSimpleToken($token): string
202+
{
203+
// Create a simple encrypted token that can be used as bearer
204+
// This is not as secure as JWT but works for debugging purposes
205+
$payload = json_encode([
206+
'token_id' => $token->id,
207+
'client_id' => $token->client_id,
208+
'user_id' => $token->user_id,
209+
'scopes' => $token->scopes,
210+
'expires_at' => $token->expires_at->timestamp ?? time() + 31536000,
211+
'random' => bin2hex(random_bytes(16)),
212+
]);
213+
214+
// Use Laravel's encryption if available
215+
if (function_exists('encrypt')) {
216+
try {
217+
return encrypt($payload);
218+
} catch (\Throwable $e) {
219+
// Fall through to base64
220+
}
221+
}
222+
223+
return base64_encode($payload);
224+
}
225+
}
226+
227+
/**
228+
* Not used - kept for compatibility
229+
*/
230+
function registerPassportMiddleware(Application $app): void
231+
{
232+
// Not needed with factory replacement approach
233+
}

0 commit comments

Comments
 (0)