Skip to content

Commit 9dd63db

Browse files
committed
SS14 OAuth2
1 parent 9808777 commit 9dd63db

File tree

4 files changed

+417
-7
lines changed

4 files changed

+417
-7
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
},
2323
"autoload": {
2424
"psr-4": {
25+
"SS14\\": "src/SS14/",
2526
"VerifierServer\\": "src/VerifierServer/"
2627
}
2728
},

src/SS14/OAuth2Authenticator.php

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
<?php declare(strict_types=1);
2+
3+
/*
4+
* This file is a part of the Civ14 project.
5+
*
6+
* Copyright (c) 2025-present Valithor Obsidion <valithor@valzargaming.com>
7+
*/
8+
9+
namespace SS14;
10+
11+
use Psr\Http\Message\ServerRequestInterface;
12+
use React\Http\Message\Response;
13+
14+
class OAuth2Authenticator
15+
{
16+
protected array $params;
17+
protected string $requesting_ip;
18+
19+
protected string $oidc_config = 'https://account.spacestation14.com/.well-known/openid-configuration';
20+
protected string $issuer = 'https://account.spacestation14.com';
21+
protected string $authorization_endpoint = '/connect/authorize';
22+
protected string $token_endpoint = '/connect/token';
23+
protected string $userinfo_endpoint = '/connect/userinfo';
24+
//protected string $end_session_endpoint = '/connect/endsession'; // Unused
25+
//protected string $check_session_iframe = '/connect/checksession'; // Unused
26+
protected string $revocation_endpoint = '/connect/revocation'; // Unused
27+
//protected string $introspection_endpoint = '/connect/introspect'; // Unused
28+
//protected string $device_authorization_endpoint = '/connect/deviceauthorization'; // Unused
29+
30+
protected string $default_redirect;
31+
32+
protected string $state;
33+
protected ?string $access_token = null;
34+
public ?object $user = null;
35+
36+
protected string $redirect_home;
37+
protected array $allowed_uri = [];
38+
39+
/**
40+
* OAuth2Authenticator constructor.
41+
*
42+
* Initializes the OAuth2 authentication process by setting up session data,
43+
* request parameters, allowed URIs, and other necessary configurations.
44+
*
45+
* @param array &$sessions
46+
* @param string $resolved_ip
47+
* @param string $web_address
48+
* @param int $http_port
49+
* @param ServerRequestInterface $request
50+
* @param string $client_id
51+
* @param string $client_secret
52+
* @param string $endpoint_name
53+
* @param string $scope
54+
*
55+
* @throws \RuntimeException If the provided request is not an instance of ServerRequestInterface.
56+
*/
57+
public function __construct(
58+
protected array &$sessions,
59+
string $resolved_ip,
60+
string $web_address,
61+
int $http_port,
62+
$request,
63+
protected string $client_id,
64+
protected string $client_secret,
65+
protected string $endpoint_name = 'ss14wa',
66+
protected string $scope = 'openid profile email'
67+
) {
68+
if (! $request instanceof ServerRequestInterface) {
69+
throw new \RuntimeException('String requests are not supported.');
70+
}
71+
72+
$this->params = $request->getQueryParams();
73+
$this->requesting_ip = $request->getServerParams()['REMOTE_ADDR'] ?? '127.0.0.1';
74+
$scheme = 'http'; //$request->getUri()->getScheme();
75+
if ($host = $request->getUri()->getHost() === '127.0.0.1') $host = $resolved_ip; // Should only happen when testing on localhost
76+
$path = $request->getUri()->getPath();
77+
$this->default_redirect = "$scheme://$host:$http_port" . explode('?', $path)[0];
78+
79+
$this->redirect_home = "$scheme://$web_address:$http_port";
80+
$this->allowed_uri[] = $this->redirect_home;
81+
$this->allowed_uri[] = $this->redirect_home . "/{$this->endpoint_name}";
82+
if ($resolved_ip) {
83+
$this->allowed_uri[] = "$scheme://$resolved_ip:$http_port/";
84+
$this->allowed_uri[] = "$scheme://$resolved_ip:$http_port/{$this->endpoint_name}";
85+
}
86+
87+
$this->state = isset($this->sessions[$this->requesting_ip]['state'])
88+
? $this->sessions[$this->requesting_ip]['state']
89+
: $this->sessions[$this->requesting_ip]['state'] = uniqid();
90+
91+
if (isset($this->sessions[$this->requesting_ip]['access_token'])) {
92+
$this->access_token = $this->sessions[$this->requesting_ip]['access_token'];
93+
$this->user = $this->getUser();
94+
}
95+
}
96+
97+
private function apiRequest(
98+
string $url,
99+
array $post = [],
100+
bool $associative = false
101+
): object
102+
{
103+
$headers = ['Accept: application/json'];
104+
if ($this->access_token) $headers[] = 'Authorization: Bearer ' . $this->access_token;
105+
106+
$ch = curl_init($url);
107+
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
108+
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
109+
if ($post) {
110+
curl_setopt($ch, CURLOPT_POST, true);
111+
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post));
112+
}
113+
114+
$response = curl_exec($ch)
115+
?: throw new \RuntimeException('cURL error: ' . curl_error($ch));
116+
117+
$decoded = json_decode($response, $associative);
118+
if (json_last_error() !== JSON_ERROR_NONE) {
119+
throw new \RuntimeException('JSON decode error: ' . json_last_error_msg());
120+
}
121+
return $decoded;
122+
}
123+
124+
public function login(
125+
int|string &$response,
126+
array &$headers,
127+
string &$body,
128+
?string $redirect_uri = null,
129+
?string $scope = null
130+
): void
131+
{
132+
if (!in_array($redirect_uri = $redirect_uri ?? $this->default_redirect, $this->allowed_uri)) {
133+
$response = Response::STATUS_FOUND;
134+
$headers = ['Location' => $this->allowed_uri[0] . '?login'];
135+
$body = '';
136+
return;
137+
}
138+
139+
$response = Response::STATUS_FOUND;
140+
$headers = ['Location' => "{$this->issuer}{$this->authorization_endpoint}?"
141+
. http_build_query([
142+
'client_id' => $this->client_id,
143+
'response_type' => 'code',
144+
'scope' => $scope ?? $this->scope,
145+
'state' => $this->state,
146+
'redirect_uri' => $redirect_uri,
147+
])];
148+
$body = '';
149+
}
150+
151+
public function logout(
152+
int|string &$response,
153+
array &$headers,
154+
string &$body
155+
): void
156+
{
157+
unset($this->sessions[$this->requesting_ip]);
158+
$response = Response::STATUS_FOUND;
159+
$headers = ['Location' => ($this->redirect_home ?? $this->default_redirect)];
160+
$body = '';
161+
}
162+
163+
public function removeToken(
164+
int|string &$response,
165+
array &$headers,
166+
string &$body
167+
): void
168+
{
169+
if ($this->access_token)
170+
{
171+
$this->apiRequest(
172+
$this->issuer . $this->revocation_endpoint,
173+
[
174+
'client_id' => $this->client_id,
175+
'client_secret' => $this->client_secret,
176+
'access_token' => $this->access_token
177+
]
178+
);
179+
}
180+
$this->logout($response, $headers, $body);
181+
}
182+
183+
public function getToken(
184+
int|string &$response,
185+
array &$headers,
186+
string &$body,
187+
string $code,
188+
string $state,
189+
string $redirect_uri = ''
190+
): ?string
191+
{
192+
if ($state === $this->state) {
193+
$token = $this->apiRequest(
194+
$this->issuer . $this->token_endpoint,
195+
[
196+
'client_id' => $this->client_id,
197+
'client_secret' => $this->client_secret,
198+
'grant_type' => 'authorization_code',
199+
'code' => $code,
200+
'redirect_uri' => $redirect_uri ?: $this->default_redirect,
201+
]
202+
);
203+
204+
if (isset($token->error)) {
205+
$response = Response::STATUS_BAD_REQUEST;
206+
$headers = ['Content-Type' => 'text/plain'];
207+
$body = 'Error: ' . $token->error;
208+
return null;
209+
}
210+
211+
$response = Response::STATUS_FOUND;
212+
$headers = ['Location' => $this->redirect_home];
213+
$body = '';
214+
return $this->sessions[$this->requesting_ip]['access_token'] = $token->access_token;
215+
}
216+
$response = Response::STATUS_BAD_REQUEST;
217+
$headers = ['Content-Type' => 'text/plain'];
218+
$body = 'Invalid state.';
219+
return null;
220+
}
221+
222+
public function getUser(): ?object
223+
{
224+
return $this->apiRequest($this->issuer . $this->userinfo_endpoint);
225+
}
226+
227+
public function isAuthed(): bool
228+
{
229+
return $this->user !== null;
230+
}
231+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<?php declare(strict_types=1);
2+
3+
/*
4+
* This file is a part of the Civ14 project.
5+
*
6+
* Copyright (c) 2025-present Valithor Obsidion <valithor@valzargaming.com>
7+
*/
8+
9+
namespace VerifierServer\Endpoints;
10+
11+
use Psr\Http\Message\ServerRequestInterface;
12+
use React\Http\Message\Response;
13+
use SS14\OAuth2Authenticator;
14+
15+
class SS14Oauth2Endpoint extends Endpoint
16+
{
17+
public function __construct(
18+
public array &$sessions,
19+
protected string $resolved_ip,
20+
protected string $web_address,
21+
protected int $http_port,
22+
){}
23+
24+
/**
25+
* @param string $method
26+
* @param ServerRequestInterface $request
27+
* @param int|string &$response
28+
* @param array &$headers
29+
* @param string &$body
30+
*/
31+
public function handle(
32+
string $method,
33+
$request,
34+
int|string &$response,
35+
array &$headers,
36+
string &$body
37+
): void
38+
{
39+
switch ($method) {
40+
case 'GET':
41+
$this->get($request, $response, $headers, $body);
42+
break;
43+
case 'POST':
44+
$this->post($request, $response, $headers, $body);
45+
break;
46+
case 'HEAD':
47+
case 'PUT':
48+
case 'DELETE':
49+
case 'PATCH':
50+
case 'OPTIONS':
51+
case 'CONNECT':
52+
case 'TRACE':
53+
default:
54+
$response = Response::STATUS_METHOD_NOT_ALLOWED;
55+
$headers = ['Content-Type' => 'text/plain'];
56+
$body = 'Method Not Allowed';
57+
break;
58+
}
59+
}
60+
61+
/**
62+
* @param ServerRequestInterface|string $request
63+
* @param int|string &$response
64+
* @param array &$headers
65+
* @param string &$body
66+
*/
67+
private function get(
68+
$request,
69+
int|string &$response,
70+
array &$headers,
71+
string &$body
72+
): void
73+
{
74+
if (! $request instanceof ServerRequestInterface) {
75+
$response = Response::STATUS_METHOD_NOT_ALLOWED;
76+
$headers = ['Content-Type' => 'text/plain'];
77+
$body = 'Method Not Allowed';
78+
return;
79+
}
80+
$params = $request->getQueryParams();
81+
82+
$OAA = new OAuth2Authenticator(
83+
$this->sessions,
84+
$this->resolved_ip,
85+
$this->web_address,
86+
$this->http_port,
87+
$request,
88+
$_ENV['SS14_OAUTH2_CLIENT_ID'],
89+
$_ENV['SS14_OAUTH2_CLIENT_SECRET'],
90+
);
91+
92+
if (isset($params['code'], $params['state'])) {
93+
/*$token =*/ $OAA->getToken($response, $headers, $body, $params['code'], $params['state']);
94+
return;
95+
}
96+
if (isset($params['login'])) {
97+
$OAA->login($response, $headers, $body);
98+
return;
99+
}
100+
if (isset($params['logout'])) {
101+
$OAA->logout($response, $headers, $body);
102+
return;
103+
}
104+
if (isset($params['remove']) && $OAA->isAuthed()) {
105+
$OAA->removeToken($response, $headers, $body);
106+
return;
107+
}
108+
}
109+
110+
/**
111+
* @param ServerRequestInterface|string $request
112+
* @param string|int &$response
113+
* @param array &$headers
114+
* @param string &$body
115+
*/
116+
private function post(
117+
$request,
118+
int|string &$response,
119+
array &$headers,
120+
string &$body
121+
): void
122+
{
123+
$this->get($request, $response, $headers, $body);
124+
}
125+
}

0 commit comments

Comments
 (0)