1: <?php
2: /*
3: * SimpleID
4: *
5: * Copyright (C) Kelvin Mo 2014-2025
6: *
7: * This program is free software; you can redistribute it and/or
8: * modify it under the terms of the GNU General Public
9: * License as published by the Free Software Foundation; either
10: * version 2 of the License, or (at your option) any later version.
11: *
12: * This program is distributed in the hope that it will be useful,
13: * but WITHOUT ANY WARRANTY; without even the implied warranty of
14: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15: * General Public License for more details.
16: *
17: * You should have received a copy of the GNU General Public
18: * License along with this program; if not, write to the Free
19: * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
20: *
21: */
22:
23: namespace SimpleID\Protocols\OAuth;
24:
25: use \Base;
26: use \Prefab;
27: use Psr\Log\LogLevel;
28: use SimpleID\ModuleManager;
29: use SimpleID\Store\StoreManager;
30: use SimpleID\Util\Events\BaseDataCollectionEvent;
31:
32: /**
33: * The manager handling OAuth based authentication
34: */
35: class OAuthManager extends Prefab {
36: /** @var Base */
37: protected $f3;
38:
39: /** @var \Psr\Log\LoggerInterface */
40: protected $logger;
41:
42: /** @var ModuleManager */
43: protected $mgr;
44:
45: /** @var AccessToken|null */
46: private $access_token = NULL;
47:
48: /** @var string */
49: private $client_auth_method;
50:
51: public function __construct() {
52: $this->f3 = Base::instance();
53: $this->logger = $this->f3->get('logger');
54: $this->mgr = ModuleManager::instance();
55: }
56:
57: /**
58: * Authenticates the OAuth client.
59: *
60: * This function detects whether credentials for an OAuth client is
61: * presented in the `Authorization` header or the POST body.
62: *
63: * @return void
64: */
65: public function initClient() {
66: $this->logger->log(LogLevel::DEBUG, 'SimpleID\Protocols\OAuth\OAuthManager->initClient');
67:
68: $store = StoreManager::instance();
69:
70: $request = new Request();
71: $header = $request->getAuthorizationHeader(true);
72:
73: if (($header != null) && ($header['#scheme'] == 'Basic')) {
74: $client_auth_method = 'client_secret_basic';
75: $client_id = $header['#username'];
76: $client_secret = $header['#password'];
77: }
78:
79: if (!isset($client_id) && $this->f3->exists('POST.client_id') && $this->f3->exists('POST.client_secret')) {
80: $client_auth_method = 'client_secret_post';
81: $client_id = $this->f3->get('POST.client_id');
82: $client_secret = $this->f3->get('POST.client_secret');
83: }
84:
85: if (isset($client_id)) {
86: $client = $store->loadClient($client_id, 'SimpleID\Protocols\OAuth\OAuthClient');
87:
88: if ($client['oauth']['client_secret'] != $client_secret) return; // @phpstan-ignore-line
89:
90: $this->client_auth_method = $client_auth_method; // @phpstan-ignore-line
91: } else {
92: $event = new OAuthInitClientEvent($request);
93: \Events::instance()->dispatch($event);
94:
95: if ($event->hasClient()) {
96: $client = $event->getClient();
97: $client_id = $client->getStoreID();
98: $this->client_auth_method = $event->getAuthMethod();
99: }
100: }
101:
102: if (isset($client)) {
103: $this->f3->set('oauth_client', $client);
104:
105: $this->logger->log(LogLevel::INFO, 'OAuth client: ' . $client_id . ' [' . $this->client_auth_method . ']'); // @phpstan-ignore-line
106: }
107: }
108:
109:
110: /**
111: * Returns whether an authenticated OAuth client is present.
112: *
113: * If the `$send_challenge` parameter is set to true, a `WWW-Authenticate`
114: * header will be sent if an authenticated OAuth client is
115: * not present
116: *
117: * @param bool $send_challenge if a challenge is to be sent
118: * @param array<string>|null $auth_methods expected authentication method
119: * @return bool true if an authenticated OAuth client is present
120: */
121: public function isClientAuthenticated($send_challenge = false, $auth_methods = null) {
122: $this->logger->log(LogLevel::DEBUG, 'SimpleID\Protocols\OAuth\OAuthManager->isClientAuthenticated');
123:
124: $result = $this->f3->exists('oauth_client');
125: if ($result && ($auth_methods != null)) {
126: if (!is_array($auth_methods)) $auth_methods = [ $auth_methods ];
127: $result = in_array($this->client_auth_method, $auth_methods);
128: if (!$result) {
129: $this->logger->log(LogLevel::ERROR, 'Unexpected authentication method: ' . $this->client_auth_method . '; expecting ' . implode(',', $auth_methods));
130: }
131: }
132:
133: if ($result) {
134: return true;
135: } else {
136: if ($send_challenge) {
137: $auth_method_map = [
138: 'client_secret_basic' => 'Basic'
139: ];
140: $http_auth_method = $auth_method_map[$this->client_auth_method];
141: $this->f3->status(401);
142: header('WWW-Authenticate: ' . $http_auth_method . ' realm="'. $this->f3->get('REALM') . '"');
143: }
144:
145: return false;
146: }
147: }
148:
149: /**
150: * Returns the authenticated OAuth client, if any.
151: *
152: * @return OAuthClient|null the authenticated OAuth client, or null
153: */
154: public function getClient() {
155: if ($this->f3->exists('oauth_client')) return $this->f3->get('oauth_client');
156: return null;
157: }
158:
159: /**
160: * Returns the method used to authenticate the current OAuth client.
161: *
162: * @return string the authentication method
163: */
164: public function getClientAuthMethod() {
165: return $this->client_auth_method;
166: }
167:
168: /**
169: * Returns a list of supported methods to authenticate an OAuth client
170: *
171: * @return array<string> a list of supported methods
172: */
173: public function getSupportedClientAuthMethods(): array {
174: $dispatcher = \Events::instance();
175: $event = new BaseDataCollectionEvent('oauth_supported_client_auth_methods');
176: $event->addResult('client_secret_basic');
177: $event->addResult('client_secret_post');
178: $dispatcher->dispatch($event);
179:
180: return $event->getResults();
181: }
182:
183: /**
184: * Authenticates the OAuth access token.
185: *
186: * This function detects whether an access token has been presented.
187: *
188: * @param bool $include_request_body if true, also detects access tokens
189: * from the request body
190: * @return void
191: */
192: public function initAccessToken($include_request_body = false) {
193: $this->logger->log(LogLevel::DEBUG, 'SimpleID\Protocols\OAuth\OAuthManager->initAccessToken');
194:
195: $bearer_token = $this->initBearerAccessToken($include_request_body);
196: if ($bearer_token) {
197: $this->access_token = AccessToken::decode($bearer_token);
198: return;
199: }
200:
201: // Try other token types
202: $event = new OAuthInitTokenEvent();
203: \Events::instance()->dispatch($event);
204: if ($event->hasToken()) {
205: $this->access_token = $event->getToken();
206: }
207: }
208:
209: /**
210: * Authenticates the OAuth bearer access token.
211: *
212: * @param bool $include_request_body if true, also detects access tokens
213: * from the request body
214: * @return string
215: */
216: protected function initBearerAccessToken($include_request_body = false) {
217: $encoded_token = null;
218:
219: $request = new Request();
220: $header = $request->getAuthorizationHeader();
221:
222: if ($header) {
223: if ($header['#scheme'] == 'Bearer')
224: $encoded_token = $header['#credentials'];
225: }
226:
227: if (!$encoded_token && $include_request_body && $this->f3->exists('REQUEST.access_token')) {
228: $encoded_token = $this->f3->get('REQUEST.access_token');
229: }
230:
231: return $encoded_token;
232: }
233:
234: /**
235: * Returns the access token included in the request.
236: *
237: * @return AccessToken|null the access token
238: */
239: public function getAccessToken() {
240: return $this->access_token;
241: }
242:
243: /**
244: * Returns whether the current access token is authorised under the
245: * specified scope.
246: *
247: * @param array<string>|string $scope the scope
248: * @param string &$error the error code returned if the access token
249: * is not authorised
250: * @return bool true if the access token is authorised
251: */
252: public function isTokenAuthorized($scope, &$error = null) {
253: if (!$this->access_token) {
254: if ($error !== null) $error = '';
255: return false;
256: }
257: if (!$this->access_token->isValid()) {
258: if ($error !== null) $error = 'invalid_token';
259: return false;
260: }
261: if (!$this->access_token->hasScope($scope)) {
262: if ($error !== null) $error = 'insufficient_scope';
263: return false;
264: }
265:
266: return true;
267: }
268: }
269: ?>
270: