Skip to content

Instantly share code, notes, and snippets.

@jenky
Last active October 7, 2024 13:17
Show Gist options
  • Save jenky/a4465f73adf90206b3e98c3d36a3be4f to your computer and use it in GitHub Desktop.
Save jenky/a4465f73adf90206b3e98c3d36a3be4f to your computer and use it in GitHub Desktop.
AWS Cognito Identity SRP authentication helper
<?php
use Aws\AwsClient;
use Aws\Result;
use Carbon\Carbon;
use phpseclib3\Math\BigInteger;
class AwsCognitoIdentitySRP
{
const N_HEX = 'FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1'.
'29024E088A67CC74020BBEA63B139B22514A08798E3404DD'.
'EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245'.
'E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED'.
'EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D'.
'C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F'.
'83655D23DCA3AD961C62F356208552BB9ED529077096966D'.
'670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B'.
'E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9'.
'DE2BCBF6955817183995497CEA956AE515D2261898FA0510'.
'15728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64'.
'ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7'.
'ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6B'.
'F12FFA06D98A0864D87602733EC86A64521F2B18177B200C'.
'BBE117577A615D6C770988C0BAD946E208E24FA074E5AB31'.
'43DB5BFCE0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF';
const G_HEX = '2';
const INFO_BITS = 'Caldera Derived Key';
/**
* @var \phpseclib3\Math\BigInteger
*/
protected $N;
/**
* @var \phpseclib3\Math\BigInteger
*/
protected $g;
/**
* @var \phpseclib3\Math\BigInteger
*/
protected $k;
/**
* @var \phpseclib3\Math\BigInteger
*/
protected $a;
/**
* @var \phpseclib3\Math\BigInteger
*/
protected $A;
/**
* @var string
*/
protected $clientId;
/**
* @var string
*/
protected $poolName;
/**
* @var \Aws\AwsClient
*/
protected $client;
/**
* Create new AWS CognitoIDP instance.
*
* @param \Aws\AwsClient $client
* @param string $clientId
* @param string $poolName
* @return void
*/
public function __construct(AwsClient $client, string $clientId, string $poolName)
{
$this->N = new BigInteger(static::N_HEX, 16);
$this->g = new BigInteger(static::G_HEX, 16);
$this->k = new BigInteger($this->hexHash('00'.static::N_HEX.'0'.static::G_HEX), 16);
$this->smallA();
$this->largeA();
$this->client = $client;
$this->clientId = $clientId;
$this->poolName = $poolName;
}
/**
* Get random a value.
*
* @return \phpseclib3\Math\BigInteger
*/
public function smallA(): BigInteger
{
if (is_null($this->a)) {
$this->a = $this->generateRandomSmallA();
}
return $this->a;
}
/**
* Get the client's public value A with the generated random number a.
*
* @return \phpseclib3\Math\BigInteger
*/
public function largeA(): BigInteger
{
if (is_null($this->A)) {
$this->A = $this->calculateA($this->smallA());
}
return $this->A;
}
/**
* Generate random bytes as hexadecimal string.
*
* @param int $bytes
* @return \phpseclib3\Math\BigInteger
*/
public function bytes(int $bytes = 32): BigInteger
{
$bytes = bin2hex(random_bytes($bytes));
return new BigInteger($bytes, 16);
}
/**
* Converts a BigInteger (or hex string) to hex format padded with zeroes for hashing.
*
* @param \phpseclib3\Math\BigInteger|string $longInt
* @return string
*/
public function padHex($longInt): string
{
$hashStr = $longInt instanceof BigInteger ? $longInt->toHex() : $longInt;
if (strlen($hashStr) % 2 === 1) {
$hashStr = '0'.$hashStr;
} elseif (strpos('89ABCDEFabcdef', $hashStr[0] ?? '') !== false) {
$hashStr = '00'.$hashStr;
}
return $hashStr;
}
/**
* Calculate a hash from a hex string.
*
* @param string $value
* @return string
*/
public function hexHash(string $value): string
{
return $this->hash(hex2bin($value));
}
/**
* Calculate a hash from string.
*
* @param string $value
* @return string
*/
public function hash($value): string
{
$hash = hash('sha256', $value);
return str_repeat('0', 64 - strlen($hash)).$hash;
}
/**
* Performs modulo between big integers.
*
* @param \phpseclib3\Math\BigInteger $a
* @param \phpseclib3\Math\BigInteger $b
* @return \phpseclib3\Math\BigInteger
*/
protected function mod(BigInteger $a, BigInteger $b): BigInteger
{
return $a->powMod(new BigInteger(1), $b);
}
/**
* Generate a random big integer.
*
* @return \phpseclib3\Math\BigInteger
*/
public function generateRandomSmallA(): BigInteger
{
return $this->mod($this->bytes(128), $this->N);
}
/**
* Calculate the client's public value A = g^a%N.
*
* @param \phpseclib3\Math\BigInteger $a
* @return \phpseclib3\Math\BigInteger
*
* @throws \InvalidArgumentException
*/
public function calculateA(BigInteger $a): BigInteger
{
$A = $this->g->powMod($a, $this->N);
if ($this->mod($a, $this->N)->equals(new BigInteger(0))) {
throw new \InvalidArgumentException('Public key failed A mod N == 0 check.');
}
return $A;
}
/**
* Calculate the client's value U which is the hash of A and B.
*
* @param \phpseclib3\Math\BigInteger $A
* @param \phpseclib3\Math\BigInteger $B
* @return \phpseclib3\Math\BigInteger
*/
public function calculateU(BigInteger $A, BigInteger $B): BigInteger
{
$A = $this->padHex($A);
$B = $this->padHex($B);
return new BigInteger($this->hexHash($A.$B), 16);
}
/**
* Extract the pool ID from pool name.
*
* @return null|string
*/
protected function poolId(): ?string
{
return explode('_', $this->poolName)[1] ?? null;
}
/**
* Authenticate user with given username and password.
*
* @param string $username
* @param string $password
* @return \Aws\Result
*
* @throws \RuntimeException
*/
public function authenticateUser(string $username, string $password): Result
{
$result = $this->client->initiateAuth([
'AuthFlow' => 'USER_SRP_AUTH',
'ClientId' => $this->clientId,
'UserPoolId' => $this->poolName,
'AuthParameters' => [
'USERNAME' => $username,
'SRP_A' => $this->largeA()->toHex(),
],
]);
if ($result->get('ChallengeName') != 'PASSWORD_VERIFIER') {
throw new \RuntimeException("ChallengeName `{$result->get('ChallengeName')}` is not supported.");
}
return $this->client->respondToAuthChallenge([
'ChallengeName' => 'PASSWORD_VERIFIER',
'ClientId' => $this->clientId,
'ChallengeResponses' => $this->processChallenge($result, $password)
]);
}
/**
* Generate authentication challenge response params.
*
* @param \Aws\Result $result
* @param string $password
* @return array
*/
protected function processChallenge(Result $result, string $password): array
{
$challengeParameters = $result->get('ChallengeParameters');
$time = Carbon::now('UTC')->format('D M j H:i:s e Y');
$secretBlock = base64_decode($challengeParameters['SECRET_BLOCK']);
$userId = $challengeParameters['USER_ID_FOR_SRP'];
$hkdf = $this->getPasswordAuthenticationKey(
$userId,
$password,
$challengeParameters['SRP_B'],
$challengeParameters['SALT']
);
$msg = $this->poolId().$userId.$secretBlock.$time;
$signature = hash_hmac('sha256', $msg, $hkdf, true);
return [
'TIMESTAMP' => $time,
'USERNAME' => $userId,
'PASSWORD_CLAIM_SECRET_BLOCK' => $challengeParameters['SECRET_BLOCK'],
'PASSWORD_CLAIM_SIGNATURE' => base64_encode($signature),
];
}
/**
* Calculates the final hkdf based on computed S value, and computed U value and the key.
*
* @param string $username
* @param string $password
* @param string $server
* @param string $salt
* @return string
*
* @throws \RuntimeException
*/
protected function getPasswordAuthenticationKey(string $username, string $password, string $server, string $salt): string
{
$u = $this->calculateU($this->largeA(), $serverB = new BigInteger($server, 16));
if ($u->equals(new BigInteger(0))) {
throw new \RuntimeException('U cannot be zero.');
}
$usernamePassword = sprintf('%s%s:%s', $this->poolId(), $username, $password);
$usernamePasswordHash = $this->hash($usernamePassword);
$x = new BigInteger($this->hexHash($this->padHex($salt).$usernamePasswordHash), 16);
$gModPowXN = $this->g->modPow($x, $this->N);
$intValue2 = $serverB->subtract($this->k->multiply($gModPowXN));
$s = $intValue2->modPow($this->smallA()->add($u->multiply($x)), $this->N);
return $this->computeHkdf(
hex2bin($this->padHex($s)),
hex2bin($this->padHex($u))
);
}
/**
* Standard hkdf algorithm.
*
* @param string $ikm
* @param string $salt
* @return string
*/
protected function computeHkdf(string $ikm, string $salt): string
{
return hash_hkdf('sha256', $ikm, 16, static::INFO_BITS, $salt);
}
}
@mtindall89
Copy link

