I'm following Google's documentation, on how to create a JWT for a specific service account.
The document instructs on how to manually create a JWT, as well as how to compute the signing process, based on JWS guidelines.
I followed the exact procedure described by Google, but no matter what i do, the generated JWT cannot be validated by jwt.io, which fails with a Invalid Signature.
Follow the guidelines, i created the service account, and a specific key, which contains the Private Key details:
{
"type": "service_account",
"project_id": "myProject",
"private_key_id": "1212121212",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Cg7FW0NGwLeD\nrpc0r1Ayta23GxVw0KCA+d/TjSyuZ3lmKiObz9EGJpHSHbX4yrODA6FvYixUrAKm\nUSSMvFLUpYM2xoEgAKnwd6XVgdnjwk7wnIIsEdyjMCbews1orr6Ze+LIPkV2WF4d\nHSAqRqJrERR1Gb9gxKC/WQhMvCotp7zFTqLcUI3eUhR3tIgpLwZFpIxXZjOTwWoB\n6bWxOe39Suft1GYAR0prFcLmXtfw43B+9gVcMLOHBBTcxojBXkQ2bhjp7dGqvlUz\n3nO/1bqbzvd5I6bQif+tjLEceyIUbE/rJ6PgW9SVtfktrQIQQ9VGtAUya4IYSEL8\nJaXZxs6jAgMBAAECggEAFe1+3J0OYZcQPZb2AjSi+1oTm6GmWSJ6ssNpu9x8pq+f\nxPSfbaUjRGhTsCOnNIlzhnDACRQIOYHSJTrJFbMc2b2XdBPyqgPfdPNTf/QNtHOK\nqUbSwj2Ho6sJdYJ+QbaGOGgO8uM2QL+uFM3RHvwUiT2SlWHsukny3ATFUAVIYPUj\nxr0m6QKBgQDiH6vL1plGsIFVWR5M\nESsZdADubhDOtml6r81aKLXJPK9LeHwJOAgTFfZHJD4D4e7KSQfYlbf9tRE7c2PE\ntcj6BVrHdtYRqaXY+Q7BW2mXRb7IJKtVxZzljPY0HcDjpZ7UqXUB/sVbxT/zbt4B\n9lIegpLJyd6RpzYhjIDv8OIaTwKBgQDRfMLsTg0+nTzmmIurmD1IhdPa7KvhGMDn\nXSs0zRR4IRC2BCn5LHYYD4cgO+mmGWxcQZREQ220W3uXwRbSTJZT6ZtzP40AXx86\nTRop5NBZYDkdJ1t9qhi2aU//5mwn5ubC/42fBCwqmRXr0nOtLhKtEONRmGGyl7hk\nWXWII2z1bQKBgAMoNArVhTBSeIvLgbvIJZTmZLjvenaYX2KiH7jZhqg3mRoyUuvA2glpo9ARzB7ut\nR5LXq5GAwOBIzMhtZWTyE53O9jI5+8g/RB7WlUx\nsZt5bkf42zhsJwZnfV480Hx8GhnCnhGcTVjJbbN5AoGBAInRfNcLpgPtHWiQ5r9W\nANd+XDLpjIUQfh+0NaQeYPG7DM59oPRqUDs/BSp21nTmSnNC537H0OHlCScpmc7G\ncpj+/jtLIhTN0IwKosaH3mJpQ3AcUI7IooFKgYrC/bwCUQ5xX7CwqaOzTKf3MtX1\nngd7mPWTFkRDxCkCnvfUfcem\n-----END PRIVATE KEY-----\n",
"client_email": "test#email",
"client_id": "121212",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/myProject-dev.iam.gserviceaccount.com"
}
I'm using .net 5, and the .net implementation for manually creating and signing a JWT, would be:
private string GetToken()
{
var header = "{\"typ\":\"JWT\",\"alg\":\"RS256\",\"kid\":\"ed36c257c59ebabb47b456828a858aa5fcda12xx\"}";
var claims = "{\"sub\":\"10217931234509168826\",\"email\":\"test#email.com\",\"iss\":\"https:\\//accounts.google.com\",\"aud\":\"MyAudience",\"exp\":1665530643,\"iat\":1665527043}";
var b64header = Convert.ToBase64String(Encoding.UTF8.GetBytes(header))
.Replace('+', '-').Replace('/', '_').Replace("=", "");
var b64claims = Convert.ToBase64String(Encoding.UTF8.GetBytes(claims))
.Replace('+', '-').Replace('/', '_').Replace("=", "");
var payload = b64header + "." + b64claims;
var message = Encoding.UTF8.GetBytes(payload);
var sig = Convert.ToBase64String(SignData(message))
.Replace('+', '-').Replace('/', '_').Replace("=", "");
return payload + "." + sig;
}
private static byte[] SignData(byte[] message)
{
var privateKeyContent = "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Cg7FW0NGwLeD\nrpc0r1Ayta23GxVw0KCA+d/TjSyuZ3lmKiObz9EGJpHSHbX4yrODA6FvYixUrAKm\nUSSMvFLUpYM2xoEgAKnwd6XVgdnjwk7wnIIsEdyjMCbews1orr6Ze+LIPkV2WF4d\nHSAqRqJrERR1Gb9gxKC/WQhMvCotp7zFTqLcUI3eUhR3tIgpLwZFpIxXZjOTwWoB\n6bWxOe39Suft1GYAR0prFcLmXtfw43B+9gVcMLOHBBTLm6QKBgQDiH6vL1plGsIFVWR5M\nESsZdADubhDOtml6r81aKLXJPK9LeHwJOAgTFfZHJD4D4e7KSQfYlbf9tRE7c2PE\ntcj6BVrHdtYRqaXY+Q7BW2mXRb7IJKtVxYPG7DM59oPRqUDs/BSp21nTmSnNC537H0OHlCScpmc7G\ncpj+/jtLIhTN0IwKosaH3mJpQ3AcUI7IooFKgYrC/bwCUQ5xX7CwqaOzTKf3MtX1\nngd7mPWTFkRDxCkCnvfUfcem\n-----END PRIVATE KEY-----";
var rsa = RSA.Create();
var privateKey = privateKeyPem.Replace("-----BEGIN PRIVATE KEY-----", string.Empty).Replace("-----END PRIVATE KEY-----", string.Empty);
privateKey = privateKey.Replace("\n", string.Empty);
privateKey = privateKey.Replace("\r\n", string.Empty);
var privateKeyBytes = Convert.FromBase64String(privateKey);
rsa.ImportPkcs8PrivateKey(privateKeyBytes, out int _);
return rsa.SignData(message, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
I don't like this manual approach, so i used the .net Cryptography classes to create and sign the JWT Token:
private string GetToken2()
{
// keeping only the payload of the key
var privateKeyPem = "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Cg7FW0NGwLeD\nrpc0r1Ayta23GxVw0KCA+d/TjSyuZ3lmKiObz9EGJpHSHbX4yrODA6FvYixUrAKm\nUSSMvFLUpYM2xoEgAKnwd6XVgdnjwk7wnIIsEzlJR5t9tWKLd1VL1133w6jigLv5kDzWQTLAoGBAL0B\n7fS672RBBgOgOtRVhWV7qYvq4aE0bkfRXfxD1GYWnzc6RoyUuvA2glpo9ARzB7ut\nR5LXq5GAwOBIzMhtZWzMZv7ypctiB5DYo/SMiBc7pAxTyE53O9jI5+8g/RB7WlUx\nsZt5bkf42zhsJwZnfV480Hx8GhnCnhGcTVjJbbN5AoGBAInRfNcLpgPtHWiQ5r9W\nANd+XDLpjIUQfh+0NaQeYPG7DM59oPRqUDs/BSp21nTmSnNC537H0OHlCScpmc7G\ncpj+/jtLIhTN0IwKosaH3mJpQ3AcUI7IooFKgYrC/bwCUQ5xX7CwqaOzTKf3MtX1\nngd7mPWTFkRDxCkCnvfUfcem\n-----END PRIVATE KEY-----\n";
var privateKey = privateKeyPem.Replace("-----BEGIN PRIVATE KEY-----", string.Empty).Replace("-----END PRIVATE KEY-----", string.Empty);
privateKey = privateKey.Replace("\n", string.Empty);
privateKey = privateKey.Replace(Environment.NewLine, string.Empty);
var privateKeyRaw = Convert.FromBase64String(privateKey);
// creating the RSA key
using var provider = new RSACryptoServiceProvider();
provider.ImportPkcs8PrivateKey(new ReadOnlySpan<byte>(privateKeyRaw), out _);
var rsaSecurityKey = new RsaSecurityKey(provider);
// Generating the token
var now = DateTime.UtcNow;
var claims = new[] {
new Claim(JwtRegisteredClaimNames.Sub, "10217931234509168826"),
new Claim("email", "myProject-dev.iam.gserviceaccount.com"),
};
var handler = new JwtSecurityTokenHandler();
var token = new JwtSecurityToken
(
"https://accounts.google.com",
"MyAudience",
claims,
now.AddMilliseconds(-30),
now.AddMinutes(60),
new SigningCredentials(rsaSecurityKey, SecurityAlgorithms.RsaSha256)
);
return handler.WriteToken(token);
}
The Google documentation indicates how to sign the JWT:
Sign the UTF-8 representation of the input using SHA256withRSA (also known as RSASSA-PKCS1-V1_5-SIGN with the SHA-256 hash function) with the private key obtained from the Google API Console.
Both the implementation strictly follow the google documentation guidelines, but the generated JWT also fails to validate. I tried several representations of the private key, i.e., replacing the '\n', leaving the '\n', etc, etc, but it always fail.
Both implementations seems correct, but something is missing!
Any ideas on what is missing ?? Thank you in advance.
--------------------- EDIT 1 ---------------------------
To guarantee that the Primary Key data is being correctly handled, and avoid string replacement and encoding, i used the BouncyCastle library which is a lightweight cryptography API, to handle the Primary Key. So i load the JSON KEY file directly, and use BouncyCastle to load the RSA Parameters:
private RSAParameters GetPrivateKeyRSAParameters()
{
var path = "c:\\myproject-key-3433434.json";
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
var credentialParameters = NewtonsoftJsonSerializer.Instance.Deserialize<JsonCredentialParameters>(stream);
RSAParameters rsaParams;
using (var tr = new StringReader(credentialParameters.PrivateKey))
{
var pemReader = new PemReader(tr);
if (pemReader.ReadObject() is not AsymmetricKeyParameter key)
{
throw new Exception("Could not read private key");
}
var privateRsaParams = key as RsaPrivateCrtKeyParameters;
rsaParams = DotNetUtilities.ToRSAParameters(privateRsaParams);
}
return rsaParams;
}
This way, instead of relying in handling the Primary Key as text, everything is handled by BouncyCastle.
For code block 1)
var rsa = RSA.Create();
rsa.ImportParameters(GetPrivateKeyRSAParameters());
For code block 2)
var rsaSecurityKey = new RsaSecurityKey(GetPrivateKeyRSAParameters());
So now i have the guarantee that the Primary Key data is being correctly handled, but the end result is the same, the resulting token has always an "Invalid Signature".
--------------------- EDIT 2 ---------------------------
Google has an example on how to do this in JAVA, in this document.
public static String generateJwt(final String saKeyfile, final String saEmail,
final String audience, final int expiryLength)
throws FileNotFoundException, IOException {
Date now = new Date();
Date expTime = new Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(expiryLength));
JWTCreator.Builder token = JWT.create()
.withIssuedAt(now)
.withExpiresAt(expTime)
.withIssuer(saEmail)
.withAudience(audience)
.withSubject(saEmail)
.withClaim("email", saEmail);
FileInputStream stream = new FileInputStream(saKeyfile);
ServiceAccountCredentials cred = ServiceAccountCredentials.fromStream(stream);
RSAPrivateKey key = (RSAPrivateKey) cred.getPrivateKey();
Algorithm algorithm = Algorithm.RSA256(null, key);
return token.sign(algorithm);
}
Replicating the code to .net, i assume it would be as:
private string GetToken5(string path)
{
var now = DateTime.UtcNow;
var claims = new[] { new Claim(JwtRegisteredClaimNames.Sub, "10217931236909168826") };
var handler = new JwtSecurityTokenHandler();
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
var serviceAccountCredential = ServiceAccountCredential.FromServiceAccountData(stream);
var token = new JwtSecurityToken
(
"https://accounts.google.com",
"Audience",
claims,
now.AddMilliseconds(-30),
now.AddMinutes(60),
new SigningCredentials(new RsaSecurityKey(serviceAccountCredential.Key), SecurityAlgorithms.RsaSha256)
);
token.Header.Add("kid", "955104a37fa903e232339e83edb29b0c45");
return handler.WriteToken(token);
}
But, this also doesn't work.
There's still something missing ...
Coding wise, the last snippet is a correct way for generating a custom JWT for Google Auth.
The problem is that when a Service Account is created, it has a specific PUBLIC KEY which is used to verify the Token.
When creating the token with the Issuer as "https://accounts.google.com", when verifying the token, another public key was used, thus the validation failed.
The solution was to implement our own .well-known/openid-configuration Endpoint, and matching the Token claims with the information provided by this endpoint.
I'm trying to use this example to calculate an AWS4 signature for an s3 upload -
http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-post-example.html
var exampleSecretKey = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY";
var exampleBase64EncodedPolicy = "eyAiZXhwaXJhdGlvbiI6ICIyMDE1LTEyLTMwVDEyOjAwOjAwLjAwMFoiLA0KICAiY29uZGl0aW9ucyI6IFsNCiAgICB7ImJ1Y2tldCI6ICJzaWd2NGV4YW1wbGVidWNrZXQifSwNCiAgICBbInN0YXJ0cy13aXRoIiwgIiRrZXkiLCAidXNlci91c2VyMS8iXSwNCiAgICB7ImFjbCI6ICJwdWJsaWMtcmVhZCJ9LA0KICAgIHsic3VjY2Vzc19hY3Rpb25fcmVkaXJlY3QiOiAiaHR0cDovL3NpZ3Y0ZXhhbXBsZWJ1Y2tldC5zMy5hbWF6b25hd3MuY29tL3N1Y2Nlc3NmdWxfdXBsb2FkLmh0bWwifSwNCiAgICBbInN0YXJ0cy13aXRoIiwgIiRDb250ZW50LVR5cGUiLCAiaW1hZ2UvIl0sDQogICAgeyJ4LWFtei1tZXRhLXV1aWQiOiAiMTQzNjUxMjM2NTEyNzQifSwNCiAgICB7IngtYW16LXNlcnZlci1zaWRlLWVuY3J5cHRpb24iOiAiQUVTMjU2In0sDQogICAgWyJzdGFydHMtd2l0aCIsICIkeC1hbXotbWV0YS10YWciLCAiIl0sDQoNCiAgICB7IngtYW16LWNyZWRlbnRpYWwiOiAiQUtJQUlPU0ZPRE5ON0VYQU1QTEUvMjAxNTEyMjkvdXMtZWFzdC0xL3MzL2F3czRfcmVxdWVzdCJ9LA0KICAgIHsieC1hbXotYWxnb3JpdGhtIjogIkFXUzQtSE1BQy1TSEEyNTYifSwNCiAgICB7IngtYW16LWRhdGUiOiAiMjAxNTEyMjlUMDAwMDAwWiIgfQ0KICBdDQp9";
var expectedValue = "8afdbf4008c03f22c2cd3cdb72e4afbb1f6a588f3255ac628749a66d7f09699e";
var awsSignature = AWS4Signer.ComputeSignature("AKIAIOSFODNN7EXAMPLE", exampleSecretKey, "us-east-1", new DateTime(2015, 12, 29, 0, 0, 0, DateTimeKind.Utc), "s3", "", exampleBase64EncodedPolicy);
I'm using the values provided in the example but not getting the expected value from the ComputeSignature method. I suspect it's because I'm passing in an empty string for the "signedheaders" argument (if so then what should this be?). Can someone explain what I'm doing wrong please?
Never managed to get it working with AWS4Signer but I have managed to do it manually.
using System.Security.Cryptography;
using System.Text;
public static class S3UploadSignature
{
/// <summary>
/// Generates the X-Amz-Signature parameter for an S3 upload via the browser
/// </summary>
/// <param name="base64EncodedPolicy">The upload policy JSON string encoded as base 64</param>
/// <param name="awsSecretKey">The AWS secret identity key</param>
/// <param name="date">The date the request was signed (yyyyMMdd)</param>
/// <param name="regionName">The AWS region the bucket sits in e.g. us-east-1</param>
/// <param name="serviceName">The AWS service name e.g. s3</param>
public static string Create(string base64EncodedPolicy, string awsSecretKey, string date, string regionName, string serviceName)
{
var signatureKey = CalculteSignatureKey(awsSecretKey, date, regionName, serviceName);
return ToBase16String(HmacSha256(base64EncodedPolicy, signatureKey));
}
private static string ToBase16String(byte[] bytes)
{
var result = new StringBuilder(bytes.Length * 2);
foreach (var b in bytes)
result.AppendFormat("{0:x2}", b);
return result.ToString();
}
private static byte[] HmacSha256(string data, byte[] key)
{
return new HMACSHA256(key).ComputeHash(Encoding.UTF8.GetBytes(data));
}
private static byte[] CalculteSignatureKey(string key, string dateStamp, string regionName, string serviceName)
{
var secret = Encoding.UTF8.GetBytes(("AWS4" + key).ToCharArray());
var date = HmacSha256(dateStamp, secret);
var region = HmacSha256(regionName, date);
var service = HmacSha256(serviceName, region);
var signing = HmacSha256("aws4_request", service);
return signing;
}
}
Please try this.
import com.amazonaws.util.BinaryUtils;
import org.junit.Assert;
import org.junit.Test;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.util.Base64;
public class AwsTest {
#Test
public void test() throws Exception {
String stringToSign = getStringToSign("<policy_json>");
Assert.assertEquals("eyAiZXhwaXJhdGlvbiI6ICIyMDE1LTEyLTMwVDEyOjAwOjAwLjAwMFoiLA0KICAiY29uZGl0aW9ucyI6IFsNCiAgICB7ImJ1Y2tldCI6ICJzaWd2NGV4YW1wbGVidWNrZXQifSwNCiAgICBbInN0YXJ0cy13aXRoIiwgIiRrZXkiLCAidXNlci91c2VyMS8iXSwNCiAgICB7ImFjbCI6ICJwdWJsaWMtcmVhZCJ9LA0KICAgIHsic3VjY2Vzc19hY3Rpb25fcmVkaXJlY3QiOiAiaHR0cDovL3NpZ3Y0ZXhhbXBsZWJ1Y2tldC5zMy5hbWF6b25hd3MuY29tL3N1Y2Nlc3NmdWxfdXBsb2FkLmh0bWwifSwNCiAgICBbInN0YXJ0cy13aXRoIiwgIiRDb250ZW50LVR5cGUiLCAiaW1hZ2UvIl0sDQogICAgeyJ4LWFtei1tZXRhLXV1aWQiOiAiMTQzNjUxMjM2NTEyNzQifSwNCiAgICB7IngtYW16LXNlcnZlci1zaWRlLWVuY3J5cHRpb24iOiAiQUVTMjU2In0sDQogICAgWyJzdGFydHMtd2l0aCIsICIkeC1hbXotbWV0YS10YWciLCAiIl0sDQoNCiAgICB7IngtYW16LWNyZWRlbnRpYWwiOiAiQUtJQUlPU0ZPRE5ON0VYQU1QTEUvMjAxNTEyMjkvdXMtZWFzdC0xL3MzL2F3czRfcmVxdWVzdCJ9LA0KICAgIHsieC1hbXotYWxnb3JpdGhtIjogIkFXUzQtSE1BQy1TSEEyNTYifSwNCiAgICB7IngtYW16LWRhdGUiOiAiMjAxNTEyMjlUMDAwMDAwWiIgfQ0KICBdDQp9", stringToSign);
String signature = getSignature(stringToSign, "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", "20151229", "us-east-1", "s3");
Assert.assertEquals("8afdbf4008c03f22c2cd3cdb72e4afbb1f6a588f3255ac628749a66d7f09699e", signature);
}
private static String getStringToSign(String policy) throws Exception {
return Base64.getEncoder().encodeToString(policy.replaceAll("\n", "\r\n").getBytes("UTF-8"));
}
private static String getSignature(String stringToSign, String secretAccessKey, String dateStamp, String regionName, String serviceName) throws Exception {
byte[] signingKey = getSignatureKey(secretAccessKey, dateStamp, regionName, serviceName);
return BinaryUtils.toHex(HmacSHA256(stringToSign, signingKey));
}
private static byte[] HmacSHA256(String data, byte[] key) throws Exception {
String algorithm="HmacSHA256";
Mac mac = Mac.getInstance(algorithm);
mac.init(new SecretKeySpec(key, algorithm));
return mac.doFinal(data.getBytes("UTF8"));
}
private static byte[] getSignatureKey(String key, String dateStamp, String regionName, String serviceName) throws Exception {
byte[] kSecret = ("AWS4" + key).getBytes("UTF8");
byte[] kDate = HmacSHA256(dateStamp, kSecret);
byte[] kRegion = HmacSHA256(regionName, kDate);
byte[] kService = HmacSHA256(serviceName, kRegion);
byte[] kSigning = HmacSHA256("aws4_request", kService);
return kSigning;
}
}
I am stuck at "Amazon Cognito Identity user pools" process.
I tried all possible codes for authenticating user in cognito userpools. But I always get error saying "Error: Unable to verify secret hash for client 4b*******fd".
Here is code:
AWS.config.region = 'us-east-1'; // Region
AWS.config.credentials = new AWS.CognitoIdentityCredentials({
IdentityPoolId: 'us-east-1:b64bb629-ec73-4569-91eb-0d950f854f4f'
});
AWSCognito.config.region = 'us-east-1';
AWSCognito.config.credentials = new AWS.CognitoIdentityCredentials({
IdentityPoolId: 'us-east-1:b6b629-er73-9969-91eb-0dfffff445d'
});
AWSCognito.config.update({accessKeyId: 'AKIAJNYLRONAKTKBXGMWA', secretAccessKey: 'PITHVAS5/UBADLU/dHITesd7ilsBCm'})
var poolData = {
UserPoolId : 'us-east-1_l2arPB10',
ClientId : '4bmsrr65ah3oas5d4sd54st11k'
};
var userPool = new AWSCognito.CognitoIdentityServiceProvider.CognitoUserPool(poolData);
var userData = {
Username : 'ronakpatel#gmail.com',
Pool : userPool
};
var cognitoUser = new AWSCognito.CognitoIdentityServiceProvider.CognitoUser(userData);
cognitoUser.confirmRegistration('123456', true,function(err, result) {
if (err) {
alert(err);
return;
}
console.log('call result: ' + result);
});
It seems that currently AWS Cognito doesn't handle client secret perfectly. It will work in the near future but as for now it is still a beta version.
For me it is working fine for an app without a client secret but fails for an app with a client secret.
So in your user pool try to create a new app without generating a client secret. Then use that app to signup a new user or to confirm registration.
According to the Docs: http://docs.aws.amazon.com/cognito/latest/developerguide/setting-up-the-javascript-sdk.html
The Javascript SDK doesn't support Apps with a Client Secret.
The instructions now state that you need to uncheck the "Generate Client Secret" when creating the app for the User Pool.
This might be a fews years late but just uncheck the "Generate client secret" option" and it will work for your web clients.
Since everyone else has posted their language, here's node (and it works in the browser with browserify-crypto, automatically used if you use webpack or browserify):
const crypto = require('crypto');
...
crypto.createHmac('SHA256', clientSecret)
.update(username + clientId)
.digest('base64')
I had the same problem in the .net SDK.
Here's how I solved in, in case anyone else needs it:
public static class CognitoHashCalculator
{
public static string GetSecretHash(string username, string appClientId, string appSecretKey)
{
var dataString = username + appClientId;
var data = Encoding.UTF8.GetBytes(dataString);
var key = Encoding.UTF8.GetBytes(appSecretKey);
return Convert.ToBase64String(HmacSHA256(data, key));
}
public static byte[] HmacSHA256(byte[] data, byte[] key)
{
using (var shaAlgorithm = new System.Security.Cryptography.HMACSHA256(key))
{
var result = shaAlgorithm.ComputeHash(data);
return result;
}
}
}
Signing up then looks like this:
public class CognitoSignUpController
{
private readonly IAmazonCognitoIdentityProvider _amazonCognitoIdentityProvider;
public CognitoSignUpController(IAmazonCognitoIdentityProvider amazonCognitoIdentityProvider)
{
_amazonCognitoIdentityProvider = amazonCognitoIdentityProvider;
}
public async Task<bool> SignUpAsync(string userName, string password, string email)
{
try
{
var request = CreateSignUpRequest(userName, password, email);
var authResp = await _amazonCognitoIdentityProvider.SignUpAsync(request);
return true;
}
catch
{
return false;
}
}
private static SignUpRequest CreateSignUpRequest(string userName, string password, string email)
{
var clientId = ConfigurationManager.AppSettings["ClientId"];
var clientSecretId = ConfigurationManager.AppSettings["ClientSecretId"];
var request = new SignUpRequest
{
ClientId = clientId,
SecretHash = CognitoHashCalculator.GetSecretHash(userName, clientId, clientSecretId),
Username = userName,
Password = password,
};
request.UserAttributes.Add("email", email);
return request;
}
}
Amazon mention how Computing SecretHash Values for Amazon Cognito in their documentation with Java application code. Here this code works with boto 3 Python SDK.
You can find your App clients in left side menu under General settings. Get those App client id and App client secret to create SECRET_HASH. For your better understand I commented out all the outputs of each and every line.
import hashlib
import hmac
import base64
app_client_secret = 'u8f323eb3itbr3731014d25spqtv5r6pu01olpp5tm8ebicb8qa'
app_client_id = '396u9ekukfo77nhcfbmqnrec8p'
username = 'wasdkiller'
# convert str to bytes
key = bytes(app_client_secret, 'latin-1') # b'u8f323eb3itbr3731014d25spqtv5r6pu01olpp5tm8ebicb8qa'
msg = bytes(username + app_client_id, 'latin-1') # b'wasdkiller396u9ekukfo77nhcfbmqnrec8p'
new_digest = hmac.new(key, msg, hashlib.sha256).digest() # b'P$#\xd6\xc1\xc0U\xce\xc1$\x17\xa1=\x18L\xc5\x1b\xa4\xc8\xea,\x92\xf5\xb9\xcdM\xe4\x084\xf5\x03~'
SECRET_HASH = base64.b64encode(new_digest).decode() # UCQj1sHAVc7BJBehPRhMxRukyOoskvW5zU3kCDT1A34=
In the boto 3 documentation, we can see lot of time ask about SECRET_HASH. So above code lines help you to create this SECRET_HASH.
If you don't want to use SECRET_HASH just uncheck Generate client secret when creating an app.
For anybody interested in using AWS Lambda to sign up a user using the AWS JS SDK, these are the steps I did:
Create another lambda function in python to generate the key:
import hashlib
import hmac
import base64
secretKey = "key"
clientId = "clientid"
digest = hmac.new(secretKey,
msg=username + clientId,
digestmod=hashlib.sha256
).digest()
signature = base64.b64encode(digest).decode()
Call the function through the nodeJS function in AWS. The signature acted as the secret hash for Cognito
Note: The answer is based heavily off George Campbell's answer in the following link: Calculating a SHA hash with a string + secret key in python
Solution for golang. Seems like this should be added to the SDK.
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
)
func SecretHash(username, clientID, clientSecret string) string {
mac := hmac.New(sha256.New, []byte(clientSecret))
mac.Write([]byte(username + ClientID))
return base64.StdEncoding.EncodeToString(mac.Sum(nil))
}
Solution for NodeJS with SecretHash
It seems silly that AWS removed the secret key from the SDK as it will not be exposed in NodeJS.
I got it working in NodeJS by intercepting fetch and adding in the hashed key using #Simon Buchan's answer.
cognito.js
import { CognitoUserPool, CognitoUserAttribute, CognitoUser } from 'amazon-cognito-identity-js'
import crypto from 'crypto'
import * as fetchIntercept from './fetch-intercept'
const COGNITO_SECRET_HASH_API = [
'AWSCognitoIdentityProviderService.ConfirmForgotPassword',
'AWSCognitoIdentityProviderService.ConfirmSignUp',
'AWSCognitoIdentityProviderService.ForgotPassword',
'AWSCognitoIdentityProviderService.ResendConfirmationCode',
'AWSCognitoIdentityProviderService.SignUp',
]
const CLIENT_ID = 'xxx'
const CLIENT_SECRET = 'xxx'
const USER_POOL_ID = 'xxx'
const hashSecret = (clientSecret, username, clientId) => crypto.createHmac('SHA256', clientSecret)
.update(username + clientId)
.digest('base64')
fetchIntercept.register({
request(url, config) {
const { headers } = config
if (headers && COGNITO_SECRET_HASH_API.includes(headers['X-Amz-Target'])) {
const body = JSON.parse(config.body)
const { ClientId: clientId, Username: username } = body
// eslint-disable-next-line no-param-reassign
config.body = JSON.stringify({
...body,
SecretHash: hashSecret(CLIENT_SECRET, username, clientId),
})
}
return [url, config]
},
})
const userPool = new CognitoUserPool({
UserPoolId: USER_POOL_ID,
ClientId: CLIENT_ID,
})
const register = ({ email, password, mobileNumber }) => {
const dataEmail = { Name: 'email', Value: email }
const dataPhoneNumber = { Name: 'phone_number', Value: mobileNumber }
const attributeList = [
new CognitoUserAttribute(dataEmail),
new CognitoUserAttribute(dataPhoneNumber),
]
return userPool.signUp(email, password, attributeList, null, (err, result) => {
if (err) {
console.log((err.message || JSON.stringify(err)))
return
}
const cognitoUser = result.user
console.log(`user name is ${cognitoUser.getUsername()}`)
})
}
export {
register,
}
fetch-inceptor.js (Forked and edited for NodeJS from Fork of https://github.com/werk85/fetch-intercept/blob/develop/src/index.js)
let interceptors = []
if (!global.fetch) {
try {
// eslint-disable-next-line global-require
global.fetch = require('node-fetch')
} catch (err) {
throw Error('No fetch available. Unable to register fetch-intercept')
}
}
global.fetch = (function (fetch) {
return (...args) => interceptor(fetch, ...args)
}(global.fetch))
const interceptor = (fetch, ...args) => {
const reversedInterceptors = interceptors.reduce((array, _interceptor) => [_interceptor].concat(array), [])
let promise = Promise.resolve(args)
// Register request interceptors
reversedInterceptors.forEach(({ request, requestError }) => {
if (request || requestError) {
promise = promise.then(_args => request(..._args), requestError)
}
})
// Register fetch call
promise = promise.then(_args => fetch(..._args))
// Register response interceptors
reversedInterceptors.forEach(({ response, responseError }) => {
if (response || responseError) {
promise = promise.then(response, responseError)
}
})
return promise
}
const register = (_interceptor) => {
interceptors.push(_interceptor)
return () => {
const index = interceptors.indexOf(_interceptor)
if (index >= 0) {
interceptors.splice(index, 1)
}
}
}
const clear = () => {
interceptors = []
}
export {
register,
clear,
}
A quick fix for the above mentioned problem statement would be to delete the existing "App Client" and crate a new one with unchecked Generate client secret
Note : Don't forget to change the app client string in the code.
In Java you could use this code:
private String getSecretHash(String email, String appClientId, String appSecretKey) throws Exception {
byte[] data = (email + appClientId).getBytes("UTF-8");
byte[] key = appSecretKey.getBytes("UTF-8");
return Base64.encodeAsString(HmacSHA256(data, key));
}
static byte[] HmacSHA256(byte[] data, byte[] key) throws Exception {
String algorithm = "HmacSHA256";
Mac mac = Mac.getInstance(algorithm);
mac.init(new SecretKeySpec(key, algorithm));
return mac.doFinal(data);
}
this is a sample php code that I use to generate the secret hash
<?php
$userId = "aaa";
$clientId = "bbb";
$clientSecret = "ccc";
$s = hash_hmac('sha256', $userId.$clientId, $clientSecret, true);
echo base64_encode($s);
?>
in this case the result is:
DdSuILDJ2V84zfOChcn6TfgmlfnHsUYq0J6c01QV43I=
for JAVA and .NET you need to pass the secret has in the auth parameters with the name SECRET_HASH.
AdminInitiateAuthRequest request = new AdminInitiateAuthRequest
{
ClientId = this.authorizationSettings.AppClientId,
AuthFlow = AuthFlowType.ADMIN_NO_SRP_AUTH,
AuthParameters = new Dictionary<string, string>
{
{"USERNAME", username},
{"PASSWORD", password},
{
"SECRET_HASH", EncryptionHelper.GetSecretHash(username, AppClientId, AppClientSecret)
}
},
UserPoolId = this.authorizationSettings.UserPoolId
};
And it should work.
The crypto package for javascript is deprecated so using crypto-js:
import CryptoJS from 'crypto-js';
import Base64 from 'crypto-js/enc-base64';
const secretHash = Base64.stringify(CryptoJS.HmacSHA256(username + clientId, clientSecret));
Remeber to run npm install #types/crypto-js crypto-js before
C++ with the Qt Framework
QByteArray MyObject::secretHash(
const QByteArray& email,
const QByteArray& appClientId,
const QByteArray& appSecretKey)
{
QMessageAuthenticationCode code(QCryptographicHash::Sha256);
code.setKey(appSecretKey);
code.addData(email);
code.addData(appClientId);
return code.result().toBase64();
};
Here is my 1 command, and it works (Confirmed :))
EMAIL="EMAIL#HERE.com" \
CLIENT_ID="[CLIENT_ID]" \
CLIENT_SECRET="[CLIENT_ID]" \
&& SECRET_HASH=$(echo -n "${EMAIL}${CLIENT_ID}" | openssl dgst -sha256 -hmac "${CLIENT_SECRET}" | xxd -r -p | openssl base64) \
&& aws cognito-idp ... --secret-hash "${SECRET_HASH}"
This solution works in March 2021:
In case you're working with a client which has both "client_secret" and "client_id" generated, instead of calculating the SECRET_HASH and providing it to the function as specified in AWS docs, pass the "client_secret".
Note: I was trying to generate new tokens from the refresh token.
let result = await cognitoIdentityServiceProvidor
.initiateAuth({
AuthFlow: "REFRESH_TOKEN",
ClientId: clientId,
AuthParameters: {
REFRESH_TOKEN: refresh_token,
SECRET_HASH: clientSecret,
},
})
.promise();
It's absurd, but it works!
There might be a more compact version, but this works for Ruby, specifically in Ruby on Rails without having to require anything:
key = ENV['COGNITO_SECRET_HASH']
data = username + ENV['COGNITO_CLIENT_ID']
digest = OpenSSL::Digest.new('sha256')
hmac = Base64.strict_encode64(OpenSSL::HMAC.digest(digest, key, data))
NodeJS solution:
Compute secret hash for authenticating action:
import * as crypto from 'crypto';
const secretHash = crypto
.createHmac('SHA256', clientSecret)
.update(email + clientId)
.digest('base64');
Compute secret hash for refresh token action:
import * as crypto from 'crypto';
const secretHash = crypto
.createHmac('SHA256', clientSecret)
.update(sub + clientId)
.digest('base64');
The parameter object looks like this:
const authenticateParams = {
ClientId: clientId,
UserPoolId: poolId,
AuthFlow: CognitoAuthFlow.ADMIN_NO_SRP_AUTH,
AuthParameters: {
PASSWORD: password,
USERNAME: email,
SECRET_HASH: secretHash,
},
};
const refreshTokenParams = {
ClientId: clientId,
UserPoolId: poolId,
AuthFlow: CognitoAuthFlow.REFRESH_TOKEN_AUTH,
AuthParameters: {
REFRESH_TOKEN: refreshToken,
SECRET_HASH: secretHash,
},
};
Usage:
import * as CognitoIdentityProvider from 'aws-sdk/clients/cognitoidentityserviceprovider';
const provider = new CognitoIdentityProvider({ region });
provider.adminInitiateAuth(params).promise(); // authenticateParams or refreshTokenParams, return a promise object.
Cognito Authentication
Error: App client is not configured for secret but secret hash was received
Providing secretKey as nil worked for me. Credentials provided include :-
CognitoIdentityUserPoolRegion (region)
CognitoIdentityUserPoolId
(userPoolId)
CognitoIdentityUserPoolAppClientId (ClientId)
AWSCognitoUserPoolsSignInProviderKey (AccessKeyId)
// setup service configuration
let serviceConfiguration = AWSServiceConfiguration(region: CognitoIdentityUserPoolRegion, credentialsProvider: nil)
// create pool configuration
let poolConfiguration = AWSCognitoIdentityUserPoolConfiguration(clientId: CognitoIdentityUserPoolAppClientId,
clientSecret: nil,
poolId: CognitoIdentityUserPoolId)
// initialize user pool client
AWSCognitoIdentityUserPool.register(with: serviceConfiguration, userPoolConfiguration: poolConfiguration, forKey: AWSCognitoUserPoolsSignInProviderKey)
All above things work with below linked code sample.
AWS Sample code : https://github.com/awslabs/aws-sdk-ios-samples/tree/master/CognitoYourUserPools-Sample/Swift
Let me know if that doesn't work for you.
The below seems to work with .NET now, for asp.net pages using the Alexa Skills SDK for .NET by Time Heur
Inject dependency
private readonly CognitoUserManager<CognitoUser> _userManager;
public RegisterModel(
UserManager<CognitoUser> userManager,
)
_userManager = userManager as CognitoUserManager<CognitoUser> as CognitoUserManager<CognitoUser>;
Then assign a hash
var user = _pool.GetUser(Input.UserName);
_userManager.PasswordHasher.HashPassword(user,Input.Password);
var result = await _userManager.CreateAsync(user, Input.Password);
I saw a .NET one suggested here, but here is the variation that worked for me since I couldn't find access to "EncryptionHelper.GetSecretHash":
private string GetHMAC(string text, string key)
{
// TODO: null checks or whatever you want on your inputs...
using (var hmacsha256 = new HMACSHA256(Encoding.UTF8.GetBytes(key)))
{
var hash = hmacsha256.ComputeHash(Encoding.UTF8.GetBytes(text));
return Convert.ToBase64String(hash);
}
}
And you call this for something like a sign up request as follows:
SignUpRequest signUpRequest = new SignUpRequest
{
ClientId = "<your_client_app_id>",
Password = "<the-password-your-user-wanted>",
Username = "<the-username-your-user-wanted",
};
// TODO: add whatever else you need to on your sign up request (like email, phone number etc...)
// and the magic line right here:
signUpRequest.SecretHash = GetHMAC(
signUpRequest.Username + "<your_client_app_id>",
"<your_client_app_secret>");
SignUpResponse response = await _provider.SignUpAsync(signUpRequest);
For me this worked like a charm. I originally was putting the client app secret directly assigned to this "SecretHash" property, but from scanning the rest of the answers here, I realized I truly needed to hash some data using that key as an input to the hash.