My question is this: how do I call a Firebase Cloud Function from a Cloud task and pass a payload through?
I tried following the tutorial here. The only difference is that I'm using Cloud functions for Firebase instead of regular Cloud Functions.
Here is my cloud function.
const functions = require("firebase-functions");
exports.myFunction = functions.https.onRequest((req, res) => {
console.log(req.query);
res.send('success');
});
When I query the url in the browser with parameters ?myparams=data I can log 'data' so I know the cloud function is basically working.
But when I try to call it from my queue (below) I get:
SyntaxError: Unexpected token o in JSON at position 1
at JSON.parse (<anonymous>)
My guess is that req is undefined.
I've been looking at this SO question and I am wondering if it has something to do with needing to use bodyParser for onRequest functions.
HTTP Event Cloud Function: request body value is undefined
I'm also seeing that some people have CORS issues with their cloud functions, which seems like it might be related.
Here is the task queue code that should be sending the payload.
const seconds = 5;
const project = 'xxxxx-xxxxxxx';
const queue = 'xxxxx';
const location = 'us-west2';
const url = 'https://us-central1-xxxxx-xxxxx.cloudfunctions.net/writeDB';
const payload = 'My data';
const parent = client.queuePath(project, location, queue);
const task = {
httpRequest: {
httpMethod: "POST",
url: url,
body: Buffer.from(JSON.stringify(payload)).toString("base64"),
headers: {
"Content-Type": "application/json"
},
oidcToken: {
serviceAccountEmail
}
}
};
task.scheduleTime = {
seconds: seconds + Date.now() / 1000,
};
const request = {parent: parent, task: task};
await client.createTask(request)
.then(response => {
const task = response[0].name;
console.log(`Created task ${task}`);
return {'Response': String(response)}
})
.catch(err => {
console.error(`Error in createTask: ${err.message || err}`);
next()
});
It calls the function, but for some reason it results in the error and the payload isn't logged.
Can anyone help?
As always, I'm happy to clarify the question if anything is unclear. Thanks!
I was able to replicate your error and I managed to fix it by changing the content type headers from "application/json" to "text/plain". I have also removed the JSON.stringify() function in the body value because your payload variable is a String type. Below is my modified sample of your code:
const {CloudTasksClient} = require('#google-cloud/tasks');
// Instantiates a client.
const client = new CloudTasksClient();
const seconds = 5;
const serviceAccountEmail = "xxxx-xxxxx-xxxxxx#appspot.gserviceaccount.com";
const project = 'xxxx-xxxxxx';
const queue = "xx-xxxxx";
const location = 'us-central1';
const url = "https://us-central1-xxxxx-xxxxx.cloudfunctions.net/myFunction";
const payload = 'My Data';
const parent = client.queuePath(project, location, queue);
async function quickstart() {
const task = {
httpRequest: {
httpMethod: "POST",
url: url,
body: Buffer.from(payload).toString("base64"), // your previous code: body: Buffer.from(JSON.stringify(payload)).toString("base64"),
headers: {
"Content-Type": "text/plain"
},
oidcToken: {
serviceAccountEmail
}
}
};
task.scheduleTime = {
seconds: seconds + Date.now() / 1000,
};
const request = {parent: parent, task: task};
await client.createTask(request)
.then(response => {
const task = response[0].name;
console.log(`Created task ${task}`);
return {'Response': String(response)}
})
.catch(err => {
console.error(`Error in createTask: ${err.message || err}`);
next()
});
}
quickstart();
In Cloud Functions, I changed req.query to req.body to get the result from Cloud Tasks
const functions = require("firebase-functions");
exports.myFunction = functions.https.onRequest((req, res) => {
console.log(req.body);
console.log('success')
res.send('success');
});
My client has a GraphQL API running on Google cloud run.
I have recieved a service account for authentication as well as access to the gcloud command line tool.
When using gcloud command line like so:
gcloud auth print-identity-token
I can generate a token that can be used to make post requests to the api. This works and I can make successful post requests to the api from postman, insomnia and from my nodejs app.
However, when I use JWT authentication with "googleapis" or "google-auth" npm libraries like so :
var { google } = require('googleapis')
let privatekey = require('./auth/google/service-account.json')
let jwtClient = new google.auth.JWT(
privatekey.client_email,
null,
privatekey.private_key,
['https://www.googleapis.com/auth/cloud-platform']
)
jwtClient.authorize(function(err, _token) {
if (err) {
console.log(err)
return err
} else {
console.log('token obj:', _token)
}
})
This outputs a "bearer" token:
token obj: {
access_token: 'ya29.c.Ko8BvQcMD5zU-0raojM_u2FZooWMyhB9Ni0Yv2_dsGdjuIDeL1tftPg0O17uFrdtkCuJrupBBBK2IGfUW0HGtgkYk-DZiS1aKyeY9wpXTwvbinGe9sud0k1POA2vEKiGONRqFBSh9-xms3JhZVdCmpBi5EO5aGjkkJeFI_EBry0E12m2DTm0T_7izJTuGQ9hmyw',
token_type: 'Bearer',
expiry_date: 1581954138000,
id_token: undefined,
refresh_token: 'jwt-placeholder'
}
however this bearer token does not work as the one above and always gives an "unauthorised error 401" when making the same requests as with the gcloud command "gcloud auth print-identity-token".
Please help, I am not sure why the first bearer token works but the one generated with JWT does not.
EDIT
I have also tried to get an identity token instead of an access token like so :
let privatekey = require('./auth/google/service-account.json')
let jwtClient = new google.auth.JWT(
privatekey.client_email,
null,
privatekey.private_key,
[]
)
jwtClient
.fetchIdToken('https://my.audience.url')
.then((res) => console.log('res:', res))
.catch((err) => console.log('err', err))
This prints an identity token, however, using this also just gives a "401 unauthorised" message.
Edit to show how I am calling the endpoint
Just a side note, any of these methods below work with the command line identity token, however when generated via JWT, it returns a 401
Method 1:
const client = new GraphQLClient(baseUrl, {
headers: {
Authorization: 'Bearer ' + _token.id_token
}
})
const query = `{
... my graphql query goes here ...
}`
client
.request(query)
.then((data) => {
console.log('result from query:', data)
res.send({ data })
return 0
})
.catch((err) => {
res.send({ message: 'error ' + err })
return 0
})
}
Method 2 (using the "authorized" client I have created with google-auth):
const res = await client.request({
url: url,
method: 'post',
data: `{
My graphQL query goes here ...
}`
})
console.log(res.data)
}
Here is an example in node.js that correctly creates an Identity Token with the correct audience for calling a Cloud Run or Cloud Functions service.
Modify this example to fit the GraphQLClient. Don't forget to include the Authorization header in each call.
// This program creates an OIDC Identity Token from a service account
// and calls an HTTP endpoint with the Identity Token as the authorization
var { google } = require('googleapis')
const request = require('request')
// The service account JSON key file to use to create the Identity Token
let privatekey = require('/config/service-account.json')
// The HTTP endpoint to call with an Identity Token for authorization
// Note: This url is using a custom domain. Do not use the same domain for the audience
let url = 'https://example.jhanley.dev'
// The audience that this ID token is intended for (example Google Cloud Run service URL)
// Do not use a custom domain name, use the Assigned by Cloud Run url
let audience = 'https://example-ylabperdfq-uc.a.run.app'
let jwtClient = new google.auth.JWT(
privatekey.client_email,
null,
privatekey.private_key,
audience
)
jwtClient.authorize(function(err, _token) {
if (err) {
console.log(err)
return err
} else {
// console.log('token obj:', _token)
request(
{
url: url,
headers: {
"Authorization": "Bearer " + _token.id_token
}
},
function(err, response, body) {
if (err) {
console.log(err)
return err
} else {
// console.log('Response:', response)
console.log(body)
}
}
);
}
})
You can find the official documentation for node OAuth2
A complete OAuth2 example:
const {OAuth2Client} = require('google-auth-library');
const http = require('http');
const url = require('url');
const open = require('open');
const destroyer = require('server-destroy');
// Download your OAuth2 configuration from the Google
const keys = require('./oauth2.keys.json');
/**
* Start by acquiring a pre-authenticated oAuth2 client.
*/
async function main() {
const oAuth2Client = await getAuthenticatedClient();
// Make a simple request to the People API using our pre-authenticated client. The `request()` method
// takes an GaxiosOptions object. Visit https://github.com/JustinBeckwith/gaxios.
const url = 'https://people.googleapis.com/v1/people/me?personFields=names';
const res = await oAuth2Client.request({url});
console.log(res.data);
// After acquiring an access_token, you may want to check on the audience, expiration,
// or original scopes requested. You can do that with the `getTokenInfo` method.
const tokenInfo = await oAuth2Client.getTokenInfo(
oAuth2Client.credentials.access_token
);
console.log(tokenInfo);
}
/**
* Create a new OAuth2Client, and go through the OAuth2 content
* workflow. Return the full client to the callback.
*/
function getAuthenticatedClient() {
return new Promise((resolve, reject) => {
// create an oAuth client to authorize the API call. Secrets are kept in a `keys.json` file,
// which should be downloaded from the Google Developers Console.
const oAuth2Client = new OAuth2Client(
keys.web.client_id,
keys.web.client_secret,
keys.web.redirect_uris[0]
);
// Generate the url that will be used for the consent dialog.
const authorizeUrl = oAuth2Client.generateAuthUrl({
access_type: 'offline',
scope: 'https://www.googleapis.com/auth/userinfo.profile',
});
// Open an http server to accept the oauth callback. In this simple example, the
// only request to our webserver is to /oauth2callback?code=<code>
const server = http
.createServer(async (req, res) => {
try {
if (req.url.indexOf('/oauth2callback') > -1) {
// acquire the code from the querystring, and close the web server.
const qs = new url.URL(req.url, 'http://localhost:3000')
.searchParams;
const code = qs.get('code');
console.log(`Code is ${code}`);
res.end('Authentication successful! Please return to the console.');
server.destroy();
// Now that we have the code, use that to acquire tokens.
const r = await oAuth2Client.getToken(code);
// Make sure to set the credentials on the OAuth2 client.
oAuth2Client.setCredentials(r.tokens);
console.info('Tokens acquired.');
resolve(oAuth2Client);
}
} catch (e) {
reject(e);
}
})
.listen(3000, () => {
// open the browser to the authorize url to start the workflow
open(authorizeUrl, {wait: false}).then(cp => cp.unref());
});
destroyer(server);
});
}
main().catch(console.error);
Edit
Another example for cloud run.
// sample-metadata:
// title: ID Tokens for Cloud Run
// description: Requests a Cloud Run URL with an ID Token.
// usage: node idtokens-cloudrun.js <url> [<target-audience>]
'use strict';
function main(
url = 'https://service-1234-uc.a.run.app',
targetAudience = null
) {
// [START google_auth_idtoken_cloudrun]
/**
* TODO(developer): Uncomment these variables before running the sample.
*/
// const url = 'https://YOUR_CLOUD_RUN_URL.run.app';
const {GoogleAuth} = require('google-auth-library');
const auth = new GoogleAuth();
async function request() {
if (!targetAudience) {
// Use the request URL hostname as the target audience for Cloud Run requests
const {URL} = require('url');
targetAudience = new URL(url).origin;
}
console.info(
`request Cloud Run ${url} with target audience ${targetAudience}`
);
const client = await auth.getIdTokenClient(targetAudience);
const res = await client.request({url});
console.info(res.data);
}
request().catch(err => {
console.error(err.message);
process.exitCode = 1;
});
// [END google_auth_idtoken_cloudrun]
}
const args = process.argv.slice(2);
main(...args);
For those of you out there that do not want to waste a full days worth of work because of the lack of documentation. Here is the accepted answer in today's world since the JWT class does not accept an audience in the constructor anymore.
import { JWT } from "google-auth-library"
const client = new JWT({
forceRefreshOnFailure: true,
key: service_account.private_key,
email: service_account.client_email,
})
const token = await client.fetchIdToken("cloud run endpoint")
const { data } = await axios.post("cloud run endpoint"/path, payload, {
headers: {
Authorization: `Bearer ${token}`
}
})
return data
I have configured an ASOS OpenIdConnect Server using and an asp.net core mvc app that uses the "Microsoft.AspNetCore.Authentication.OpenIdConnect": "1.0.0 and "Microsoft.AspNetCore.Authentication.Cookies": "1.0.0". I have tested the "Authorization Code" workflow and everything works.
The client web app processes the authentication as expected and creates a cookie storing the id_token, access_token, and refresh_token.
How do I force Microsoft.AspNetCore.Authentication.OpenIdConnect to request a new access_token when it expires?
The asp.net core mvc app ignores the expired access_token.
I would like to have openidconnect see the expired access_token then make a call using the refresh token to get a new access_token. It should also update the cookie values. If the refresh token request fails I would expect openidconnect to "sign out" the cookie (remove it or something).
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
AuthenticationScheme = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
ClientId = "myClient",
ClientSecret = "secret_secret_secret",
PostLogoutRedirectUri = "http://localhost:27933/",
RequireHttpsMetadata = false,
GetClaimsFromUserInfoEndpoint = true,
SaveTokens = true,
ResponseType = OpenIdConnectResponseType.Code,
AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet,
Authority = http://localhost:27933,
MetadataAddress = "http://localhost:27933/connect/config",
Scope = { "email", "roles", "offline_access" },
});
It seems there is no programming in the openidconnect authentication for asp.net core to manage the access_token on the server after received.
I found that I can intercept the cookie validation event and check if the access token has expired. If so, make a manual HTTP call to the token endpoint with the grant_type=refresh_token.
By calling context.ShouldRenew = true; this will cause the cookie to be updated and sent back to the client in the response.
I have provided the basis of what I have done and will work to update this answer once all work as been resolved.
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
AuthenticationScheme = "Cookies",
ExpireTimeSpan = new TimeSpan(0, 0, 20),
SlidingExpiration = false,
CookieName = "WebAuth",
Events = new CookieAuthenticationEvents()
{
OnValidatePrincipal = context =>
{
if (context.Properties.Items.ContainsKey(".Token.expires_at"))
{
var expire = DateTime.Parse(context.Properties.Items[".Token.expires_at"]);
if (expire > DateTime.Now) //TODO:change to check expires in next 5 mintues.
{
logger.Warn($"Access token has expired, user: {context.HttpContext.User.Identity.Name}");
//TODO: send refresh token to ASOS. Update tokens in context.Properties.Items
//context.Properties.Items["Token.access_token"] = newToken;
context.ShouldRenew = true;
}
}
return Task.FromResult(0);
}
}
});
You must enable the generation of refresh_token by setting in startup.cs:
Setting values to AuthorizationEndpointPath = "/connect/authorize"; // needed for refreshtoken
Setting values to TokenEndpointPath = "/connect/token"; // standard token endpoint name
In your token provider, before validating the token request at the end of the HandleTokenrequest method, make sure you have set the offline scope:
// Call SetScopes with the list of scopes you want to grant
// (specify offline_access to issue a refresh token).
ticket.SetScopes(
OpenIdConnectConstants.Scopes.Profile,
OpenIdConnectConstants.Scopes.OfflineAccess);
If that is setup properly, you should receive a refresh_token back when you login with a password grant_type.
Then from your client you must issue the following request (I'm using Aurelia):
refreshToken() {
let baseUrl = yourbaseUrl;
let data = "client_id=" + this.appState.clientId
+ "&grant_type=refresh_token"
+ "&refresh_token=myRefreshToken";
return this.http.fetch(baseUrl + 'connect/token', {
method: 'post',
body : data,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json'
}
});
}
and that's it, make sure that your auth provider in HandleRequestToken is not trying to manipulate the request that is of type refresh_token:
public override async Task HandleTokenRequest(HandleTokenRequestContext context)
{
if (context.Request.IsPasswordGrantType())
{
// Password type request processing only
// code that shall not touch any refresh_token request
}
else if(!context.Request.IsRefreshTokenGrantType())
{
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "Invalid grant type.");
return;
}
return;
}
The refresh_token shall just be able to pass through this method and is handled by another piece of middleware that handles refresh_token.
If you want more in depth knowledge about what the auth server is doing, you can have a look at the code of the OpenIdConnectServerHandler:
https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server/blob/master/src/AspNet.Security.OpenIdConnect.Server/OpenIdConnectServerHandler.Exchange.cs
On the client side you must also be able to handle the auto refresh of the token, here is an example of an http interceptor for Angular 1.X, where one handles 401 reponses, refresh the token, then retry the request:
'use strict';
app.factory('authInterceptorService',
['$q', '$injector', '$location', 'localStorageService',
function ($q, $injector, $location, localStorageService) {
var authInterceptorServiceFactory = {};
var $http;
var _request = function (config) {
config.headers = config.headers || {};
var authData = localStorageService.get('authorizationData');
if (authData) {
config.headers.Authorization = 'Bearer ' + authData.token;
}
return config;
};
var _responseError = function (rejection) {
var deferred = $q.defer();
if (rejection.status === 401) {
var authService = $injector.get('authService');
console.log("calling authService.refreshToken()");
authService.refreshToken().then(function (response) {
console.log("token refreshed, retrying to connect");
_retryHttpRequest(rejection.config, deferred);
}, function () {
console.log("that didn't work, logging out.");
authService.logOut();
$location.path('/login');
deferred.reject(rejection);
});
} else {
deferred.reject(rejection);
}
return deferred.promise;
};
var _retryHttpRequest = function (config, deferred) {
console.log('autorefresh');
$http = $http || $injector.get('$http');
$http(config).then(function (response) {
deferred.resolve(response);
},
function (response) {
deferred.reject(response);
});
}
authInterceptorServiceFactory.request = _request;
authInterceptorServiceFactory.responseError = _responseError;
authInterceptorServiceFactory.retryHttpRequest = _retryHttpRequest;
return authInterceptorServiceFactory;
}]);
And here is an example I just did for Aurelia, this time I wrapped my http client into an http handler that checks if the token is expired or not. If it is expired it will first refresh the token, then perform the request. It uses a promise to keep the interface with the client-side data services consistent. This handler exposes the same interface as the aurelia-fetch client.
import {inject} from 'aurelia-framework';
import {HttpClient} from 'aurelia-fetch-client';
import {AuthService} from './authService';
#inject(HttpClient, AuthService)
export class HttpHandler {
constructor(httpClient, authService) {
this.http = httpClient;
this.authService = authService;
}
fetch(url, options){
let _this = this;
if(this.authService.tokenExpired()){
console.log("token expired");
return new Promise(
function(resolve, reject) {
console.log("refreshing");
_this.authService.refreshToken()
.then(
function (response) {
console.log("token refreshed");
_this.http.fetch(url, options).then(
function (success) {
console.log("call success", url);
resolve(success);
},
function (error) {
console.log("call failed", url);
reject(error);
});
}, function (error) {
console.log("token refresh failed");
reject(error);
});
}
);
}
else {
// token is not expired, we return the promise from the fetch client
return this.http.fetch(url, options);
}
}
}
For jquery you can look a jquery oAuth:
https://github.com/esbenp/jquery-oauth
Hope this helps.
Following on from #longday's answer, I have had success in using this code to force a client refresh without having to manually query an open id endpoint:
OnValidatePrincipal = context =>
{
if (context.Properties.Items.ContainsKey(".Token.expires_at"))
{
var expire = DateTime.Parse(context.Properties.Items[".Token.expires_at"]);
if (expire > DateTime.Now) //TODO:change to check expires in next 5 mintues.
{
context.ShouldRenew = true;
context.RejectPrincipal();
}
}
return Task.FromResult(0);
}
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.