I'm using phpseclib 2.x specifically so I can use your class as is. I did remove the namespace and that's it (as I don't have/use that namespace).

I have narrowed it down to the initiateAuth call being where this happens. I had this same issue when trying build this whole process myself, which led me to your class.

So frustrating, as I'm not even sure how to debug further since the memory error really seems to prevent getting any deeper. And can't find anyone else who's reported this type of issue.

@mtindall89
Copy link

FYI to anyone who may come across it, the issue was that PHP was needing to allocate 600+mb to complete this process (Seems high to me, unsure if that's normal/expected), and the server we were running this on had a low php memory limit. Increasing that resolved the issue. It seems that the BigInteger library uses a lot of memory.

@mastercho
Copy link

Since yesterday i start getting requests exceeded seems its IP related and AWS banned IP because from localhost requests still works. Is there a way to apply proxy into lib so requests go troughs proxy to AWS?

@jenky
Copy link
Author

jenky commented Nov 18, 2021

Since yesterday i start getting requests exceeded seems its IP related and AWS banned IP because from localhost requests still works. Is there a way to apply proxy into lib so requests go troughs proxy to AWS?

This simple class doesn't handle the HTTP request. In fact it uses aws client to make the request. You should looking for the help from aws/aws-sdk-php

@mastercho
Copy link

Since yesterday i start getting requests exceeded seems its IP related and AWS banned IP because from localhost requests still works. Is there a way to apply proxy into lib so requests go troughs proxy to AWS?

This simple class doesn't handle the HTTP request. In fact it uses aws client to make the request. You should looking for the help from aws/aws-sdk-php

i know and what is documneted here https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/guide_configuration.html not working maybe someone managed to find workaround thats why i did ask :)

@jenky
Copy link
Author

jenky commented Nov 18, 2021

Since yesterday i start getting requests exceeded seems its IP related and AWS banned IP because from localhost requests still works. Is there a way to apply proxy into lib so requests go troughs proxy to AWS?

This simple class doesn't handle the HTTP request. In fact it uses aws client to make the request. You should looking for the help from aws/aws-sdk-php

i know and what is documneted here https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/guide_configuration.html not working maybe someone managed to find workaround thats why i did ask :)

