Skip to content

Commit 687d519

Browse files
committed
✨ Implements OAuth2 Access token and user data fetching
1 parent b1b0902 commit 687d519

File tree

7 files changed

+224
-30
lines changed

7 files changed

+224
-30
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Auth\OAuth\DataObjects;
6+
7+
use Tempest\Mapper\MapFrom;
8+
use function Tempest\map;
9+
10+
final readonly class AccessToken
11+
{
12+
public function __construct(
13+
#[MapFrom('access_token')]
14+
public string $accessToken,
15+
16+
#[MapFrom('token_type')]
17+
public string $tokenType,
18+
19+
public string $scope,
20+
) {}
21+
22+
public static function from(array $data): self
23+
{
24+
return map($data)->to(self::class);
25+
}
26+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Auth\OAuth\DataObjects;
6+
7+
use Tempest\Mapper\MapFrom;
8+
use function Tempest\map;
9+
10+
final readonly class OAuthUserData
11+
{
12+
public function __construct(
13+
public int|string $id,
14+
public string $nickname,
15+
public string $name,
16+
public string $email,
17+
public string $avatar,
18+
) {}
19+
20+
public static function from(array $data): self
21+
{
22+
return map($data)->to(self::class);
23+
}
24+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Auth\OAuth;
6+
7+
use Tempest\Auth\OAuth\DataObjects\OAuthUserData;
8+
9+
final class GithubOAuthProvider implements OAuth2ProviderContract
10+
{
11+
use IsOAuth2Provider;
12+
13+
public private(set) array $scopes = ['user:email'];
14+
15+
public private(set) string $authorizationUrl = 'https://github.com/login/oauth/authorize';
16+
17+
public private(set) string $accessTokenUrl = 'https://github.com/login/oauth/access_token';
18+
19+
public private(set) string $userDataUrl = 'https://api.github.com/user';
20+
21+
// TODO : goto trait
22+
public function __construct(
23+
public readonly string $clientId,
24+
public readonly string $clientSecret,
25+
) {}
26+
27+
/**
28+
* TODO : goto trait
29+
* Return headers used in access token endpoint
30+
*
31+
* @param string $code The code verifier from OAuth redirection
32+
*
33+
* @return array<string, mixed>
34+
*/
35+
public function getAccessTokenHeaders(string $code): array
36+
{
37+
return [
38+
'Accept' => 'application/json',
39+
'Content-Type' => 'application/json'
40+
];
41+
}
42+
43+
/**
44+
* TODO : goto trait
45+
* Return body fields used in access token endpoint
46+
*
47+
* @param string $code The code verifier from OAuth redirection
48+
*
49+
* @return array<string, mixed>
50+
*/
51+
public function getAccessTokenFields(string $code): array
52+
{
53+
return [
54+
'grant_type' => 'authorization_code',
55+
'client_id' => $this->clientId,
56+
'client_secret' => $this->clientSecret,
57+
'code' => $code,
58+
'redirect_uri' => 'http://127.0.0.1:8000/auth/github/callback'
59+
];
60+
}
61+
62+
/**
63+
* TODO : goto trait, maybe as abstract method
64+
*/
65+
public function getUserDataFromResponse(array $body): OAuthUserData
66+
{
67+
return new OAuthUserData(
68+
id: $body['id'],
69+
nickname: $body['login'],
70+
name: $body['name'],
71+
email: $body['email'],
72+
avatar: $body['avatar_url'] ?? '',
73+
);
74+
}
75+
}

packages/auth/src/OAuth/GithubSSOProvider.php

Lines changed: 0 additions & 23 deletions
This file was deleted.

packages/auth/src/OAuth/OAuth2ProviderContract.php

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
namespace Tempest\Auth\OAuth;
66

7-
interface OAuth2ProviderContract extends OAuthProviderContract
7+
use Tempest\Auth\OAuth\DataObjects\OAuthUserData;
8+
9+
interface OAuth2ProviderContract
810
{
911
/**
1012
* @var array<string> The default scopes for the OAuth2 provider.
@@ -26,5 +28,28 @@ interface OAuth2ProviderContract extends OAuthProviderContract
2628
*/
2729
public string $userDataUrl {get;}
2830

29-
// public function getUserData(array $headers = []): OAuth2UserData;
31+
/**
32+
* Return headers used in access token endpoint
33+
*
34+
* @param string $code The code verifier from OAuth redirection
35+
*
36+
* @return array<string, mixed>
37+
*/
38+
public function getAccessTokenHeaders(string $code): array;
39+
40+
/**
41+
* Return body fields used in access token endpoint
42+
*
43+
* @param string $code The code verifier from OAuth redirection
44+
*
45+
* @return array<string, mixed>
46+
*/
47+
public function getAccessTokenFields(string $code): array;
48+
49+
/**
50+
* Transform the response body into valid user data object
51+
*
52+
* @param array $body Json decoded response body from the user data endpoint
53+
*/
54+
public function getUserDataFromResponse(array $body): OAuthUserData;
3055
}

packages/auth/src/OAuth/OAuthManager.php

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,24 @@
44

55
namespace Tempest\Auth\OAuth;
66

7-
use Tempest\Container\Inject;
7+
use Tempest\Auth\OAuth\DataObjects\AccessToken;
8+
use Tempest\Auth\OAuth\DataObjects\OAuthUserData;
9+
use Tempest\Http\Session\Session;
810
use Tempest\HttpClient\HttpClient;
11+
use function dd;
12+
use function json_encode;
13+
use function Tempest\get;
914
use function Tempest\Support\str;
1015

1116
final class OAuthManager
1217
{
13-
#[Inject]
14-
private HttpClient $httpClient;
18+
private readonly HttpClient $httpClient;
1519

1620
public function __construct(
1721
private readonly OAuth2ProviderContract $driver,
18-
) {}
22+
) {
23+
$this->httpClient = get(HttpClient::class);
24+
}
1925

2026
public function generateAuthorizationUrl(
2127
?array $scopes = null,
@@ -29,13 +35,74 @@ public function generateAuthorizationUrl(
2935

3036
if ( ! $isStateless ) {
3137
$queryData['state'] = $this->generateState();
38+
// TODO : Store the state in the session for later validation
3239
}
3340

3441
$queryString = http_build_query(array_filter($queryData), arg_separator: '&');
3542

3643
return $this->driver->authorizationUrl . '?' . $queryString;
3744
}
3845

46+
public function generateAccessToken(
47+
string $code,
48+
?string $state = null
49+
): AccessToken {
50+
$response = $this->httpClient->post(
51+
uri: $this->driver->accessTokenUrl,
52+
headers: $this->driver->getAccessTokenHeaders($code),
53+
body: json_encode($this->driver->getAccessTokenFields($code))
54+
);
55+
56+
try {
57+
$body = json_decode($response->body, associative: true);
58+
$accessToken = AccessToken::from($body);
59+
} catch ( \Error $e ) {
60+
$errorMessage = 'Failed to decode access token response.';
61+
62+
if ( isset($body['error'], $body['error_description']) ) {
63+
$errorMessage .= sprintf(
64+
' Error: "%s". Description: "%s"',
65+
$body['error'],
66+
$body['error_description']
67+
);
68+
}
69+
70+
throw new \RuntimeException( $errorMessage );
71+
}
72+
73+
return $accessToken;
74+
}
75+
76+
public function fetchUserDataFromToken(AccessToken $accessToken): OAuthUserData
77+
{
78+
$response = $this->httpClient->get(
79+
uri: $this->driver->userDataUrl,
80+
headers: [
81+
'Authorization' => $accessToken->tokenType . ' ' . $accessToken->accessToken,
82+
'Accept' => 'application/json',
83+
]
84+
);
85+
86+
try {
87+
$body = json_decode($response->body, associative: true);
88+
$userData = $this->driver->getUserDataFromResponse($body);
89+
} catch ( \Error $e ) {
90+
$errorMessage = 'Failed to get user data.';
91+
92+
if ( isset($body['error'], $body['error_description']) ) {
93+
$errorMessage .= sprintf(
94+
' Error: "%s". Description: "%s"',
95+
$body['error'],
96+
$body['error_description']
97+
);
98+
}
99+
100+
throw new \RuntimeException( $errorMessage );
101+
}
102+
103+
return $userData;
104+
}
105+
39106
private function formatScopes(array $scopes, string $scopeSeparator): string
40107
{
41108
return implode($scopeSeparator, $scopes);

tests/Integration/Auth/AuthSSOTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
namespace Tests\Tempest\Integration\Auth;
66

7-
use Tempest\Auth\OAuth\GithubSSOProvider;
7+
use Tempest\Auth\OAuth\GithubOAuthProvider;
88
use Tempest\Auth\OAuth\OAuthManager;
99
use Tempest\Support\Namespace\Psr4Namespace;
1010
use Tests\Tempest\Integration\FrameworkIntegrationTestCase;

0 commit comments

Comments
 (0)