Skip to content

Commit

Permalink
Merge pull request #286 from silinternational/feature/store-access-to…
Browse files Browse the repository at this point in the history
…ken-in-session

store access token in session
  • Loading branch information
hobbitronics authored Aug 7, 2024
2 parents 070e0fe + 86b160a commit 12e7b66
Show file tree
Hide file tree
Showing 18 changed files with 488 additions and 271 deletions.
39 changes: 13 additions & 26 deletions api.raml
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ version: 4
protocols: [ HTTPS ]
mediaType: application/json
securitySchemes:
AuthzBearerToken:
type: x-{other}
AuthzHttpOnlyCookie:
type: x-cookie
describedBy:
headers:
Authorization:
Cookie:
description: |
The access token is stored in a secure, HttpOnly cookie
type: string
example: Bearer gwgyxgf0vbuwYrI60sbbxLM3U1Jjw2lyvG53Kjjy9cq
securedBy: [ AuthzBearerToken ]
example: access_token=your_secure_token_value
securedBy: [ AuthzHttpOnlyCookie ]
types:
Config:
type: object
Expand Down Expand Up @@ -221,9 +223,6 @@ types:
get:
securedBy: null
queryParameters:
client_id:
description: unique identifier provided by the UI client
type: string
ReturnTo:
description: URL to return to after login or invite
type: string
Expand All @@ -236,18 +235,16 @@ types:
responses:
302:
description: >
On successful login or invite, a redirection to `ReturnTo` with `state`, `token_type`,
`expires_utc` and `access_token` appended as query string parameters. If `invite`
was provided, but it has expired, this response will be a redirection to
`ReturnToOnError` with no query string parameters.
On successful login or invite, a redirection to `ReturnTo`. If `invite` was provided, but it has expired,
this response will be a redirection to `ReturnToOnError` with no query string parameters.
headers:
location:
description: redirect URL
type: string
example:
https://idp-pw.org/profile/intro?state=&token_type=Bearer&expires_utc=2018-12-20T18:41:52Z&access_token=hgEzk1kceC40FHeJm6cyfr5FgE9Tc4UH
https://idp-pw.org/profile/intro?state=&expires_utc=2018-12-20T18:41:52Z
example:
https://idp-pw.org/profile/intro?state=&token_type=Bearer&expires_utc=2018-12-20T18:41:52Z&access_token=hgEzk1kceC40FHeJm6cyfr5FgE9Tc4UH
https://idp-pw.org/profile/intro?state=&expires_utc=2018-12-20T18:41:52Z
400:
description: Error or missing information in request.
post:
Expand All @@ -260,10 +257,6 @@ types:
/logout:
get:
securedBy: null
queryParameters:
access_token:
description: token provided at login
type: string
responses:
200:
/config:
Expand Down Expand Up @@ -657,19 +650,13 @@ types:
body:
properties:
code: string
client_id: string
responses:
200:
description: >
Reset was validated. Response contains a 32-character access
token. Subsequent API calls should use `client_id` concatenated
with `access_token` in the Authorization header.
body:
properties:
access_token: string
Reset was validated. Subsequent API calls will use a http only cookie for the access token.
400:
description: >
The provided code was incorrect or `client_id` was missing.
The provided code was incorrect.
410:
description: >
The code has expired.
Expand Down
42 changes: 42 additions & 0 deletions application/common/components/auth/HttpOnlyAuth.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

namespace common\components\auth;

use Yii;
use yii\filters\auth\AuthMethod;
use yii\web\UnauthorizedHttpException;

class HttpOnlyAuth extends AuthMethod
{
/**
* Authenticates the user based on the access_token stored in the session.
* @param \yii\web\User $user the user object
* @param \yii\web\Request $request the request object
* @param \yii\web\Response $response the response object
* @return \yii\web\IdentityInterface | null the authenticated user identity. If authentication information is not provided, null will be returned.
* @throws UnauthorizedHttpException if authentication information is provided but is invalid.
*/
public function authenticate($user, $request, $response)
{
$accessToken = $request->cookies->getValue('access_token');

if ($accessToken === null) {
return null;
}

$identity = $user->loginByAccessToken($accessToken, get_class($this));
if ($identity === null) {
$this->handleFailure($response);
}

return $identity;
}

/**
* @inheritdoc
*/
public function handleFailure($response)
{
throw new UnauthorizedHttpException('Your request was made with invalid credentials.');
}
}
4 changes: 2 additions & 2 deletions application/common/config/main.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@
$prefixData = [
'env' => YII_ENV,
];
$userId = Yii::$app->user->isGuest ? 'guest' : Yii::$app->user->id;
if ($request instanceof \yii\web\Request) {
// Assumes format: Bearer consumer-module-name-32randomcharacters
$prefixData['id'] = substr($request->headers['Authorization'], 7, 16) ?: 'unknown';
$prefixData['id'] = $userId;
$prefixData['ip'] = $request->getUserIP();
$prefixData['method'] = $request->getMethod();
$prefixData['url'] = $request->getUrl();
Expand Down
42 changes: 0 additions & 42 deletions application/common/helpers/Utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -304,48 +304,6 @@ public static function getZxcvbnScore($password)
}
}

