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+ }
0 commit comments