Did you try to set proxy on http option?
image

@mastercho
Copy link

Since yesterday i start getting requests exceeded seems its IP related and AWS banned IP because from localhost requests still works. Is there a way to apply proxy into lib so requests go troughs proxy to AWS?

This simple class doesn't handle the HTTP request. In fact it uses aws client to make the request. You should looking for the help from aws/aws-sdk-php

i know and what is documneted here https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/guide_configuration.html not working maybe someone managed to find workaround thats why i did ask :)

Did you try to set proxy on http option? image

like i said in previous comment i tried...

@tar-2
Copy link

tar-2 commented Jul 1, 2022

That would be awesome! I'm still trying to work it out on my own as well. I think the second link that I shared can be a good starting point as I haven't seen any other solid implementation for the DEVICE_PASSWORD_VERIFIER challenge. I can create a user pool on my personal account and give you the credentials with the proper IAM permissions or there is also this program offered by AWS - we can open a repo.

Let me know what you think.

I'm also getting the "incorrect username or password" error: were you able to solve it? Thanks in advance. @marco3211 @jenky

@ayoub-bousetta
Copy link

Life saver...

@kiwialec
Copy link

This is hopefully so painfully obvious that no one else has this problem, but I debugged 'Incorrect username or password' for hours.

It turned out that I was using my IdentityPoolId instead of my UserPoolId in the poolId field. Changing it got the auth working immediately

@surfjam518
Copy link

Thanks so much for this, was a life saver. BUT!!! the service I am accessing has now switched on MFA permanently and I need to have my system capable of accessing it unattended.

The answer is to implement Device Authentication. This seems to almost work for me but fails as the final step (DEVICE_PASSWORD_VERIFIER) just thowing the usual 'Incorrect username or password'.