/**
* Get client_id from request or session and then store in session
* @return string
* @throws \Exception
*/
public static function getClientIdOrFail()
{
$request = \Yii::$app->request;
if (\Yii::$app->request->isPut) {
$clientId = $request->getBodyParam('client_id');
} else {
$clientId = $request->get('client_id');
}

if ($clientId === null) {
$clientId = \Yii::$app->session->get('clientId');
if ($clientId === null) {
\Yii::warning([
'action' => 'login - get client id or fail',
'status' => 'error',
'request_method' => $request->getMethod(),
'request_url' => $request->getAbsoluteUrl(),
'body_params' => $request->getBodyParams(),
'user_agent' => $request->getUserAgent(),
]);
throw new \Exception('Missing client_id');
}
}
try {
\Yii::$app->session->set('clientId', $clientId);
} catch (\Exception $e) {
\Yii::error([
'action' => 'login - save clientId to session',
'status' => 'error',
'message' => $e->getMessage(),
]);
throw $e;
}

return $clientId;
}

/**
* Return HMAC SHA256 of access token
* @param string $accessToken
Expand Down
8 changes: 4 additions & 4 deletions application/common/models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -580,21 +580,21 @@ public function setPassword($newPassword)
}

/**
* @param string $clientId
* @param string $authType
* @return string
* @throws \Exception
*/
public function createAccessToken($clientId, $authType)
public function createAccessToken($authType)
{
/*
* Create access_token and update user
*/
$accessToken = Utils::generateRandomString(32);
/*
* Store combination of clientId and accessToken for bearer auth
* Store accessToken for auth
*/
$this->auth_type = $authType;
$this->access_token = Utils::getAccessTokenHash($clientId . $accessToken);
$this->access_token = Utils::getAccessTokenHash($accessToken);
$this->access_token_expiration = Utils::getDatetime(
time() + \Yii::$app->params['accessTokenLifetime']
);
Expand Down
4 changes: 2 additions & 2 deletions application/frontend/components/BaseRestController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

namespace frontend\components;

use common\components\auth\HttpOnlyAuth;
use yii\filters\AccessControl;
use yii\filters\auth\CompositeAuth;
use yii\filters\auth\HttpBearerAuth;
use yii\helpers\ArrayHelper;
use yii\rest\Controller;
use yii\web\ForbiddenHttpException;
Expand All @@ -22,7 +22,7 @@ public function behaviors()
'authenticator' => [
'class' => CompositeAuth::class,
'authMethods' => [
HttpBearerAuth::class, // Use header ... Authorization: Bearer abc123
HttpOnlyAuth::class, // custom authentication
],
'except' => ['options'],
],
Expand Down
48 changes: 16 additions & 32 deletions application/frontend/controllers/AuthController.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
use Sil\Idp\IdBroker\Client\ServiceException;
use yii\filters\AccessControl;
use yii\helpers\ArrayHelper;
use yii\helpers\Html;
use yii\web\BadRequestHttpException;
use yii\web\ServerErrorHttpException;

Expand Down Expand Up @@ -57,20 +56,6 @@ public function actionLogin()
$log = ['action' => 'login'];

try {
/*
* Grab client_id for use in token after successful login
*/
try {
$clientId = Utils::getClientIdOrFail();
} catch (\Exception $e) {
\Yii::warning(\Yii::t('app', 'Auth.MissingClientID'));

// This condition happens if a user sits on the IDP login prompt long
// enough for the session to expire. As a workaround, redirect back to
// the profile UI home page, which should restart the login process.
return $this->redirect(\Yii::$app->params['uiUrl']);
}

/*
* Grab state for use in response after successful login
*/
Expand All @@ -90,8 +75,17 @@ public function actionLogin()
}
}

