I have been using the new GIS library with the One Tap UX. I have followed all the steps outlined in the setup guide and the authentication works as expected, in fact I like the new flow; Nonetheless I am experiencing a very peculiar issue. For some reason, the picture property is missing from the credential response.
A couple of things to note are:
This is an internal application, which means it is only being used by the google workspace users of the respective domain.
This happens with all users whose profile picture is set using the Admin Directory API.
What I have in the front end is the following:
const promptParent = "signInBox";
const gsiInitializeConfig = {
client_id: '4xxyy55zzz.apps.googleusercontent.com',
callback: handleCredentialResponse,
prompt_parent_id: promptParent,
auto_select: true,
cancel_on_tap_outside: false
};
let idClient;
const doSignIn = ()=>{
idClient = google.accounts;
idClient.id.initialize(gsiInitializeConfig);
idClient.id.prompt();
}
const handleCredentialResponse = (response)=>{
const {credential} = response;
//send idToken to the backend for verification
});
When the DOMContent is loaded, I programatically invoke the doSignIn function. And the one tap shows and it works great. It returns the idToken which then I send to the backend for verification. In the backend I am using express and I have the following:
const token = getTokenFromBody();
if(!token){ return unauthenticated(res); }
const auth2Client = new OAuth2Client(GOOGLE_CLIENT_ID);
const ticket = await auth2Client.verifyIdToken({
audience: GOOGLE_CLIENT_ID,
idToken: token
}).catch(error=>{
console.error(`Failed to verify google id token: ${error}`);
unauthorized(res, "Invalid grant");
});
if(!ticket){ return; }
const ticketPayload = ticket.getPayload();
console.log("ticketPayload", JSON.stringify(ticketPayload));
The logged object for the ticketPayload above looks like this:
{
iss: "https://accounts.google.com",
nbf: 1378,
aud: "44xxyy55zz.apps.googleusercontent.com",
sub: "1559234417",
hd: "domain.com",
email: "user#domain.com",
email_verified: true,
azp: "44xxyy55zz.apps.googleusercontent.com",
name: "User Name",
given_name: "User",
family_name: "Name",
iat: 1664828678,
exp: 1664832278,
jti: "f0549d2544c905sadfcbc13110"
}
That is the response for all the users in the domain, however, for my user, whose photo was set using the page "https://myaccount.google.com", the response is the following:
{
iss: "https://accounts.google.com",
nbf: 1378,
aud: "44xxyy55zz.apps.googleusercontent.com",
sub: "1559234417",
hd: "domain.com",
email: "user#domain.com",
email_verified: true,
azp: "44xxyy55zz.apps.googleusercontent.com",
name: "User Name",
picture: "https://lh3.googleusercontent.com/a-/N5pZpE-zbJUg3=s96-c", // <-----!!!
given_name: "User",
family_name: "Name",
iat: 1664828678,
exp: 1664832278,
jti: "f0549d2544c905sadfcbc13110"
}
In comparisson with the old google sign in library, this behavior is different. How can I get the picture property for all users in the domain?
I reached out to Google Workspace support were they educated me with the following:
Admin-set user profile picutres (either set by an Admin via the Admin Console itself, or by the Admin SDK API call) are private and are not returned in the credential response of the Google Sign In flow.
The reason for that is that when an administrator sets a profile photo to a user's account, the photo becomes visible only to users in the organization and to external users they use Google Chat with. In contrast, (and only if users are allowed to manage their own profile photo) user-set photos are public by default. That is explained under the "Where a user's photo appears section of this support article.
This behavior cannot be changed for privacy reasons. An admin-set photo is neither public information, nor is set by the user, hence is not available in the token.
Although the solution provided makes sense, there is something that I feel is wrong. The fact that the old google sign in library never presented this behavior makes me feel that way. Why should the new sign in library present this behavior now? It is really absurd.
Related
I want to set up custom claims to a certain number of users let's say 5 users would be admins on my website. I want these 5 users to be able to log in through the login page which would redirect them to the dashboard.
but I still don't fully understand the concept of the custom claims and how to use them and firebase documentation is limited with examples.
In their example they show that I can pass a uid that I want to assign a custom claim to, but how is this supposed to be a variable when i want certain users uid's from my firestore database Users collection to be admins and have a custom claim, in other words, where would I put this code or how would I assign a custom claim to more than one user at a time and how and where would this code be executed.
if anyone can give me an example of how I would make this work.
here is what I did:
created a firebaseAdmin.js file:
var admin = require("firebase-admin");
// lets say for instance i want these two users to be admins
//2jfow4fd3H2ZqYLWZI2s1YdqOPB42
//2jfow4vad2ZqYLWZI2s1YdqOPB42 what am i supposed to do?
admin
.auth()
.setCustomUserClaims(uid, { admin: true })
.then(() => {
// The new custom claims will propagate to the user's ID token the
// next time a new one is issued.
});
I honestly don't know what to do from here.
Custom Claims can only be set from a privileged server environment via the Firebase Admin SDK. The easiest ways are either using a Node.js script (running the Admin SDK) or a Cloud Function (which also uses the Admin SDK).
Let's look at the example of a Callable Cloud Function that you call from your front-end (and in which you could check the UID of the user who is calling it, i.e. a Super Admin).
exports.setAdminClaims = functions.https.onCall(async (data, context) => {
// If necessary check the uid of the caller, via the context object
const adminUIDs = ['2jfow4fd3H2ZqYLWZI2s1YdqOPB42', '767fjdhshd3H2ZqYLWZI2suyyqOPB42'];
await Promise.all(adminUIDs.map(uid => admin.auth().setCustomUserClaims(uid, { admin: true })));
return { result: "Operation completed" }
});
A Node.js script would be similar:
#!/usr/bin/node
const admin = require('firebase-admin');
admin.initializeApp({
credential: admin.credential.cert(".....json") // See remark on the private key below
});
const adminUIDs = ['2jfow4fd3H2ZqYLWZI2s1YdqOPB42', '767fjdhshd3H2ZqYLWZI2suyyqOPB42'];
Promise.all(adminUIDs.map(uid => admin.auth().setCustomUserClaims(uid, { admin: true })))
.then(() => {
console.log("Operation completed")
})
You must generate a private key file in JSON format for your service account , as detailed in the doc.
Then, when the Claims are set, you can access these Claims in your web app, and adapt the UI (or the navigation flow) based on the fact the user has (or not) the admin claim. More detail here in the doc.
I implemented sending emails via nodemailer.
Now when I create new user, that new user get "welcome email".
Problem is cus that "welcome email" should contain option for
resetting password.
How to add Firebase Resetting link in nodemailer email template?
This is my Email Template code for nodemailer
const output = `
<p>You have access to the Church Mutual Assignment Tool.</p>
<p>Follow this link to create new password for your account ${userRecord.email}:</p>
<a href="${resetPasswordLink}">
${resetPasswordLink}
</a>
<p>Thanks,</p>
<p>Your Church Mutual Assignment Tool team</p>
`
let message = {
from: 'nyik6nntutmq3vz6#ethereal.email',
to: `${user.email}`,
subject: 'Welcome to the Church Mutual Assignment Tool',
text: 'Plaintext version of the message',
html: output
}
This is my Nodemailer code:
var mailer = require('nodemailer')
var mailConfig = {
host: 'smtp.ethereal.email',
port: 587,
auth: {
user: 'nyik6nntutmq3vz6#ethereal.email',
pass: '3cbRjkZdPquDqA725s'
}
}
var transporter = mailer.createTransport(mailConfig)
module.exports = transporter
The Admin SDK now has some methods that allow you to do just this exact thing. Check out the docs on the email action links, specifically the "Generate password reset email link" section.
// Admin SDK API to generate the password reset link.
const email = 'user#example.com';
admin.auth().generatePasswordResetLink(email, actionCodeSettings)
.then((link) => {
// Do stuff with link here
})
.catch((error) => {
// Some error occurred.
});
Full disclosure - I haven't actually used any of those functions, and I'm a little concerned that the page in question refers a lot to mobile apps - so you might have to pass it the mobile app config.
const actionCodeSettings = {
// URL you want to redirect back to. The domain (www.example.com) for
// this URL must be whitelisted in the Firebase Console.
url: 'https://www.example.com/checkout?cartId=1234',
// This must be true for email link sign-in.
handleCodeInApp: true,
iOS: {
bundleId: 'com.example.ios'
},
android: {
packageName: 'com.example.android',
installApp: true,
minimumVersion: '12'
},
// FDL custom domain.
dynamicLinkDomain: 'coolapp.page.link'
};
On the other hand, the page also says these features provide the ability to:
Ability to customize how the link is to be opened, through a mobile
app or a browser, and how to pass additional state information, etc.
Which sounds promising, allowing it to open in the browser... but if you are developing for web - and the function errors out when not provided iOS/Android information... then I'm afraid you'll have to do it the old fashioned approach and create your own implementation... but I'm leaning towards this .generatePasswordResetLink should work for you.
I have an Express web application on a Node.js server. It supports sending email notifications for registration, password reset, etc. For this, I use Nodemailer and XOAuth2 to connect to Google API using a normal Google account. The connection works well and emails are being sent as expected. But after a while (1-3 days), Google sends me a message saying my account "has been suspended because of a violation of our Terms of Service" (screenshot). So I have to manually recover the account using the link in the notification and then the emails that were blocked during suspension are sent.
After getting 3 of those notifications, I noticed that they follow only if I triggered my email sending function (see below)
I tried to log XOAuth2's token refresh and seems like it works
I tried to renew the refreshToken from the playground several times but the problem remains
IMHO, it might be that I'm using a free account and Google wants me to pay for using its API, it thinks I do some kind spams, or it just doesn't want me to use its API. I'm I wrong?
So my questions are:
Why does my account get suspended?
What can I do to fix this problem?
Code details (approximate):
var bluebird = require('bluebird');
var nodemailer = require('nodemailer');
var xoauth2 = require('xoauth2');
// clientId, clientSecret taken from: https://console.developers.google.com/
// Create credentials > OAuth client ID > Web application >
// Name = Nodemailer,
// Authorised redirect URIs = https://developers.google.com/oauthplayground
//
// refreshToken taken from: https://developers.google.com/oauthplayground/
// Access token location: Autorization header w/ Bearer prefix
// Access type: Offline
// Force prompt: Consent Screen
// Use your own OAuth credentials: yes
// Authorized APIs: everything inside Gmail API v1
// Auto refresh the token after it expires: yes
xoauth2Gen = xoauth2.createXOAuth2Generator({
user: 'example#gmail.com', // my real google account
clientId: '84037...t.com', // my real clientId
clientSecret: 'c3Yo...KP', // my real clientSecret
refreshToken: '1/ex...Wk' // my real refreshToken
// nothing more here
});
var mail_transport: {
service: 'Gmail',
auth: { xoauth2: xoauth2Gen }
};
var mailer = bluebird.promisifyAll(nodemailer.createTransport(mail_transport));
mailer.sendMail({
from : '"Example User" <example#gmail.com>',
to : 'recipient#gmail.com',
subject : 'Example subject',
text : 'Example\nplain\ntext',
// could it be bad html?
html : 'Example<br><strong>html</strong><br>version'
}).then(function (info) { console.log(info); });
I've recently made simple app using ionic framework (also with phonegap android/ios). I needed to store some user data in database. I found out about firebase and successfully integrated it in my app. I've let user log in anonymously or by google. In my case each user should see only his content so I've added rules - and they work. However my app needs to run without internet connection on mobile platforms (android/ios) and firebase doesn't provide full offline storage (only temponary) in javascript (persistent storage is for now implemented in native only). So basically I hit the wall. I've been told about CouchDB/PouchDB and it indeed support offline storage and sync with remote server. I've created an account on cloudant, but then I remembered I need user authorization - as I said user needs to have only his own data. So let's say guest have only local database and user logged in with google have database also on remote server so he can switch device and work with the same (his own) data. But I can't find such tool in CouchDB (cloudant). Is there any other solution, which works with ionic smoothly and provides:
offline database,
authorization with google,
remote database with sync (with local db) after successful authorization,
remote database is hosted on some server (may be paid, but trial needed) - i don't want to worry about server - client only
?
TL;DR: I'm looking for a hosting service working on ionic/angularjs/cordova with remote database + local database and synchronisation between them.
What is wrong with using MongoDB it is by far the easiest to integrate with Express/Node.js using the npm mongoose. It's really popular and great way to store data and its similar to firebase.
For example:
var Express = require('express'),
app = Express.router();
app.route('/api/auth/signup').post(users.signup);
Schema Example:
var mongoose = require('mongoose'),
Schema = mongoose.Schema
var UserSchema = new Schema({
firstName: {
type: String,
trim: true,
default: '',
required: 'Please fill in your first name'//message given if you fail to provide the required field.
},
lastName: {
type: String,
trim: true,
default: '',
required: 'Please fill in your last name'
},
displayName: {
type: String,
trim: true
},
email: {
type: String,
unique: 'Email already exists. Sign In?',
lowercase: true,
trim: true,
default: '',
validate: [validateLocalStrategyEmail, 'Please fill a valid email address'],//create a custom Email Validation strategy
required: 'Please enter a valid business Email'
},
});
Sample Controller:
exports.signup = function (req, res) {
// For security measurement we remove the roles from the req.body object
delete req.body.roles;
// Init user and add missing fields
var user = new User(req.body);
user.provider = 'local';
user.displayName = user.firstName + ' ' + user.lastName;
// Then save the user
user.save(function (err) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
// Remove sensitive data before login
user.password = undefined;
user.salt = undefined;
var jwtToken = authentication.signToken(user);
res.json({ user: user, token: jwtToken });
}
});
};
You can find some of this on MEAN.js Github.
But you must use LocalStorage. JSON web tokens for your ionic app if you want it to work on an iPad/iPhone.
The best bet is to use PouchDB which works with local storage (works on ionic/cordova) and will sync to a remote couchdb server with just a few lines of code:
var localDB = new PouchDB('mylocaldb');
var remoteDB = new PouchDB('http://localhost:5984/myremotedb');
localDB.sync(remoteDB);
The only tricky bit is the remote server with authentication. CouchDB can handle oauth out of the box, but on a per-database case, meaning you need to create a database per user, which is what many people do, but that's quite involved. Do a search for CouchDB on npmjs.com and see what comes up.
Alternatively, you can wrap your CouchDB behind a node server like Express, and handle auth in Express before passing the requests through.
It's not an easy task, and there's a reason Firebase charges a small fortune once you start racking up usage. If you're not big on server-side stuff, a simpler approach might be to use meteor or hoodie.
I am using Nodejs + javascript SDK, in which I am creating a new customer using predefined customer ID, which is working fine.
Now, using the same customer Id, I am generating a token at the backend and send it to the client. Now at client I am running .
var card = {
number: '4111111111111111',
cvv: '832',
expirationMonth: '10',
expirationYear: '2020',
cardholderName: 'Ankur Agarwal',
billingAddress: {
postalCode: '560076'
},
};
var client = new braintree.api.Client({clientToken: clientToken});
client.tokenizeCard(card, function (err, nonce) {
// Got Error "Unable to tokenize card"
})
Here is the http response it originally got from the server.
/**/callback_json1({"error":{"message":"User does not have the required permissions for this action"},"fieldErrors":[],"status":403})
I have enabled the API access for the account in the sandbox
There are some extra parameters which are not in docs, due to which its givng such a response. Once I removed the extra parameters from the request, its working fine.
I encountered similar error and it was caused by invalid client token. I was passing MerchantId instead of MerchantAccountId as a parameter.
Gateway.ClientToken.generate(new ClientTokenRequest()
{
MerchantAccountId = "Your MerchantAccountId" // NOT MerchantId
});
To manage your merchant accounts login to your Braintree control panel, go to Settings -> Processing and scroll down to Merchant Accounts