As far as I can see there are two potential problem areas:
1 - confirmDevice - Where you have to confirm the device you're using and send over DeviceSecretVerifierConfig details including a PasswordVerifier and salt. Given that this is the primary area of difference from your code above I think this is where it is going wrong and hence even if I am getting things correct on the finl DEVICE_PASSWORD_VERIFIER section it will of course fail due to this earlier issue. This is my hack:

    $passSalt = $this->getDeviceSecretVerifierConfig($deviceGroupKey, $deviceK);
    $result = $this->client->confirmDevice([
        'AccessToken' => $accessT,
        'DeviceKey' => $deviceK,
        'DeviceName' => $location." Auto",
        'DeviceSecretVerifierConfig' => [
            'PasswordVerifier' => $passSalt['PasswordVerifier'],
            'Salt' => $passSalt['Salt'],
        ],
    ]);

    public function getDeviceSecretVerifierConfig(string $deviceGroupKey, string $deviceKey): array
    {
            $randomPassword = $this->bytes(40);
            $fullPassword = $this->hash(sprintf('%s%s:%s', $deviceGroupKey, $deviceKey, $randomPassword));
            $salt = $this->bytes(16);
            $SaltToHashDevices = $this->padHex(new BigInteger($salt->toHex(),16));

            $passwordVerifier = $this->g->modPow(
            new BigInteger($this->hexHash($SaltToHashDevices . $fullPassword), 16),
            $this->N
            );
            return [
                    'Salt' => base64_encode($this->padHex($SaltToHashDevices)),
                    'PasswordVerifier' => base64_encode($this->padHex($passwordVerifier)),
                    'rndPass' => $randomPassword //->toString()
            ];
    }

2 - DEVICE_PASSWORD_VERIFIER has a problem... as said though I believe the problem is most likely with the above.

So to the question, has anyone expanded this for device authentication, and if you have it working PLEASE cna you let me have the solution as it is making me mad.

@surfjam518
Copy link

commented

So After many hours....... I have managed to fix it. The issue was in my getDeviceSecretVerifierConfig function which now reads as follows:

public function getDeviceSecretVerifierConfig(string $deviceGroupKey, string $deviceKey): array
{

    $randomPassword = $this->bytes(40);
    $fullPassword = $this->hash(sprintf('%s%s:%s', $deviceGroupKey, $deviceKey, $randomPassword));

    $salt = $this->bytes(16);
    $SaltToHashDevices = $this->padHex(new BigInteger($salt->toHex(),16));

    $x = new BigInteger($this->hexHash($SaltToHashDevices.$fullPassword), 16);
    $gModPowXN = $this->g->modPow($x, $this->N);
    $passwordVerifier = $this->padHex($gModPowXN->toHex());

    return [
        'Salt' => base64_encode(hex2bin($SaltToHashDevices)),
        'PasswordVerifier' => base64_encode(hex2bin($passwordVerifier)),
        'rndPass' => $randomPassword
    ];
}

Hopefully this will save someone else a whole lot of time if they need to add device verification. Thankfully I can now wave goodbye to sms MFA requests.... or at least until the service is 'played' with again.

Enjoy

@marcocot
Copy link

marcocot commented Nov 28, 2023

Hi, we are trying to implement device authentication, using this code. The problem is that after responding to the SMS_MFA challenge, I try to call confirmDevice like this:

$response = $this->client->respondToAuthChallenge([
    "ChallengeName"      => "SMS_MFA",
    "ChallengeResponses" => $challengeResponses,
    'Session'            => $session,
    'ClientId'           => $this->appClientId,
    'UserPoolId'         => $this->userPoolId,
]);


$metadata = $response->get('AuthenticationResult');

$accessToken = $metadata['AccessToken'] ?? null;
$deviceGroupKey = $metadata['NewDeviceMetadata']['DeviceGroupKey'] ?? null;
$deviceKey = $metadata['NewDeviceMetadata']['DeviceKey'] ?? null;

$passSalt = $srp->getDeviceSecretVerifierConfig($deviceGroupKey, $deviceKey);
$result = $this->client->confirmDevice([
    'AccessToken' => $accessToken,
    'DeviceKey' => $deviceKey,
    'DeviceName' => "My personal location" . " Auto",
    'DeviceSecretVerifierConfig' => [
        'PasswordVerifier' => $passSalt['PasswordVerifier'],
        'Salt' => $passSalt['Salt'],
    ],
]);

