Browser uploads to s3 with instance roles - php

This is a follow-up on my previous question regarding policy document signing using instance profiles.
I'm developing a system that allows drag & drop uploads directly to an S3 bucket; an AJAX request is first made to my server containing the file metadata. Once verified, my server responds with the form parameters that are used to complete the upload.
The process of setting up browser based uploads is well explained here and it all works as expected in my local test environment.
However, once my application gets deployed on an EC2 instance, I'm seeing this error when the browser attempts to upload the file:
<Error>
<Code>InvalidAccessKeyId</Code>
<Message>The AWS Access Key Id you provided does not exist in our records.</Message>
<RequestId>...</RequestId>
<HostId>...</HostId>
<AWSAccessKeyId>ASIAxxxyyyzzz</AWSAccessKeyId>
</Error>
The value of ASIAxxxyyyzzz here comes from the instance role credentials, as obtained from the metadata service; it seems that those credentials can't be used outside of EC2 to facilitate browser based uploads.
I've looked at the Security Token Service as well to generate another set of temporary credentials by doing this:
$token = $sts->assumeRole(array(
'RoleArn' => 'arn:aws:iam::xyz:role/mydomain.com',
'RoleSessionName' => 'uploader',
));
$credentials = new Credentials($token['Credentials']['AccessKeyId'], $token['Credentials']['SecretAccessKey']);
The call givens me a new set of credentials, but it give the same error as above when I use it.
I hope that someone has done this before and can tell me what stupid thing I've missed out :)

The AWS docs are very confusing on this, but I suspect that you need to include the x-amz-security-token parameter in the S3 upload POST request and that its value matches the SessionToken you get from STS ($token['Credentials']['SessionToken']).
STS temporary credentials are only valid when you include the corresponding security token.
The AWS documentation for the POST request states that:
Each request that uses Amazon DevPay requires two x-amz-security-token
form fields: one for the product token and one for the user token.
but that parameter is also used outside of DevPay, to pass the STS token and you would only need to pass it once in the form fields.

As pointed out by dcro's answer, the session token needs to be passed to the service you're using when you use temporary credentials. The official documentation mentions the x-amz-security-token field, but seems to suggest it's only used for DevPay; this is probably because DevPay uses the same type of temporary credentials and therefore requires the session security token.
2013-10-16: Amazon has updated their documentation to make this more obvious.
As it turned out, it's not even required to use STS at all; the credentials received by the metadata service come with such a session token as well. This token is automatically passed for you when the SDK is used together with temporary credentials, but in this case the final request is made by the browser and thus needs to be passed explicitly.
The below is my working code:
$credentials = Credentials::factory();
$signer = new S3Signature();
$policy = new AwsUploadPolicy(new DateTime('+1 hour', new DateTimeZone('UTC')));
$policy->setBucket('upload.mydomain.com');
$policy->setACL($policy::ACL_PUBLIC_READ);
$policy->setKey('uploads/test.jpg');
$policy->setContentType('image/jpeg');
$policy->setContentLength(5034);
$fields = array(
'AWSAccessKeyId' => $credentials->getAccessKeyId(),
'key' => $path,
'Content-Type' => $type,
'acl' => $policy::ACL_PUBLIC_READ,
'policy' => $policy,
);
if ($credentials->getSecurityToken()) {
// pass security token
$fields['x-amz-security-token'] = $credentials->getSecurityToken();
$policy->setSecurityToken($credentials->getSecurityToken());
}
$fields['signature'] = $signer->signString($policy, $credentials);
I'm using a helper class to build the policy, called AwsUploadPolicy; at the time of writing it's not complete, but it may help others with a similar problem.
Permissions were the last problem; my code sets the ACL to public-read and doing so requires the additional s3:PutObjectAcl permission.
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"s3:PutObject",
"s3:PutObjectAcl"
],
"Sid": "Stmt1379546195000",
"Resource": [
"arn:aws:s3:::upload.mydomain.com/uploads/*"
],
"Effect": "Allow"
}
]
}

Related

Content field is Empty when Accessing through AWS PHP SDK and AWS AppConfig

Everyone does anyone have an idea of how to use AWS AppConfig with the AWS SDK PHP.
My Particular use case is, I am running a simple PHP app on the EC2 instance and want to receive the JSON configurations written in the AppConfig.
use Aws\AppConfig\Exception\AppConfigException;
use Aws\AppConfig\AppConfigClient;
$appConfigClient = new AppConfigClient(['version' => 'latest', 'region' => 'ap-south-1']);
$clientid = uniqid('', true);
$params = [
'Application' => $APP_CONFIG_APP,
'ClientId' => $clientid,
'Configuration' => $APP_CONFIG_CONFIGURATION_PROFILE,
'Environment' => $APP_CONFIG_ENVIRONMENT
];
$response = $appConfigClient->getConfiguration($params);
$config = $response['Content'];
Also, I am authorizing with the AppConfig by an Administrator IAM Role Provided so, no issues on that side and I am able to get the following output
{ "Content": {}, "ConfigurationVersion": "1", "ContentType": "application\/octet-stream", "#metadata": { "statusCode": 200, client_id=60696", and some more fields...}
But the issue is I am not getting the AppConfig Content but able to extract the metadata regarding the Data.
So, Anyone who had tried this please help me out here.
I haven't used AppConfig yet but am reading up about it at the minute.
If you look at the information in the 'Important' box on the page linked below then it says that the content section is not returned if there is no change to the appconfig data.
So maybe the original request had data but then any subsequent request is not detecting a change and therefore not sending any content through
It is probably worth updating the data in AppConfig and then running a new request to see if the content is populated as required.
See:
https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-retrieving-the-configuration.html

How to implement authentication on a REST architecture with Parse

I am currently redoing a legacy web application that uses the PHP Parse SDK, and I am in the login authentication part. In the old application, we used $ _SESSION and ParseToken when doing ParseUser::signIn() and ParseUser::currentUser() to check if you have a session with a valid token, however the new application is being made using the REST architecture, where one of the REST concepts is that the server must not keep state, that is, be stateless, and in that case it would be the client that would have to send the necessary data.
When searching the internet and forums, I saw that it is common for developers to authenticate with JWT, where the client would make a request for a server's route and the server would return a token, and through that token authentication would take place.
I even implemented something using Firebase / jwt-php, where the client [Postman] makes a request for the route /login sending via body [username, password] and in case of success, returns the token to be used in secure route requests.
NOTE: Code is as simple as possible, without validation and cleaning just to show the example.
Action /login
$username = $request->getParsedBody()['username'];
$password = $request->getParsedBody()['password'];
$userAuthenticated = ParseUser::logIn($username, $password);
$payload = [
'data' => $userAuthenticated,
'exp' => time() + 3600
];
$token = JWT::encode($payload, $_ENV['JWT_SECRET_KEY']);
echo json_encode(['token' => $token]);
And the protected routes have a middleware that checks if the time has expired, and if this has happened, an exception with a 401 code is launched.
So far so good, authentication works, the problem I don't know if it's right to do it this way, since I need to give a ParseUser::logIn(), just to generate a session in the database and I don't even use it this session to do some authentication, with the exception of operations in the bank, because from what I saw in the documentation, if there is no valid session in the database, the application will return invalid session token error and also when making the request for another route ParseUser::currentUser() returns null, and this may be a problem in the future.
Does anyone have any idea how I can implement authentication for a REST application made in PHP? I appreciate the help !!
I believe the easiest way would be just replacing the default session storage (which uses $_SESSION) to something else that stores the session in, for example, Redis. Reference: https://docs.parseplatform.org/php/guide/#session-storage-interface
But the way you are doing should also work. You will only have to make sure that, every time that a request comes, you will decode the JWT, get the Parse Session token from there, and use ParseUser::become to set the current user: https://docs.parseplatform.org/php/guide/#setting-the-current-user

Spotify API php - unable to request data and get access token

I am currently new to using php and Laravel and working with an API however I have been following the Spotify PHP tutorial https://github.com/jwilsson/spotify-web-api-php.
I've also put in bold some of my questions that I wanted to ask , hopefully someone can help.
I have followed all steps but need help just to get it working.
Put the following code in its own file, lets call it auth.php. Replace CLIENT_ID and CLIENT_SECRET with the values given to you by Spotify.
(Where abouts should I save this file?)
The REDIRECT_URI is the one you entered when creating the Spotify app, make sure it's an exact match.
(I used my localhost:8888/callback/ not sure if that is correct?) Obviously I haven't put me details in here on this website as for security reasons.
<?php
require 'vendor/autoload.php';
$session = new SpotifyWebAPI\Session(
'CLIENT_ ID',
'CLIENT_SECRET',
'REDIRECT_URL'
);
$options = [
'scope' => [
'playlist-read-private',
'user-read-private',
],
];
header('Location: ' . $session->getAuthorizeUrl($options));
die();
?>
When the user has approved your app, Spotify will redirect the user together with a code to the specifed redirect URI. You'll need to use this code to request a access token from Spotify.
put this code in a new file called callback.php:
Do replace client id and secret with my detail? also how do I save the access token?
require 'vendor/autoload.php';
$session = new SpotifyWebAPI\Session(
'CLIENT_ID',
'CLIENT_SECRET',
'REDIRECT_URI'
);
// Request a access token using the code from Spotify
$session->requestAccessToken($_GET['code']);
$accessToken = $session->getAccessToken();
$refreshToken = $session->getRefreshToken();
// Store the access and refresh tokens somewhere. In a database for example.
// Send the user along and fetch some data!
header('Location: app.php');
die();
In a third file, app.php, tell the API wrapper which access token to use, and then make some API calls!
(Where do i also save this file and how do I make these calls in my Laravel Controllers?)
require 'vendor/autoload.php';
$api = new SpotifyWebAPI\SpotifyWebAPI();
// Fetch the saved access token from somewhere. A database for example.
$api->setAccessToken($accessToken);
// It's now possible to request data about the currently authenticated user
print_r(
$api->me()
);
// Getting Spotify catalog data is of course also possible
print_r(
$api->getTrack('7EjyzZcbLxW7PaaLua9Ksb')
);
(Where abouts should I save this file?)
You can save this file in differents places in laravel, for testing you could write it in a controller (not the best but you can).
Do replace client id and secret with my detail?
Yes of course !
also how do I save the access token?
You can save in a database or in a session or where you want. If you store it in a session you will have to make a new request to get a new Access token if the user logged out of your application. In a database you can reuse it.
Many access token are only available for a specific duration. The spotify doc should speak of it.
(Where do i also save this file and how do I make these calls in my Laravel Controllers?)
For testing you can do this in your controller, but it's a good idea to have a service layer where you put the business logic of your application.
Do not copy require 'vendor/autoload.php'; in your file laravel handle the composer autoload already.

How can I re-acquire a Shopify OAuth access token for a store that has previously installed my application?

I requested authorization for a public application to be able to access store data via the Shopify API.
The store successfully authorized my application via an authorization request URL such as
https://some-store.myshopify.com/admin/oauth/authorize?client_id=123abc&scope=read_inventory%2Cread_products&redirect_uri=http%3A%2F%mysite.com%2Fauth.php&state=123456
and the response was passed back to my application. This response (containing the code that can be exchanged for a permanent access token) was mishandled by my application (an error on the page meant that the access token was not stored).
Everything I read regarding requesting these tokens involves authorization by the store - but given the store has already authorized my application, passed back the code and that code has already successfully been exchanged for a token: is there a way my application can request that same token or a fresh one using my API keys given that the application is already authorized?
The only method I currently can find for requesting a token requires starting back at the beginning and fetching a code for exchange etc.
I working in PHP and using Luke Towers' php shopify wrapper
This stage was completed successfully:
function check_authorization_attempt()
{
$data = $_GET;
$api = new Shopify($data['shop'], [
'api_key' => '123',
'secret' => '456',
]);
$storedAttempt = null;
$attempts = json_decode(file_get_contents('authattempts.json'));
foreach ($attempts as $attempt) {
if ($attempt->shop === $data['shop']) {
$storedAttempt = $attempt;
break;
}
}
return $api->authorizeApplication($storedAttempt->nonce, $data);
}
$response = check_authorization_attempt();
and I would have been able to read the access token from :
$access_token = $response->access_token;
But this was the stage at which my application hit an error in accessing a database in which to write said token.
I cannot repeat it without repeating the auth request because the data in $_GET that's passed to this function comes from Shopify's response to the shop owner authorizing the access, and includes amoung other things the code for exchange.
You have to re-ask for authorization. It is no one's fault but yours that your persistence layer code was incorrect. So there is nothing you can do to change that. Ensure your code works. Since the client has no token in your App persistence layer, your App will retry the authorization token exchange. They do not have to delete your App first. So basically, the next time your client tries to use the App, YES they will asked to approve it, but who cares, they will, and you'll get a good auth token to store. You have fixed your code (right), so that will work. You are one step closer to glory.
Shopify does return the Permanent Access Token, but the ACCESS_MODE must be "Offline" for the token to be permanent.
With ACCESS_MODE offline, your app receives the permanent access token
to make requests whenever you want, without the user's permission.
Documentation:
https://shopify.dev/tutorials/authenticate-with-oauth#step-2-ask-for-permission
https://shopify.dev/concepts/about-apis/authentication#api-access-modes

OAuth2 Google Service Account invalid_grant

The keys I've posted here are from a container I've deleted, but were all valid keys that have been provided to me by Google.
I'm attempting to implement: https://developers.google.com/analytics/devguides/config/mgmt/v3/mgmtReference/management/uploads/uploadData
The uploadData function works fine, and everything is accepted in Google Analytics. My problem lies with Google's OAuth2: https://developers.google.com/analytics/devguides/config/mgmt/v3/mgmtAuthorization
From what I've gathered, for the end-point I wish to hit, I must use an OAuth2 token, and cannot use an api key. The request doesn't accept ?key={api}, and only Bearer Authorization. Using the Service Account request, all I receive is invalid_grant. I've updated my clocks and does various miss-matches of values to no avail. Mind you, with similar keys, all other workflows work, but I don't wish to have user interaction as this is a background task.
json for key data (provided by Google):
{
"private_key_id": "825119b6ab0eabf2029a4e1cf562fa88090736a0",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBANCQ+tGWdTUOL6py\nhk/KGK/ClNFQnzRrzPvOgeHCqENqeunN5LJYBlrf0OOmRzJjV67WZc3cHKu95kYr\nI+Sz0NlsmPYiwP2eMUKL5HX2JEXx/T8Bf7SWK78G7BnPKxA1fKISSftJ1IJ9neH5\nqhe4zEIB2NUcc6h3GHqBoQx4/4/dAgMBAAECgYEAsegpe2RrQEGEmVEtjpwmaK6D\nQPUTiKS36sdhdREVdMQ8anmtrg92BEhMqBNrQekJn2LU3j/22OyYo5wi9vAHohPI\nKYODw6mUemk/ULyuMGesC7nRq9sM7YnJk3KlkYrtLVR9THwAPfZ73k4UswsGFw4e\naCX6SwtNnQTHruCvCAECQQD8ZkxRf2LdP0LZYrqcB0TD2P1rYeX+IHW5sC6mdDjQ\nv6HWXjviEBfQH6kaxpUvRaSHTk1p2a5pHOjVu9DdkGXdAkEA04qc+nXH6xkBf4yE\nLODzUuAMo/QU1C+SC9AS1WbfAuRyRCkuD0SNTbK8Ec+pkqy/Q6VuvjLvvTosB9+O\nVhIyAQJASYY3RypXj2HFRHQZLiiD5JVKRUSwbdXg1WW4QS7r+gtIxpyOzyym8y61\n4SHmBW5BHlU2AdayktYkEVbz4gcVVQJBAI9JOZEwzEyDMI+btz/K0yYUmptHTgB3\nhF45/zfLKU2FPZzLo+Y1kdzKLzeFSKAQILGKUdvFFrw+tepTU88bHgECQAlp4/sy\nJ2m+zo5HsGBRP4gSxoVqiPuysT9tywJoUeo/3f+0jkDbVylTKTHpnqNk2ijFd1YS\n5ARPrKY4iXG7UoU\u003d\n-----END PRIVATE KEY-----\n",
"client_email": "42064665633-fbbnb79350js2h22e8k1s3h9t52rursu#developer.gserviceaccount.com",
"client_id": "42064665633-fbbnb79350js2h22e8k1s3h9t52rursu.apps.googleusercontent.com",
"type": "service_account"
}
The other 2 associated tags to create {Base64url encoded header}.{Base64url encoded claim set}.{Base64url encoded signature} are:
$header = [
'alg' => 'RS256',
'typ' => 'JWT'
];
$body = [
"iss" => "42064665633-fbbnb79350js2h22e8k1s3h9t52rursu.apps.googleusercontent.com",
"scope" => "https:\/\/www.googleapis.com\/auth\/analytics https:\/\/www.googleapis.com\/auth\/analytics.edit",
"aud" => "https:\/\/accounts.google.com\/o\/oauth2\/token",
"exp" => strtotime('1 hour'),
"iat" => strtotime('now')
];
$header = base64_encode(json_encode($header));
$body = base64_encode(json_encode($body));
The signature is defined as the private_key in the google docs link above. I've tried every excerpt of information from Java questions pertaining to this method of authentication, from escaping slashes (shown above), to omitting fields.
What have I done wrong/left out that causes {"error" : "invalid_grant"}?
I am familiar with oauth2 for objective c but here is my best shot at handling this:
invalid_grant trying to get oAuth token from google
Your internal clock may be out of sync with the google clock or your refresh token may be refreshed too much.
Make sure your client key and secret are up to date, if not revoke them and get new ones.
In PHP, make sure you are using the POST request.
Source: https://developers.google.com/analytics/devguides/reporting/core/v2/gdataAuthentication
While not a proper solution to getting JWT Service Accounts to work, I'm now storing a refresh token for the web route. Everything is working well, it's just a lot of information I need to keep in my config.yml file that I'd rather not.
https://developers.google.com/accounts/docs/OAuth2WebServer#refresh

Categories