I am trying to list the matching users from cognito userpool using expressJs api to check if the username already exists or not.
Code:
listUserFromUserpool = async () => {
var params = {
UserPoolId: process.env.USER_POOL_ID /* required */,
AttributesToGet: ["username"],
Filter: 'username = "example"',
Limit: 5,
PaginationToken: "What to pass here initially ?",
};
try {
let data = await this.cognitoIdentity.listUsers(params).promise();
return data;
} catch (error) {
console.log(error);
return false;
}
};
But getting error in PaginationToken . What should i insert into the PaginationToken param initially as I get the PaginationToken in the next response?
Or, is there any way to get single user without using pagination?
I have followed the example in Display the Sign In With Google button to get a Google sign in button working in my Angular application:
<div id="g_id_onload"
class="mt-3"
data-client_id="XXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.apps.googleusercontent.com"
data-login_uri="http://localhost:1337/login/google"
data-auto_prompt="false">
</div>
<div class="g_id_signin"
data-width="250"
data-type="standard"
data-size="large"
data-theme="outline"
data-text="continue_with"
data-shape="rectangular"
data-logo_alignment="center">
</div>
Once the user signs in, I verify and decode the JWT token provided by Google in my Express server using jsonwebtoken:
app.post('/login/google', express.urlencoded(), async(request, response, next) => {
try {
console.log(`${request.method} ${request.url} was called.`);
let token: string = request.body.credential;
let body: Response = await fetch('https://www.googleapis.com/oauth2/v1/certs', { method: 'GET', headers: { Accept: 'application/json' }});
let json: any = await body.json();
let certificates: string[] = Object.keys(json).map(key => json[key]);
let decoded: any;
let lastError: any;
certificates.every(certificate => {
try {
decoded = jwt.verify(token, certificate, { algorithms: ['RS256'], ignoreExpiration: false });
}
catch (error) {
lastError = error;
}
return !decoded;
});
if (!decoded)
throw lastError;
}
catch (error) {
next(error);
}
});
The problem is that the decoded token does not contain the user's gender or birthday information. How can I obtain this data?
I have just recently tried manually appending the https://www.googleapis.com/auth/user.birthday.read and https://www.googleapis.com/auth/user.gender.read scopes to my application's OAuth Consent Screen found at https://console.cloud.google.com/apis/credentials/consent/edit, but I don't see the user being prompted to provide this data to my application when it runs. I tried deleting permissions to my application from my account at accounts.google.com (under the Third-Party Access section) as well in hopes that it might prompt for these extra pieces of data. I am not sure at this point how to go about getting this extra data because I can't seem to find a good documentation piece on how to achieve this. Also, I wanted to add that my test account's Gender and Birthday information is set to be Private in https://myaccount.google.com/personal-info. I was wondering if its possible to fetch these private scopes somehow.
So, just to be clear, when I try to sign in I still only get the following prompt, which makes me believe that something is wrong and its not actually requesting the scope for birthday and gender from the user:
Confirm you want to sign in to [Application Name] with [User's Name].
To create your account, Google will share your name, email address,
and profile picture with [Application Name].
I also tried going on https://developers.google.com/oauthplayground/ and I pasted this in for Input your own scopes: https://www.googleapis.com/auth/userinfo.email,https://www.googleapis.com/auth/userinfo.profile,https://www.googleapis.com/auth/user.birthday.read,https://www.googleapis.com/auth/user.gender.read. I then hit the Authorize API button, logged in, granted access to these scopes (was prompted correctly on the playground), performed the token exchange, then I tried to List possible operations and under the People API, I called the get people endpoint, and modified the URI to https://people.googleapis.com/v1/people/me as per the documentation. This endpoint seems to work to fetch the data I need, but now I can't seem to wrap my head around what authorization parameters to use for this endpoint from the data I get back from the POST to my Express server. I have also tried enabling the People API from Enabled APIs & services.
You are using signin. Signin is open id connect and returns an id token. Id tokes contain very few claims. Gender is not one of them.
The only way to get access to the full user profile info is to go though the people api as you have mentioned.
You can use the try me to see it working and generate the sample for you.
<script src="https://apis.google.com/js/api.js"></script>
<script>
/**
* Sample JavaScript code for people.people.get
* See instructions for running APIs Explorer code samples locally:
* https://developers.google.com/explorer-help/code-samples#javascript
*/
function authenticate() {
return gapi.auth2.getAuthInstance()
.signIn({scope: "https://www.googleapis.com/auth/contacts https://www.googleapis.com/auth/contacts.readonly https://www.googleapis.com/auth/directory.readonly https://www.googleapis.com/auth/user.addresses.read https://www.googleapis.com/auth/user.birthday.read https://www.googleapis.com/auth/user.emails.read https://www.googleapis.com/auth/user.gender.read https://www.googleapis.com/auth/user.organization.read https://www.googleapis.com/auth/user.phonenumbers.read https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile"})
.then(function() { console.log("Sign-in successful"); },
function(err) { console.error("Error signing in", err); });
}
function loadClient() {
gapi.client.setApiKey("YOUR_API_KEY");
return gapi.client.load("https://people.googleapis.com/$discovery/rest?version=v1")
.then(function() { console.log("GAPI client loaded for API"); },
function(err) { console.error("Error loading GAPI client for API", err); });
}
// Make sure the client is loaded and sign-in is complete before calling this method.
function execute() {
return gapi.client.people.people.get({
"resourceName": "people/me",
"personFields": "genders"
})
.then(function(response) {
// Handle the results here (response.result has the parsed body).
console.log("Response", response);
},
function(err) { console.error("Execute error", err); });
}
gapi.load("client:auth2", function() {
gapi.auth2.init({client_id: "YOUR_CLIENT_ID"});
});
</script>
<button onclick="authenticate().then(loadClient)">authorize and load</button>
<button onclick="execute()">execute</button>
The issue that you are then going to have is the above sample uses Oauth2 and not open id connect (signin) It needs an access token to work. If you check your code I belive that the signin does return an access token. Your job then is to feed the access token to the code above so that you dont have to go though the authorization process again.
So far i have not found anyone able to link the new signin system with the old oauth2 system. If you get it to work i would love to see it.
Html
To call this api you need an access_token. a google access token is not a jwt. it is not the id_token
GET https://people.googleapis.com/v1/people/me?personFields=genders&key=[YOUR_API_KEY] HTTP/1.1
Authorization: Bearer [YOUR_ACCESS_TOKEN]
Accept: application/json
I finally managed to get it working with the help of this guide.
I had to scrap the idea of using the Google sign in button because it does not seem to allow extended scopes such as birthday and gender (well, not if they're private anyways - if anyone finds a way of doing it with the sign in button, please post an answer). Luckily, their OAuth API does support extended scopes. As such, I've implemented my own Google sign in button using the googleapis package.
There are a few steps to this:
Use the googleapis package to generate a URI to present to the user that will ask them to consent to gender and birthday access.
For example:
app.get('/login/google/uri', async(request, response, next) => {
try {
console.log(`${request.method} ${request.url} was called.`);
let client = new google.auth.OAuth2(
'ClientID',
'ClientSecret',
`http://localhost:4200/login/google/redirect`
);
const scopes = [
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/user.birthday.read',
'https://www.googleapis.com/auth/user.gender.read'
];
const authorizationUrl: string = client.generateAuthUrl({
access_type: 'offline',
scope: scopes,
include_granted_scopes: false
});
response.status(200).send({ uri: authorizationUrl });
}
catch (error) {
next(error);
}
});
Ensure that http://localhost:4200/login/google/redirect (or whatever redirect URI you use) is part of your OAuth 2.0 Client ID Credential's Authorized redirect URIs in the console.
Google will redirect to your redirect URI (http://localhost:4200/login/google/redirect) with a query parameter named code. For example: http://localhost:4200/login/google/redirect?code=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX&scope=email%20profile%20https:%2F%2Fwww.googleapis.com%2Fauth%2Fuser.gender.read%20https:%2F%2Fwww.googleapis.com%2Fauth%2Fuser.birthday.read%20https:%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email%20https:%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile%20openid&authuser=0&prompt=consent
Take the code (XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX) and exchange it for an access token.
For example:
let client = new google.auth.OAuth2(
'ClientID',
'ClientSecret',
`http://localhost:4200/login/google/redirect`
);
let code: string = request.params.code;
let { tokens } = await client.getToken(code);
console.log(tokens.access_token);
Use the access_token (it looks something like XXXX.XXXXXXXXXXXXXXXXXXXXXXXXXXX-XXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX) when making requests to the People API and set it in the Authorization header as the bearer token.
For example:
curl "https://people.googleapis.com/v1/people/me?key=XXXXXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXX&personFields=genders,birthdays" -H "Authorization: Bearer XXXX.XXXXXXXXXXXXXXXXXXXXXXXXXXX-XXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
key is your API key from the console (you can create one and restrict it to the People API - if you don't see the People API as a restriction option you might need to enable it from the Enabled APIs and services tab). I'm sure there is a more API friendly way of making this request in the googleapis package that you can explore, but I just wanted to highlight how it works with curl.
The response you will see should be like this:
{
"resourceName": "people/XXXXXXXXXXXXXXXXXXXX",
"etag": "XXXXXXXXXXXXXXXXXXXXX",
"genders": [
{
"metadata": {
"primary": true,
"source": {
"type": "PROFILE",
"id": "XXXXXXXXXXXXXXXXXXXX"
}
},
"value": "male",
"formattedValue": "Male"
}
],
"birthdays": [
{
"metadata": {
"primary": true,
"source": {
"type": "ACCOUNT",
"id": "XXXXXXXXXXXXXXXXXXXX"
}
},
"date": {
"year": 1901,
"month": 1,
"day": 1
}
}
]
}
Edit: Just for completion, here is the API friendly way to do all of this.
First, generate this URI and redirect the user to it:
app.get('/login/google/uri', async(request, response, next) => {
try {
console.log(`${request.method} ${request.url} was called.`);
let client = new googleapis.Auth.OAuth2Client(
Globals.GoogleClientID,
Globals.GoogleClientSecret,
`${Globals.UIHost}/login`
);
const scopes = [
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/user.birthday.read',
'https://www.googleapis.com/auth/user.gender.read'
];
const authorizationUrl: string = client.generateAuthUrl({
access_type: 'offline',
scope: scopes,
include_granted_scopes: false
});
response.status(200).send({ uri: authorizationUrl });
}
catch (error) {
next(error);
}
});
Second, after the user has signed in and you get a code posted back to your redirect URI, parse the query param for the code and use it like how I am doing so in the following POST method on my server to get these extra user details for birthdays, genders, and emails:
app.post('/login/google', express.json(), async(request, response, next) => {
try {
console.log(`${request.method} ${request.url} was called.`);
let client = new googleapis.Auth.OAuth2Client(
Globals.GoogleClientID,
Globals.GoogleClientSecret,
`${Globals.UIHost}/login`
);
let code: string = request.body.code;
let { tokens } = await client.getToken(code);
let accessToken: string = tokens.access_token;
client.setCredentials({ access_token: accessToken });
let people = new googleapis.people_v1.People({});
let result = await people.people.get({
resourceName: 'people/me',
personFields: 'emailAddresses,birthdays,genders',
auth: client
});
console.log(result.data);
}
catch (error) {
next(error);
}
});
result.data should contain the information.
If you are using NestJS with typescript, this worked for me
#Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
constructor(configService: ConfigService) {
super({
clientID: configService.get('GOOGLE_CLIENT_ID'),
clientSecret: configService.get('GOOGLE_SECRET'),
callbackURL: configService.get('GOOGLE_REDIRECT_URL'),
scope: [
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/plus.login',
'https://www.googleapis.com/auth/user.birthday.read',
'https://www.googleapis.com/auth/user.phonenumbers.read',
'https://www.googleapis.com/auth/user.gender.read',
],
});
}
async validate(
accessToken: string,
refreshToken: string,
profile: any,
done: VerifyCallback,
): Promise<any> {
const { name, emails, photos, sub, birthday, phoneNumber, gender } =
profile;
const user = {
sub,
email: emails[0].value,
firstName: name.givenName,
lastName: name.familyName,
picture: photos[0].value,
dob: birthday,
phoneNumber,
gender,
refreshToken,
accessToken,
};
done(null, user);
}
}
Then add GoogleStrategy to your provider. Of course, don't forget your keys in your .env file.
I am using http cloud function ( https://firebase.google.com/docs/functions/http-events ) to write documents to a firestore collection:
exports.hello = functions.https.onRequest(
(req: { query: { name: string } }, res: { send: (arg0: string) => void }) => {
console.log(req.query.name);
var name = req.query.name || 'unknown';
res.send('hello' + name);
admin
.firestore()
.collection('ulala')
.doc()
.set({ token: 'asd' }, { merge: true });
}
);
this is a test. The problem is that, once you deploy and get the link to the function, it is executable by everyone. I would like instead that only I (project owner) can use it . Is it possible to do this?
One possible solution is to restrict your HTTPS Cloud Function to only a specific "Admin" user of your app.
There is an official Cloud Function sample which shows how to restrict an HTTPS Function to only the Firebase users of the app/Firebase project: Authorized HTTPS Endpoint sample.
You need to adapt it to check if the user is the Admin user. For example by checking the userID in the try/catch block at line 60 of the index.js file (untested).
try {
const decodedIdToken = await admin.auth().verifyIdToken(idToken);
if (decodedToken.uid !== "<the admin user uid>") {
throw new Error("Wrong user");
} else {
req.user = decodedIdToken;
next();
}
return;
} catch (error) {
functions.logger.error('Error while verifying Firebase ID token:', error);
res.status(403).send('Unauthorized');
return;
}
The two drawbacks of this approach are:
Your Admin user needs to be declared as a Firebase project user in the Authentication service
You hard code the Admin userID in your Cloud Function (you could use the Google Cloud Secret Manager service to securely store it as a configuration value, see the doc).
IMPORTANT SIDE NOTE:
In your Cloud Function you call the send() method before the asynchronous work is complete:
res.send('hello' + name);
admin
.firestore()
.collection('ulala')
.doc()
.set({ token: 'asd' }, { merge: true });
By calling the send() method, you actually terminate the Cloud Function, indicating to the Cloud Functions instance running your function that it can shut down. Therefore in the majority of the cases the asynchronous set() operation will not be executed.
You need to do as follows:
admin
.firestore()
.collection('ulala')
.doc()
.set({ token: 'asd' }, { merge: true })
.then(() => {
res.send('hello' + name);
})
I would suggest you watch the 3 videos about "JavaScript Promises" from the Firebase video series as well as read this page of the documentation which explain this key point.
I am implementing a cloud function for updating the current user's password.
Basically, the logic I want to follow is:
(Client side)
0. Complete form and submit the data (current password and new password).
(Backend)
1. Get the current user email from the callable function context.
2. Re-authenticate the current user using the provided current password.
2.1. If success, change the password and send a notification email.
2.2. Else, throw an error.
Here is my current code:
const { auth, functions } = require("../../services/firebase");
...
exports.updatePassword = functions
.region("us-central1")
.runWith({ memory: "1GB", timeoutSeconds: 120 })
.https.onCall(async (data, context) => {
const { currentPassowrd, newPassword } = data;
const { email, uid: userId } = context.auth.token;
if (!userId) {
// throw ...
}
try {
//
// Problem: `firebase-admin` authentication doesn't include
// the `signInWithEmailAndPassword()` method...
//
await auth.signInWithEmailAndPassword(email, currentPassowrd);
await auth.updateUser(userId, {
password: newPassword,
});
sendPasswordUpdateEmail(email);
} catch (err) {
// ...
throw AuthErrors.cannotUpdatePassword();
}
});
My problem is that the firebase-admin package doesn't include the signInWithEmailAndPassword, and I need a way to handle this, to check that "currentPassword" is correct, inside my function.
My other option, if the one I have described is not possible, is to update the password using the firebase sdk in the client side, and then to call a firebase function to send the notification email.
Strictly speaking you don't need to re-authenticate the user in the Cloud Function: If you get a value for context.auth.uid in your Callable Cloud Function, it means that the user is authenticated in the front-end and you can therefore safely call the updateUser() method.
If you want to deal with the case when the user left his device opened, and someone updates his password, as explained in the comments under your question, I would suggest you use the reauthenticateWithCredential() method in the front-end, which re-authenticates a user using a fresh credential.
Do as follows:
import {
EmailAuthProvider,
getAuth,
reauthenticateWithCredential,
} from 'firebase/auth'
const email = auth.currentUser.email;
// Capture the password value
// e.g. via a pop-up window
const password = ...;
const auth = getAuth();
const credential = EmailAuthProvider.credential(
email,
password
);
await reauthenticateWithCredential(
auth.currentUser,
credential
);
// If no error is thrown, you can call the Callable Cloud Function, knowing the user has just re-signed-in.
I'm trying to establish a federation among Amazon and Salesforce, in this way: if a user correctly authenticates through Salesforce it will see all S3 buckets in the given account.
Quite simple, I followed this blog post and changed something (i.e. I don't use a DyanamoDb table and the callback is for simplicity inside an S3 bucket). The flow that I'm trying to implement is called Enhanced (simplified) flow (details here):
I slightly modified the callback code compared to the article:
function onPageLoad() {
var url = window.location.href;
var match = url.match('id_token=([^&]*)');
var id_token = "";
if (match) {
id_token = match[1];
} else {
console.error("Impossibile recuperare il token");
}
AWS.config.region = "eu-west-1"
const cognitoParams = {
AccountId: "ACC_ID",
IdentityPoolId: "eu-west-1:XXX",
Logins: {
"login.salesforce.com": id_token
}
}
const identity = new AWS.CognitoIdentity()
identity.getId(cognitoParams, function (err, identityData) {
if (err) {
printMessage(err);
return;
}
const identityParams = {
IdentityId: identityData.IdentityId,
Logins: cognitoParams.Logins
}
identity.getCredentialsForIdentity(identityParams, function (err, data) {
if (err) {
printMessage(err);
} else {
var c = {
region: 'eu-west-1',
accessKeyId: data.Credentials.AccessKeyId,
secretAccessKey: data.Credentials.SecretKey
};
var s3 = new AWS.S3(c);
// HERE IS THE ERRORE - data is empty and response contains the error
s3.listBuckets((response, data) => {
data.Buckets.forEach(function (value) { appendMessage(value.Name) })
});
}
});
});
// IRRELEVANT CODE
}
I can get the token from Salesforce, I can get the access and secret keys but when I try to list the buckets I get a laconic:
The AWS Access Key Id you provided does not exist in our records.
I found this error reasonable since I have no user at all and the keys are created on-the-fly. Where can I hit my head? The SDK is 2.103.0.
Could be due to eventual consistency of IAM, can you try to include a delay before calling the listbucket api or make the request to us-east-1 endpoint?
http://docs.aws.amazon.com/IAM/latest/UserGuide/troubleshoot_general.html#troubleshoot_general_access-denied-service2.
GetCredentialsForIdentity returns temporary credentials. So you should include AccessKeyId, SecretKey and SessionToken to make the request.
http://docs.aws.amazon.com/cognitoidentity/latest/APIReference/API_GetCredentialsForIdentity.html
Hope this helps.