but the client returns the error "Invalid device key given.". does anyone have ideas on how to fix this?
thanks!

@revuponline
Copy link

you are a life saver. thank you. your buy me a beer link 404s but if you've got another send it over.

@jenky
Copy link
Author

jenky commented Jan 30, 2024

You can try this one. I'm glad that my code was able to help.

@rsys-dinesh-kumar
Copy link

I have question what will be in the case of DEVICE_SRP_AUTH, DEVICE_PASSWORD_VERIFIER

@rsys-masook-ahmad
Copy link

Hello @jenky,
we're encountering an issue with the DEVICE_PASSWORD_VERIFIER challenge - it's returning an 'Incorrect username and password' error. Could you possibly assist us by providing code that utilizes both DEVICE_SRP_AUTH and DEVICE_PASSWORD_VERIFIER? We believe this could resolve our problem.

public function authenticateUser(string $username, string $password, string $deviceKey)
{
try {
$result = $this->client->initiateAuth([
'AuthFlow' => 'USER_PASSWORD_AUTH',
'ClientId' => config('services.aws_cognito.client_id'),
'UserPoolId' => config('services.aws_cognito.user_pool_id'),
'AuthParameters' => [
'USERNAME' => $username,
'PASSWORD' => $password,
'DEVICE_KEY' => $deviceKey,
],
]);

        if ($result->get('ChallengeName') == 'DEVICE_SRP_AUTH') {
            $result = $this->client->respondToAuthChallenge([
                'ChallengeName' => 'DEVICE_SRP_AUTH',
                'ClientId' => $this->clientId,
                'ChallengeResponses' => [
                    'USERNAME' => $username,
                    'DEVICE_KEY' => $deviceKey,
                    'SRP_A' => $this->largeA()->toHex(),
                ],
                'SESSION' => $result->get('Session'),
            ]);
        }


        if ($result->get('ChallengeName') != 'DEVICE_PASSWORD_VERIFIER') {
            throw new RuntimeException("ChallengeName `{$result->get('ChallengeName')}` is not supported.");
        }

        if ($result->get('ChallengeName') == 'DEVICE_PASSWORD_VERIFIER') {
            $challengeParameters = $this->processChallenge($result, $password);
            $result =  $this->client->respondToAuthChallenge([
                'ChallengeName' => 'DEVICE_PASSWORD_VERIFIER',
                'ClientId' => $this->clientId,
                'ChallengeResponses' => $challengeParameters,
            ]);
        }
    } catch (\Throwable $th) {
        print_r($th->getMessage());
        die;
    }
}

/**
 * Generate authentication challenge response params.
 */
/**
 * Generate authentication challenge response params.
 */
protected function processChallenge(Result $result, $password): array
{
    $challengeParameters = $result->get('ChallengeParameters');
    $time = Carbon::now()->tz('UTC')->format('D M j H:i:s e Y');
    $secretBlock = base64_decode($challengeParameters['SECRET_BLOCK']);
    $userName = $challengeParameters['USERNAME'];
    $deviceKey = $challengeParameters['DEVICE_KEY'];
    $deviceGroupKey = '*********';


    //$fullPassword = $this->getDeviceSecretVerifierConfig($deviceGroupKey, $userName);
    $hkdf = $this->getPasswordAuthenticationKey(
        $userName,
        $password,
        $challengeParameters['SRP_B'],
        $challengeParameters['SALT']
    );

   
    $msg = $this->poolId().$userName.$secretBlock.$time;
    $signature = hash_hmac('sha256', $msg, $hkdf, true);

    return [
        'TIMESTAMP' => $time,
        'USERNAME' => $userName,
        'DEVICE_KEY' => $deviceKey,
        'PASSWORD_CLAIM_SECRET_BLOCK' => $challengeParameters['SECRET_BLOCK'],
        'PASSWORD_CLAIM_SIGNATURE' => base64_encode($signature),
    ];
}

Thank you!

@subhan300
Copy link

@steveWinter Hi, can you plz share you js code in which you have mplenented password_verifirer challenge ?

@yasuaki640
Copy link

@jenky
Thanks for the great gist.
I would like to publish a composer package based on this implementation, is that a problem?
The package will include a reference to this gist.

@yasuaki640
Copy link

I have made a composer package of the code based on this great gist. (Of course, the link to this gist is included.)
Please let me know if there are any problems and I will remove it.

https://github.com/yasuaki640/cognito-srp-php

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment