Skip to content

Commit

Permalink
Fetch access token
Browse files Browse the repository at this point in the history
  • Loading branch information
Ioannis committed Apr 28, 2025
1 parent 89de835 commit 188c831
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 22 deletions.
12 changes: 12 additions & 0 deletions app/plugins/CoreServer/resources/locales/en_US/core_server.po
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ msgstr "{0,plural,=1{SMTP Server} other{SMTP Servers}}"
msgid "controller.SqlServers"
msgstr "{0,plural,=1{SQL Server} other{SQL Servers}}"

msgid "controller.Oauth2Servers"
msgstr "{0,plural,=1{Oauth2 Server} other{Oauth2 Servers}}"

msgid "enumeration.HttpAuthTypeEnum.BA"
msgstr "Basic"

Expand Down Expand Up @@ -76,6 +79,12 @@ msgstr "Invalid state received in callback"
msgid "error.Oauth2Servers.token"
msgstr "Error obtaining access token: {0}"

msgid "info.Oauth2Servers.token.ok"
msgstr "Access Token Obtained"

msgid "info.Oauth2Servers.token.obtain"
msgstr "Obtain New Token"

msgid "field.auth_type"
msgstr "Authentication Type"

Expand Down Expand Up @@ -109,6 +118,9 @@ msgstr "Redirect URI"
msgid "field.Oauth2Servers.scope"
msgstr "Scopes"

msgid "info.Oauth2Servers.access_token.ok"
msgstr "Access Token Obtained"

msgid "field.SmtpServers.default_from"
msgstr "Default From Address"

Expand Down
69 changes: 60 additions & 9 deletions app/plugins/CoreServer/src/Controller/Oauth2ServersController.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

use App\Controller\StandardPluginController;
use Cake\Event\EventInterface;
use CoreServer\Lib\Enum\Oauth2GrandTypesEnum;

class Oauth2ServersController extends StandardPluginController
{
Expand Down Expand Up @@ -68,29 +69,79 @@ public function beforeRender(EventInterface $event)
* @since COmanage Registry v5.2.0
*/

public function callback($id)
public function callback($id): void
{
$this->autoRender = false;
// We have to look in $_GET because what we get back isn't a Cake style named parameter
// (ie: code=foo, not code:foo)

try {
if (empty($_GET['code']) || empty($_GET['state'])) {
throw new RuntimeException(__d('core_server', 'error.Oauth2Servers.callback'));
throw new \RuntimeException(__d('core_server', 'error.Oauth2Servers.callback'));
}

// Verify that state is our hashed session ID, as per RFC6749 §10.12
// recommendations to prevent CSRF.
// https://tools.ietf.org/html/rfc6749#section-10.12

if ($_GET['state'] != hash('sha256', session_id())) {
throw new RuntimeException(__d('core_server', 'error.Oauth2Servers.state'));
throw new \RuntimeException(__d('core_server', 'error.Oauth2Servers.state'));
}

$response = $this->Oauth2Servers->exchangeCode($id, $_GET['code'], $this->Oauth2Server->redirectUri($id));
$response = $this->Oauth2Servers->exchangeCode($id, $_GET['code'], $this->Oauth2Servers->redirectUri((int)$id));

$this->Flash->set(_txt('rs.server.oauth2.token.ok'), array('key' => 'success'));
} catch (Exception $e) {
$this->Flash->set($e->getMessage(), array('key' => 'error'));
$this->Flash->success(__d('core_server', 'info.Oauth2Servers.access_token.ok'));
} catch (\Exception $e) {
$this->Flash->error($e->getMessage());
}

$this->performRedirect();
}

/**
* Obtain an access token for a Oauth2Server.
*
* @since COmanage Registry v5.2.0
* @param integer $id Oauth2Server ID
*/

public function token($id): void
{
// Pull our configuration, initially to find out what type of grant type we need
$osrvr = $this->Oauth2Servers->get($id);

if(!$osrvr) {
$this->Flash->error(__d('error', 'notfound', [__d('core_server', 'controller.Oauth2Servers')]));
$this->performRedirect();
}

try {
switch($osrvr->access_grant_type) {
case Oauth2GrandTypesEnum::AuthorizationCode:
// Issue a redirect to the server
$targetUrl = $osrvr->url
. '/authorize?response_type=code'
. '&client_id=' . $osrvr->clientid
. '&redirect_uri=' . urlencode($this->Oauth2Servers->redirectUri($id))
. '&state=' . hash('sha256', session_id());
// Scope is optional
if(!empty($osrvr->scope)) {
$targetUrl .= '&scope='. str_replace(' ', '%20', $osrvr->scope);
}

$this->redirect($targetUrl);
break;
case Oauth2GrandTypesEnum::ClientCredentials:
// Make a direct call to the server
$this->Oauth2Servers->obtainToken((int)$id, 'client_credentials');
$this->Flash->success(__d('core_server', 'info.Oauth2Servers.access_token.ok'));
break;
default:
// No other flows currently supported
throw new LogicException('NOT IMPLEMENTED');
}
} catch(\Exception $e) {
$this->Flash->error($e->getMessage());
}

$this->performRedirect();
Expand Down Expand Up @@ -118,8 +169,8 @@ function performRedirect(): void
$target['?'] = [
'co_id' => $this->getCOID()
];

$this->redirect($target);
}

$this->redirect($target);
}
}
135 changes: 124 additions & 11 deletions app/plugins/CoreServer/src/Model/Table/Oauth2ServersTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
use CoreServer\Lib\Enum\Oauth2GrandTypesEnum;

