API Authentication using HMAC - authentication

I am looking for a decent method of authentication to use when writing a simple API for use within our internal systems. Other questions on Stack Overflow have suggested HMAC along with links to tutorials, which I went ahead and decided to implement.
After setting this up, I realized I am not exactly sure how significant this is for actual authentication. The tutorial used Here lists a public hash on the client which is never used in the server side code. It just hashes the content and privateHash values together and compares them on the server. As this is all being passed through the headers, I am wondering how secure this actually is? What is the publicHash value for as it does not even seem to be used?
Client:
<?php
$publicHash = '3441df0babc2a2dda551d7cd39fb235bc4e09cd1e4556bf261bb49188f548348';
$privateHash = 'e249c439ed7697df2a4b045d97d4b9b7e1854c3ff8dd668c779013653913572e';
$content = json_encode( array( 'test' => 'content' ) );
$hash = hash_hmac('sha256', $content, $privateHash);
$headers = array(
'X-Public: '.$publicHash,
'X-Hash: '.$hash
);
$ch = curl_init('http://domain.com/api2/core/device/auth');
curl_setopt($ch,CURLOPT_HTTPHEADER,$headers);
curl_setopt($ch,CURLOPT_RETURNTRANSFER,true);
curl_setopt($ch,CURLOPT_POSTFIELDS,$content);
$result = curl_exec($ch);
curl_close($ch);
echo "RESULT\n======\n".print_r($result, true)."\n\n";
?>
Server
function auth()
{
$app = \Slim\Slim::getInstance();
$request = $app->request();
$publicHash = $request->headers('X-Public');
$contentHash = $request->headers('X-Hash');
$privateHash = 'e249c439ed7697df2a4b045d97d4b9b7e1854c3ff8dd668c779013653913572e';
$content = $request->getBody();
$hash = hash_hmac('sha256', $content, $privateHash);
if ($hash == $combinedHash)
{
$data = array('status' => "success");
response($data);
}
else
{
$data = array('status' => "failed");
response($data);
}
}

I think the article you posted uses the term hash in confusing way. In the article $publicHash is an username and $privateHash is secret key used to sign the payload.
In other words, like the article says the value of X-Public header is supposed to be used for querying the user specific secret key from database. This secret key is then used to to create an hash from the payload. This hash is then compared to value of X-Hash header.
If the values match then you can be sure the payload has not been tampered and/or the sender of the payload knows the secret key.
This approach is not stateless. It requires you to hit database for each request to find out the secret key for current user. It is also bit problematic if your client is untrusted (ie. JavaScript enabled browser). Works well for machine to machine communication though.
Alternative
You might want to check article called Using JSON Web Tokens as API Keys. JSON Web Tokens provide stateless solution. They are also tamper proof because tokens are signed / hashed with HMAC.

Related

Cannot get JWT (json web token) to work with Apple App Store Connect API in PHP

I get a 401/Not Authorized return for my JTW using PHP with Apple's App Store Connect API.
Using php 7.1.3, I've tried various libraries and raw php (code below). I'm pretty sure the header and payload are fine and the problem is the signing of it, using the p8 private key file I downloaded from Apple. I have quadruple checked the kid, iss, and private key file.
// Create token header as a JSON string
$header = json_encode([
'typ' => 'JWT',
'alg' => 'ES256',
'kid' => '1234567980'
]);
// Create token payload as a JSON string
$payload = json_encode([
'iss' => '12345678-1234-1234-1234-123456789012',
'exp' => time()+60*10, // 20 minute max allowed
'aud' => 'appstoreconnect-v1'
]);
$base64UrlHeader = rtrim(strtr(base64_encode($header), '+/', '-_'), '=');
$base64UrlPayload = rtrim(strtr(base64_encode($payload), '+/', '-_'), '=');
$privateKey = openssl_pkey_get_private('file://'.resource_path('/assets/AppleKey_1234567980.p8'));
$signature = '';
openssl_sign("$base64UrlHeader.$base64UrlPayload", $signature, $privateKey, 'sha256');
// Encode Signature to Base64Url String
$base64UrlSignature = rtrim(strtr(base64_encode($signature), '+/', '-_'), '=');
// Create JWT
$jwt = "$base64UrlHeader.$base64UrlPayload.$base64UrlSignature";
When I submit this to Apple via curl, I get a 401/Not Authorized with details "Authentication credentials are missing or invalid" and nothing more specific.
Has anyone used Apple's App Store Connect API with PHP - google searches have slim to no results I can find.
You can check this solution. i manage to get authenticated and get data from appleconnect. https://stackoverflow.com/a/57150060/860353