$accessToken = $user->createAccessToken($clientId, User::AUTH_TYPE_LOGIN);

$accessToken = $user->createAccessToken(User::AUTH_TYPE_LOGIN);
$secure = YII_ENV != 'dev' || YII_ENV != 'test' ? true : false;

\Yii::$app->response->cookies->add(new \yii\web\Cookie([
'name' => 'access_token',
'value' => $accessToken,
'expire' => $user->access_token_expiration,
'httpOnly' => true, // Ensures the cookie is not accessible via JavaScript
'secure' => $secure, // Ensures the cookie is sent only over HTTPS
'sameSite' => 'Lax', // Adjust as needed
]));
$loginSuccessUrl = $this->getLoginSuccessRedirectUrl($state, $accessToken, $user->access_token_expiration);

$log['email'] = $user->email;
Expand Down Expand Up @@ -131,12 +125,14 @@ public function actionLogin()

public function actionLogout()
{
$accessToken = \Yii::$app->request->get('access_token');
$cookies = \Yii::$app->response->cookies;
$accessToken = $cookies->getValue('access_token');
if ($accessToken !== null) {
/*
* Clear access_token
*/
$accessTokenHash = Utils::getAccessTokenHash($accessToken);
$cookies->remove('access_token');
$user = User::findOne(['access_token' => $accessTokenHash]);
if ($user != null) {
$user->destroyAccessToken();
Expand Down Expand Up @@ -209,20 +205,8 @@ public function getLoginSuccessRedirectUrl($state, $accessToken, $tokenExpiratio
* build url to redirect user to
*/
$afterLogin = $this->getAfterLoginUrl($relayState);
if (strpos($afterLogin, '?')) {
$joinChar = '&';
} else {
$joinChar = '?';
}
$url = $afterLogin . sprintf(
'%sstate=%s&token_type=Bearer&expires_utc=%s&access_token=%s',
$joinChar,
Html::encode($state),
Utils::getIso8601($tokenExpiration),
$accessToken
);

return $url;

return $afterLogin;
}

/**
Expand Down
23 changes: 12 additions & 11 deletions application/frontend/controllers/ResetController.php
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ public function actionResend($uid)
/**
* Validate reset code. Logs user in if successful
* @param string $uid
* @return array
* @return null
* @throws BadRequestHttpException
* @throws NotFoundHttpException
* @throws ServerErrorHttpException
Expand All @@ -254,12 +254,6 @@ public function actionValidate($uid)
throw new NotFoundHttpException();
}

try {
$clientId = Utils::getClientIdOrFail();
} catch (\Exception $e) {
throw new BadRequestHttpException(\Yii::t('app', 'Reset.MissingClientID'), 1483979025);
}

$log = [
'action' => 'Validate reset',
'reset_id' => $reset->id,
Expand Down Expand Up @@ -292,7 +286,16 @@ public function actionValidate($uid)
* Reset verified successfully, create access token for user
*/
try {
$accessToken = $reset->user->createAccessToken($clientId, User::AUTH_TYPE_RESET);
$accessToken = $reset->user->createAccessToken(User::AUTH_TYPE_RESET);
$secure = (YII_ENV != 'dev' || YII_ENV != 'test') ? true : false;
\Yii::$app->response->cookies->add(new \yii\web\Cookie([
'name' => 'access_token',
'value' => $accessToken,
'expire' => $reset->user->access_token_expiration,
'httpOnly' => true, // Ensures the cookie is not accessible via JavaScript
'secure' => $secure, // Ensures the cookie is sent only over HTTPS
'sameSite' => 'Lax', // Adjust as needed
]));

$log['status'] = 'success';
\Yii::warning($log);
Expand All @@ -308,10 +311,8 @@ public function actionValidate($uid)
'error' => Json::encode($reset->getFirstErrors()),
]);
}
return null;

return [
'access_token' => $accessToken,
];
} catch (\Exception $e) {
$log['status'] = 'error';
$log['error'] = 'Unable to log user in after successful reset verification';
Expand Down
4 changes: 3 additions & 1 deletion application/tests/api.suite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ modules:
enabled:
- ApiHelper
- tests\api\FixtureHelper
- PhpBrowser:
url: http://localhost/index-test.php
- REST:
url: http://localhost/index-test.php
depends: PhpBrowser
coverage:
enabled: true
include:
- common/*
- frontend/*
- frontend/*
Loading

0 comments on commit 12e7b66

Please sign in to comment.