class Oauth2ServersTable extends HttpServersTable {
use \App\Lib\Traits\PrimaryLinkTrait;

/**
* Perform Cake Model initialization.
*
Expand All @@ -57,6 +59,8 @@ public function initialize(array $config): void {
$this->setDisplayField('hostname');

$this->setPrimaryLink('server_id');
$this->setAllowLookupPrimaryLink(['token', 'callback']);

$this->setRequiresCO(true);

$this->setAutoViewVars([
Expand All @@ -71,24 +75,122 @@ public function initialize(array $config): void {
'entity' => [
'delete' => false, // Delete the pluggable object instead
'edit' => ['platformAdmin', 'coAdmin'],
'view' => ['platformAdmin', 'coAdmin']
'view' => ['platformAdmin', 'coAdmin'],
'token' => ['platformAdmin', 'coAdmin'],
'callback' => true,
],
// Actions that operate over a table (ie: do not require an $id)
'table' => [
'add' => ['platformAdmin', 'coAdmin'],
'index' => ['platformAdmin', 'coAdmin']
'index' => ['platformAdmin', 'coAdmin'],
]
]);
}


/**
* Exchange an authorization code for an access and refresh token.
*
* @param int|string $id Oauth2Server ID
* @param string $code Access code returned by call to /oauth/authorize
* @param string $redirectUri Callback URL used for initial request
* @return mixed Object of data as returned by server, including access and refresh token
* @throws RuntimeException
*@since COmanage Registry v5.2.0
*/

public function exchangeCode(int|string $id, string $code, string $redirectUri, $store=true): mixed
{
return $this->obtainToken((int)$id, 'authorization_code', $code, $redirectUri, $store);
}

/**
* Obtain an OAuth token.
*
* @param Integer $id Oauth2Server ID
* @param String $grantType OAuth grant type
* @param String|null $code Access code returned by call to /oauth/authorize, for authorization_code grant
* @param String|null $redirectUri Callback URL used for initial request, for authorization_code grant
* @param Boolean $store If true, store the retrieved tokens in the Oauth2Server configuration
* @return mixed Object of data as returned by server, including access and refresh token
* @throws RuntimeException
*@since COmanage Registry v5.2.0
*/

public function obtainToken(int $id, string $grantType, string $code=null, string $redirectUri=null, bool $store=true): mixed
{
// Pull our configuration
$srvr = $this->get($id);

$httpClient = $this->createHttpClient($id);

$postData = [
'client_id' => $srvr->clientid,
'client_secret' => $srvr->client_secret,
'grant_type' => $grantType
];

if($grantType == 'refresh_token') {
$postData['refresh_token'] = $srvr->refresh_token;
$postData['format'] = 'json';
} elseif($grantType == 'authorization_code' && $code) {
$postData['code'] = $code;
$postData['redirect_uri'] = $redirectUri;
} else {
$postData['scope'] = str_replace(' ', '%20', $srvr->scope);
}

$postUrl = $srvr->url . "/token";

$results = $httpClient->post($postUrl, $postData);

$json = json_decode($results->getStringBody());

if($results->getStatusCode() != 200) {
// There should be an error in the response
throw new \RuntimeException(__d('core_server', 'error.Oauth2Servers.token', [$json->error . ": " . $json->error_description]));
}

if($store) {
// Save the fields we want to keep
$data = [
'id' => $id,
'access_token' => $json->access_token,
// Store the raw result in case the server has added some custom attributes
'token_response' => json_encode($json)
];

// We shouldn't have a new refresh token on a refresh_token grant
// (which just gets us a new access token). Additionally, section
// 4.4.3 of RFC 6749 explains that the server should NOT return
// a refresh token for a client credentials grant.
if($grantType != 'refresh_token' && property_exists($json, 'refresh_token')) {
$data['refresh_token'] = $json->refresh_token;
}

// If the Oauth2 server returned `expires_in` use it to set the
// access token expiration time. See section 5.1 of RFC 6749.
if(property_exists($json, 'expires_in')) {
$data['access_token_exp'] = time() + $json->expires_in;
}

// Update the dataset
$srvr = $this->patchEntity($srvr, $data);
if (!$this->save($srvr)) {
throw new \RuntimeException(__d('error', 'save' [__d('core_server', 'field.Oauth2Servers.access_token')]));
}
}

return $json;
}


/**
* Generate a redirect URI for the given server ID.
*
* @param int|string $id The unique identifier of the OAuth2 server
* @return string The full URL of the redirect URI
*/
public function redirectUri($id): string
public function redirectUri(int|string $id): string
{
$callback = [
'plugin' => 'CoreServer',
Expand All @@ -101,6 +203,22 @@ public function redirectUri($id): string
}


/**
* Refresh the OAuth access token using the stored refresh token.
*
* @param int|string $id The unique identifier of the OAuth2 server
* @return string The new access token
* @throws RuntimeException
* @since COmanage Registry v5.2.0
*/
public function refreshToken(int|string $id):string
{
$json = $this->obtainToken((int)$id, 'refresh_token');

return $json->access_token;
}


/**
* Set validation rules.
*
Expand Down Expand Up @@ -152,13 +270,8 @@ public function validationDefault(Validator $validator): Validator {
]);
$validator->allowEmptyString('access_token');

$validator->add('access_token_exp', [
'content' => [
'rule' => 'validateNotBlank',
'provider' => 'table'
]
]);
$validator->allowEmptyString('access_token_exp');
$validator->integer('access_token_exp')
->allowEmptyString('access_token_exp');

return $validator;
}
Expand Down
22 changes: 20 additions & 2 deletions app/plugins/CoreServer/templates/Oauth2Servers/fields.inc
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,25 @@ print $this->element('form/listItem', [
],
]]);

$generateLink = [];
if(!empty($vv_obj->id)) {
$generateLink = [
'url' => [
'plugin' => 'CoreServer',
'controller' => 'Oauth2Servers',
'action' => 'token',
$vv_obj->id
],
'label' => __d('core_server', 'info.Oauth2Servers.token.obtain'),
'class' => 'provisionbutton nospin btn btn-primary btn-sm',
];
}

print $this->element('form/listItem', [
'arguments' => [
'fieldName' => 'access_token'
]]);
'fieldName' => 'access_token',
'status' => !empty($vv_obj->access_token) ? __d('enumeration', 'SetBooleanEnum.1') : __d('enumeration', 'SetBooleanEnum.0'),
'link' => $generateLink,
'labelIsTextOnly' => true
]
]);

0 comments on commit 188c831

Please sign in to comment.