i'm trying to validate an access token isued by aws cognito on backend but i'm stucked at this, i have this:
<?php
namespace App\Bridge;
class AwsCognitoClient
{
private $client;
public function __construct(
string $key,
string $secret,
string $poolId,
string $clientId,
string $clientSecret,
string $region,
string $version
) {
$this->region = $region;
$this->poolId = $poolId;
$config = [
'credentials' => [
'key' => $key,
'secret' => $secret,
],
'region' => $region,
'version' => $version,
'app_client_id' => $clientId,
'app_client_secret' => $clientSecret,
'user_pool_id' => $poolId,
];
$aws = new \Aws\Sdk($config);
$this->client = $aws->createCognitoIdentityProvider();
}
public function verifyToken($access_token)
{
try {
$user = $this->client->getUser([
'AccessToken' => $access_token
]);
error_log("USER NULL ALWAYS: " . json_encode($user));
} catch (\Exception $e) {
error_log("ERROR COGNITO: " . $e->getMessage());
}
}
}
but $this->client->getUser always be empty and if token is expired throw correct exception for expired, but if token is valid not show nothing only $user be empty;
what is wrong?
Related
I really appreciate it if someone can help me here. Is there a way I can call the endpoint that generates the api jwt token within my container, anytime the last one expires? below is auth part of my container
App::class => function (ContainerInterface $container) {
AppFactory::setContainer($container);
$app = AppFactory::create();
$app->add(new Tuupola\Middleware\JwtAuthentication([
"secret" => $_ENV['JWT_SECRET'],
"ignore" => ["/api/token","/users"], //s
"error" => function ($response, $arguments) {
$data["status"] = "error";
$data["message"] = $arguments["message"];
//$app->post('/api/token', \App\Action\ApiAuthAction::class)->setName('user-api');
return $response
->withHeader("Content-Type", "application/json")
->getBody()->write((string)json_encode($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
}
]));
return $app;
},
This is my auth file
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, array $args = []): ResponseInterface
{
$userData = $this->userReader->findUserByEmail($request->getParsedBody());
if ($userData) {
$now = new DateTime();
$future = new DateTime($_ENV['JWT_EXPAIRED'] . " minutes");
$jti = (new Base62)->encode(random_bytes(16));
$payload = [
"iat" => $now->getTimeStamp(),
"exp" => $future->getTimeStamp(),
"jti" => $jti,
"sub" => $userData->email
];
$secret = $_ENV['JWT_SECRET'];
$token = JWT::encode($payload, $secret, "HS256");
$data["token"] = $token;
$data["expires"] = $future->getTimeStamp();
$response->getBody()->write((string)json_encode([
'success' => true,
'message' => $token
]));
} else {
$response->getBody()->write((string)json_encode([
'success' => false,
'message' => 'Invalid Email or Password'
]));
}
return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
}
I have created a custom authentication class which get's called from middleware, this will forward the user to AWS Cognito Hosted UI, the user will login then get returned to the application with a authorization code. This code is then exchanged for the access_token, id_token and refresh_token.
Once the access_token and id_token has been validated using the jwks keys I can extract the users email address and then return this within the User model which extends Authenticatable. Then i'd like to use $user within my Twig templates the same way i can with the default login system for Laravel.
Below is my code:
// config/auth.php
'guards' => [
'web' => [
'driver' => 'cognito',
'provider' => 'users',
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User\User::class,
],
// app/Providers/AuthServiceProvider.php
public function boot()
{
$this->registerPolicies();
Auth::extend('cognito', static function ($app, $name, array $config) {
return new CognitoUserProvider(new User());
});
}
// app/Auth/CognitoUserProvider.php
<?php
namespace App\Auth;
use App\Models\User;
use Firebase\JWT\JWT;
use GuzzleHttp\Client;
use CoderCat\JWKToPEM\JWKConverter;
class CognitoUserProvider
{
private $user;
private $clientId;
private $clientSecret;
private $poolId;
private $region;
private $domain;
private $guzzle;
public function __construct(User $user)
{
$this->clientSecret = config('app.aws_cognito_client_secret');
$this->poolId = config('app.aws_cognito_pool_id');
$this->clientId = config('app.aws_cognito_client_id');
$this->region = config('app.aws_region');
$this->domain = implode('.', [
config('app.aws_cognito_domain'),
'auth',
config('app.aws_region'),
'amazoncognito.com'
]);
$this->guzzle = new Client();
$this->user = $user;
}
/**
* #return User|bool|void
*/
public function authenticate()
{
if (request()->input('code')) {
$tokens = $this->exchangeTokens();
$accessToken = $this->validateToken($tokens['access_token']);
$userToken = $this->validateToken($tokens['id_token']);
if (!$accessToken || !$userToken) {
dd('JsonException Exception Occurred.');
}
request()->session()->put('access_token', $accessToken);
request()->session()->put('id_token', $userToken);
request()->session()->put('refresh_token', $tokens['refresh_token']);
$this->user->setAttribute('email', $userToken->email);
$this->user->setAttribute('first_name', 'Martyn');
$this->user->setAttribute('last_name', 'Ball');
$this->user->setAttribute('id', 5);
return $this->user;
}
if (request()->session()->has('refresh_token')) {
return $this->refreshToken();
}
$this->sendToLoginPage();
}
/**
* #param $token
*
* #return false|object
*/
private function validateToken($token)
{
$domain = implode('.', [ 'cognito-idp', $this->region, 'amazonaws.com' ]);
$jwks = file_get_contents("https://$domain/{$this->poolId}/.well-known/jwks.json");
try {
$jwks = collect(json_decode($jwks, true, 512, JSON_THROW_ON_ERROR)['keys']);
$jwks = $jwks->keyBy('kid')->map(static function ($current) {
return (new JWKConverter())->toPEM($current);
})->toArray();
$jwt = new JWT();
$jwt::$leeway = config('app.jwt_leeway');
return $jwt::decode($token, $jwks, ['RS256']);
} catch (\JsonException $e) {
return false;
}
}
/**
* #return false|mixed
*/
private function exchangeTokens()
{
$params = [
'grant_type' => 'authorization_code',
'client_id' => $this->clientId,
'code' => request()->input('code'),
'redirect_uri' => 'https://localhost/',
];
$result = $this->guzzle->post("https://{$this->domain}/oauth2/token?".http_build_query($params), [
'headers' => [
'Content-Type' => 'application/x-www-form-urlencoded',
'Authorization' => 'Basic '.base64_encode("{$this->clientId}:{$this->clientSecret}")
]
]);
if ($result->getStatusCode() === 200) {
try {
return json_decode($result->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
return false;
}
}
return false;
}
/**
* #return void
*/
private function sendToLoginPage(): void
{
$query = http_build_query([
'client_id' => $this->clientId,
'response_type' => 'code',
'redirect_uri' => 'https://localhost/'
]);
header("Location: https://{$this->domain}/login?$query&scope=email+openid");
exit;
}
/**
* #return void
*/
public function logout(): void
{
dd(debug_backtrace());
}
}
I am building registration form with Passport in Laravel using repository pattern, and when I try to register with postman I get error
Undefined property: Illuminate\\Http\\Response::$id
It breaks when I try and return response like this
return response(['user' => $user, 'access_token' => $accessToken]);
but when I just return $user instead then it works fine, but I don't have access token then. How can I register succesfully with access token? Any help is appreciated. Here is my code.
AuthController.php
public function register(RegisterUserRequest $request)
{
try {
$user = $this->service->createNewUser($request);
return $this->returnResource($user);
} catch (\Exception $exception) {
return $this->failureJson($exception);
}
}
AuthService.php
public function createNewUser(BaseRequest $request)
{
DB::beginTransaction();
try {
$request['confirmation_hash'] = Str::random(50);
$request['password'] = Hash::make($request['password']);
$user = parent::create($request);
Mail::to($user['email'])->send(new ConfirmationMail($user)); // Send email to user to confirm his email!
$accessToken = $user->createToken('authToken')->accessToken;
DB::commit();
return response(['user' => $user, 'access_token' => $accessToken]); // HERE IT BREAKS!!!
} catch (\Exception $exception) {
DB::rollBack();
throw new \Exception($exception->getMessage());
}
}
UserResource.php
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'first_name' => $this->first_name,
'last_name' => $this->last_name,
'email' => $this->email,
'password' => $this->password,
'remember_token' => $this->remember_token,
'api_token' => $this->api_token,
'confirmation_hash' => $this->confirmation_hash,
'email_verified_at' => $this->email_verified_at,
'is_verified' => $this->is_verified,
'verification_expires_at' => $this->verification_expires_at
];
}
Inside your createNewUser try to just return user return $user and then inside your UserResource.php create the token ' api_token' => $this->createToken('authToken')->accessToken
I am developing an API using Laravel Passport for authentication and my problem is that I cannot change the default message when the login fail due to invalid credentials.
LoginController.php
public function login(Request $request) {
$this->validate($request, [
'username' => 'required',
'password' => 'required'
]);
return $this->issueToken($request, 'password');
}
IssueTokenTrait.php
public function issueToken(Request $request, $grantType, $scope = "") {
$params = [
'grant_type' => $grantType,
'client_id' => $this->client->id,
'client_secret' => $this->client->secret,
'scope' => $scope
];
if($grantType !== 'social'){
$params['username'] = $request->username ?: $request->email;
}
$request->request->add($params);
$proxy = Request::create('oauth/token', 'POST');
return Route::dispatch($proxy);
}
When I put invalid credentials, it returns:
{
"error": "invalid_credentials",
"error_description": "The user credentials were incorrect.",
"message": "The user credentials were incorrect."
}
I want to change this message because I want the message to depend on the language.
Not sure but i try my best to answer you.
use League\OAuth2\Server\Exception\OAuthServerException;
public function issueToken(Request $request, $grantType, $scope = "",ServerRequestInterface $service_request) {
$params = [
'grant_type' => $grantType,
'client_id' => $this->client->id,
'client_secret' => $this->client->secret,
'scope' => $scope
];
if($grantType !== 'social'){
$params['username'] = $request->username ?: $request->email;
}
$request->request->add($params);
$proxy = Request::create('oauth/token', 'POST');
throw OAuthServerException::invalidRequest('access_token', object_get($error,
'error.message'));
return Route::dispatch($proxy);
}
Change App\Exceptions\Handler.php under:
public function render($request, Exception $exception)
{
...
$class = get_class($exception);
...
if ($class == 'League\OAuth2\Server\Exception\OAuthServerException' ){
return response()->json([
'code'=>$exception->getHttpStatusCode(),
'error'=>$exception->getMessage(),
'error_type'=>$exception->getErrorType()
],
$exception->getHttpStatusCode());
}
...
return parent::render($request, $exception);
}
In the TokenRepository you can see 3 similar methods. It create new entry to the tokens table but each method has different fields.
How can I refactor this? Should I merge 3 methods into 1 method or should I use strategy pattern?
TokenRepository Class:
class TokenRepository
{
public function createTokenDigitalOcean(User $user, $name, $accessToken, $refreshToken = null)
{
return $user->tokens()->create([
'name' => $name,
'provider' => 'digital_ocean',
'access_token' => $accessToken,
'refresh_token' => $refreshToken,
]);
}
public function createTokenLinode(User $user, $name, $key)
{
return $user->tokens()->create([
'name' => $name,
'provider' => 'linode',
'linode_key' => $key,
]);
}
public function createTokenAws(User $user, $name, $key, $secret)
{
return $user->tokens()->create([
'name' => $name,
'provider' => 'aws',
'aws_key' => $key,
'aws_secret' => $secret,
]);
}
}
I have 3 classes like DigitalOceanProvider, LinodeProvider and AwsProvider. For example of using LinodeProvider and AwsProvider class.
class LinodeProvider
{
public function callback()
{
$this->tokenRepo->createTokenLinode($user, $name, $key);
}
}
class AwsProvider
{
public function callback()
{
$this->tokenRepo->createTokenAws($user, $name, $key, $secret);
}
}
This may be a bit overkill, but in order to make life a bit easier in the future, you could create separate implementations of each that extend an abstract class. This way you can unify and define the interface and easily add new token types.
<?php namespace Foo\Tokens;
abstract class Token
{
protected $name = '';
protected $key = '';
protected $provider = '';
public function __construct($name, $key)
{
$this->name = $name;
$this->key = $key;
}
public function data()
{
return [
'name' => $this->name,
'provider' => $this->provider,
'token' => $this->key
];
}
}
Next, we create our Digital Ocean token class. This class can either use the default implementation or redefine it.
<?php namespace Foo\Tokens;
use Foo\Tokens\Token;
class DigitalOceanToken extends Token
{
protected $provider = 'digital_ocean';
public function __construct($name, $key, $refreshToken = null)
{
parent::__construct($name, $key);
$this->refreshToken = $refreshToken;
}
public function data()
{
return [
'name' => $this->name,
'provider' => $this->provider,
'key' => $this->key,
'refreshToken' => $this->refreshToken
];
}
}
The TokenRepository now merely cares about attaching a given token to a user.
<?php namespace Foo;
use User;
use Foo\Tokens\Token;
class TokenRepository
{
public function createToken(User $user, Token $token)
{
return $user->tokens()->create(
$token->data()
);
}
}
And your service providers are as simple as...
<?php
use Foo\Tokens\AwsToken;
class AwsProvider
{
public function callback()
{
$this->tokenRepo->createToken(
$user, new AwsToken($name, $key, $secret)
);
}
}
This isn't working code, as I've not attempted to run it however it's just another idea of how you can organize and assign responsibility. Hope it helps, and welcome feedback from others.
According to me you should implement it like this:
class TokenRepository
{
public function createTokenForVendor(User $user, $inputs)
{
return $user->tokens()->create($inputs);
}
}
and inside your callback:
class VendorProvider
{
public function callback()
{
switch($tokenType) {
case 'DigitalOcean':
$inputs = [
'name' => $name,
'provider' => 'digital_ocean',
'access_token' => $accessToken,
'refresh_token' => $refreshToken,
];
break;
case 'Linode':
$inputs = [
'name' => $name,
'provider' => 'linode',
'linode_key' => $key,
];
break;
case 'Aws':
$inputs = [
'name' => $name,
'provider' => 'aws',
'aws_key' => $key,
'aws_secret' => $secret,
];
break;
}
$this->tokenRepo->createTokenForVendor($user, $inputs);
}
}
Hoping you should do some code structure revamp.
Hope this helps!