Actions on Google / DialogFlow: get user data from idToken without conv object

I have a deployed Action that has Google Sign-in Account Linking enabled. This Action uses a cloud function as fullfilment. We extract the user from the DialogFlow call using this method:
function userFromRequest(request) {
return request.body.originalDetectIntentRequest.payload.user;
}
This function returns this user data:
{
"idToken": "eyJhbGciOiJSU...",
"lastSeen": "2018-11-29T16:58:22Z",
"locale": "en-US",
"userId": "ABwpp..."
}
My question is: how can I get the user information such as email, name, etc, from outside the DialogFlow app.
All the documentation examples have a conv object available:
app.intent('Default Welcome Intent', async (conv) => {
const {payload} = conv.user.profile;
const name = payload ? ` ${payload.given_name}` : '';
}
In our case, we want to simply take the userId or idToken and retrieve the user info. It could be something like this:
const dialogflow = require("actions-on-google");
const app = dialogflow({clientId: '94661...#apps.googleusercontent.com'});
app.getUserData(idToken); //this does not exists, how to have something equivalent?
The idToken is just a normal JWT (JSON Web Token) that has been signed by one of Google's keys (which rotate very frequently). Although you should verify the signature, you don't need to.
You can use any JWT library. Since it looks like you're using node.js, you can use something like the jsonwebtoken package to decode it with something like this:
const jwt = require('jsonwebtoken');
// get the decoded payload ignoring signature, no secretOrPrivateKey needed
const decoded = jwt.decode(token);
You really should verify the signature, however, so you'll need to get the keys in a format that is useful. The part of the multivocal library that does this uses the JWK version of the keys from Google and converts them into PEM format to verify.
you can use "google-auth-library" to verify the token and get the payload. here is the link to the documentation

mautic - I want to add contact in mautic via api

I want to add contact in mautic via an API. Below I have the code, but it's not adding the contact in mautic.
I have installed mautic in localhost. Studied the API form in the mautic documentation and tried to do it for at least 2 days, but I am not getting any results on it.
<?php
// Bootup the Composer autoloader
include __DIR__ . '/vendor/autoload.php';
use Mautic\Auth\ApiAuth;
session_start();
$publicKey = '';
$secretKey = '';
$callback = '';
// ApiAuth->newAuth() will accept an array of Auth settings
$settings = array(
'baseUrl' => 'http://localhost/mautic', // Base URL of the Mautic instance
'version' => 'OAuth2', // Version of the OAuth can be OAuth2 or OAuth1a. OAuth2 is the default value.
'clientKey' => '1_1w6nrty8k9og0kow48w8w4kww8wco0wcgswoow80ogkoo0gsks', // Client/Consumer key from Mautic
'clientSecret' => 'id6dow060fswcswgsgswgo4c88cw0kck4k4cc0wkg4gows08c', // Client/Consumer secret key from Mautic
'callback' => 'http://localhost/mtest/process.php' // Redirect URI/Callback URI for this script
);
/*
// If you already have the access token, et al, pass them in as well to prevent the need for reauthorization
$settings['accessToken'] = $accessToken;
$settings['accessTokenSecret'] = $accessTokenSecret; //for OAuth1.0a
$settings['accessTokenExpires'] = $accessTokenExpires; //UNIX timestamp
$settings['refreshToken'] = $refreshToken;
*/
// Initiate the auth object
$initAuth = new ApiAuth();
$auth = $initAuth->newAuth($settings);
/*
if( $auth->getAccessTokenData() != null ) {
$accessTokenData = $auth->getAccessTokenData();
$settings['accessToken'] = $accessTokenData['access_token'];
$settings['accessTokenSecret'] = 'id6dow060fswcswgsgswgo4c88cw0kck4k4cc0wkg4gows08c'; //for OAuth1.0a
$settings['accessTokenExpires'] = $accessTokenData['expires']; //UNIX timestamp
$settings['refreshToken'] = $accessTokenData['refresh_token'];
}*/
// Initiate process for obtaining an access token; this will redirect the user to the $authorizationUrl and/or
// set the access_tokens when the user is redirected back after granting authorization
// If the access token is expired, and a refresh token is set above, then a new access token will be requested
try {
if ($auth->validateAccessToken()) {
// Obtain the access token returned; call accessTokenUpdated() to catch if the token was updated via a
// refresh token
// $accessTokenData will have the following keys:
// For OAuth1.0a: access_token, access_token_secret, expires
// For OAuth2: access_token, expires, token_type, refresh_token
if ($auth->accessTokenUpdated()) {
$accessTokenData = $auth->getAccessTokenData();
echo "<pre>";
print_r($accessTokenData);
echo "</pre>";
//store access token data however you want
}
}
} catch (Exception $e) {
// Do Error handling
}
use Mautic\MauticApi;
//use Mautic\Auth\ApiAuth;
// ...
$initAuth = new ApiAuth();
$auth = $initAuth->newAuth($settings);
$apiUrl = "http://localhost/mautic/api";
$api = new MauticApi();
$contactApi = $api->newApi("contacts", $auth, $apiUrl);
$data = array(
'firstname' => 'Jim',
'lastname' => 'Contact',
'email' => 'jim#his-site.com',
'ipAddress' => $_SERVER['REMOTE_ADDR']
);
$contact = $contactApi->create($data);
echo "<br/>contact created";
Any help will be appreciated.
use Curl\Curl;
$curl = new Curl();
$un = 'mayank';
$pw = 'mayank';
$hash = base64_encode($un.':'.$pw);
$curl->setHeader('Authorization','Basic '.$hash);
$res = $curl->post(
'http://mautic.local/api/contacts/new',
[
'firstname'=>'fn',
'lastname'=>'ln',
'email'=>'t1#test.com'
]
);
var_dump($res);
This is something very simple i tried and it worked for me, please try cleaning cache and enable logging, unless you provide us some error it's hard to point you in right direction. Please check for logs in app/logs directory as well as in /var/logs/apache2 directory.
In my experience sometimes after activating the API in the settings the API only starts working after clearing the cache.
Make sure you have activated the API in the settings
Clear the cache:
cd /path/to/mautic
rm -rf app/cache/*
Then try again
If this didn't work, try to use the BasicAuth example (You have to enable this I the settings again and add a new User to set the credentials)
I suspect that the OAuth flow might be disturbed by the local settings / SSL configuration.
these steps may be useful:
make sure API is enabled(yes I know it's might be obvious but still);
check the logs;
check the response body;
try to send it as simple json via Postman
it may be one of the following problems:
Cache;
You are not sending the key:value; of the required custom field;
you are mistaken with authentication;
Good luck :)

Google login in PHP backend and JS frontend

Front end is 100% JS. User click on sign in button and an authResult['code'] is received and send via ajax to localhost/api/user/login which has the following content:
$code = $data['code'];
require_once 'Google/Client.php';
$client = new Google_Client();
$client->setClientId('xxxxxx');
$client->setClientSecret('xxxxx');
$client->setRedirectUri('http://localhost:8080');
$client->setScopes('email'); //Why do I need this? I already set scope in JS.
$client->authenticate($code); //It fails here. with no error. just 400 bad request.
$token = json_decode($client->getAccessToken());
$reqUrl = 'https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=' .
$token->access_token;
$req = new Google_HttpRequest($reqUrl);
$tokenInfo = json_decode(
$client::getIo()->authenticatedRequest($req)->getResponseBody());
//Check errors.
//Save user personal info in database
//Set login sessions
Why do I need to set scopes if I already set them in javascript?
Why is it failing when authenticate function is called? Im getting no erros.
Why do I need a setRedirectUri() when it is on the backend?
You don't need to set scopes in this case.
(see answer 3, but also): Check your client ID matches the one used in the Javascript, and that the client secret is exactly as in the console (no trailing/leading spaces).
Changing your redirecturi to 'postmessage' - this is the string used when the code was generated via the Javascript process.
You can also try manually constructing the URL and calling it with curl to make sure everything is as you expect: https://developers.google.com/accounts/docs/OAuth2WebServer#handlingtheresponse

Symfony REST API authentication without sfGuardPlugin

I'm trying to find information on securing a HTTP REST API in a Symfony project, but all I can find is information about using sfGuardPlugin. From what I can see, this plugin isn't very useful for web services. It tries to have user profile models (which aren't always that simple) and have "sign in" and "sign out" pages, which obviously are pointless for a stateless REST API. It does a lot more than I'll ever have need for and I what to keep it simple.
I want to know where to implement my own authorisation method (loosely based on Amazon S3's approach). I know how I want the authorisation method to actually work, I just don't know where I can put code in my Symfony app so that it runs before every request is processed, and lets approved requests continue but unsuccessful requests return a 403.
Any ideas? I can't imagine this is hard, I just don't know where to start looking.
There is a plugin for RESTful authentication -> http://www.symfony-project.org/plugins/sfRestfulAuthenticationPlugin
Not used it though ....
How where you planning to authenticate users ?
The jobeet tutorial uses tokens ... http://www.symfony-project.org/jobeet/1_4/Doctrine/en/15
I ended up finding what I was looking for by digging into the code for sfHttpAuthPlugin. What I was looking for was a "Filter". Some details and an example is described in the Askeet sample project.
Stick a HTTP basicAuth script in your <appname>_dev.php (Symfony 1.4 =<) between the project configuration "require" and the configuration instance creation.
Test it on your dev. If it works, put the code in your index.php (the live equivalent of <appname>_dev.php) and push it live.
Quick and dirty but it works. You may want to protect that username/password in the script though.
e.g.
$realm = 'Restricted area';
//user => password
$users = array('username' => 'password');
if (empty($_SERVER['PHP_AUTH_DIGEST'])) {
header('HTTP/1.1 401 Unauthorized');
header('WWW-Authenticate: Digest realm="'.$realm.
'",qop="auth",nonce="'.uniqid().'",opaque="'.md5($realm).'"');
die('Text to send if user hits Cancel button');
}
// || !isset($users[$data['username']]
// analyze the PHP_AUTH_DIGEST variable
if (!($data = http_digest_parse($_SERVER['PHP_AUTH_DIGEST'])) || !isset($users[$data['username']])) {
header('HTTP/1.1 401 Unauthorized');
header('WWW-Authenticate: Digest realm="'.$realm.
'",qop="auth",nonce="'.uniqid().'",opaque="'.md5($realm).'"');
die('Wrong Credentials!');
}
// generate the valid response
$A1 = md5($data['username'] . ':' . $realm . ':' . $users[$data['username']]);
$A2 = md5($_SERVER['REQUEST_METHOD'].':'.$data['uri']);
$valid_response = md5($A1.':'.$data['nonce'].':'.$data['nc'].':'.$data['cnonce'].':'.$data['qop'].':'.$A2);
if ($data['response'] != $valid_response) {
header('HTTP/1.1 401 Unauthorized');
header('WWW-Authenticate: Digest realm="'.$realm.
'",qop="auth",nonce="'.uniqid().'",opaque="'.md5($realm).'"');
die('Wrong Credentials!');
}
// function to parse the http auth header
function http_digest_parse($txt)
{
// protect against missing data
$needed_parts = array('nonce'=>1, 'nc'=>1, 'cnonce'=>1, 'qop'=>1, 'username'=>1, 'uri'=>1, 'response'=>1);
$data = array();
$keys = implode('|', array_keys($needed_parts));
preg_match_all('#(' . $keys . ')=(?:([\'"])([^\2]+?)\2|([^\s,]+))#', $txt, $matches, PREG_SET_ORDER);
foreach ($matches as $m) {
$data[$m[1]] = $m[3] ? $m[3] : $m[4];
unset($needed_parts[$m[1]]);
}
return $needed_parts ? false : $data;
}
// ****************************************************************************
// ok, valid username & password.. continue...