Nodemail to send mail for firebase app with service account - firebase

I get the following message when trying to send mail with Nodemailer :
'535-5.7.8 Username and Password not accepted.
Learn more at\n535 5.7.8 https://support.google.com/mail/?p=BadCredentials y42sm13399804wrc.51 - gsmtp',
responseCode: 535,
command: 'AUTH XOAUTH2' }
I am using a service account from a firebase project, and have granted access to the GMail API. But the nodemailer docs for 2LO is really scarce so I wonder if anyone could help me find if I use the correct credentials ?
user (functions.config().gmail.user) : myfirebaseproject#appspot.gserviceaccount.com
-
function sendContactMail(contactName, contactEmail, contactDate, contactText) {
// Create transport
let transporter = nodemailer.createTransport({
host: 'smtp.gmail.com',
port: 465,
secure: true,
auth: {
type: 'OAuth2',
user: functions.config().gmail.user,
serviceClient: functions.config().gmail.client_id,
privateKey: functions.config().gmail.private_key
//accessToken: 'ya29.Xx_XX0xxxxx-xX0X0XxXXxXxXXXxX0x',
//expires: 1484314697598
}
});
// Mail Options
let mailOptions = {
from: `"${APP_NAME}" <${SENDER}>`,
replyTo: `${contactEmail}`,
to: 'recipient#gmail.com',
subject: `Nouveau contact photo de ${contactName}`,
html: `Nom : ${contactName}<br/>
Email : ${contactEmail}<br/>
Date du mariage : ${contactDate}<br/>
Message : ${contactText}`,
disableFileAccess: true,
disableUrlAccess:true
};
// Send Mail
return transporter.sendMail(mailOptions);
}

Has your account two steps authentication enabled? In that case generate an app password on https://myaccount.google.com/apppasswords and use it instead of your normal password:
const mailTransport = nodemailer.createTransport(
`smtps://user#gmail.com:generatedAppPassword#smtp.gmail.com`);

I have Cloud Function with this code and it's work fine. Try this
mailTransport = nodemailer.createTransport({
service: 'Gmail',
auth: {
user: ${email},
pass: ${password}
}
});

Related

How can I add payload when generating JWT in http JWT AUTH?

This is what I could find on official ballerina learn-by-examples website.
But how can I sign payloads and return JWT on some endpoints, like we do in NodeJS using jsonwebtoken
You can use the jwt:issue() method to achieve this.
jwt:IssuerConfig issuerConfig = {
username: "user",
issuer: "wso2",
audience: "example.com",
expTime: 3600,
signatureConfig: {
config: {
keyFile: "./resources/private.key"
}
},
customClaims: {
"scope": "scope1 scope2"
}
};
string jwt = check jwt:issue(issuerConfig);
You can refer this service example for a scenario where this approach was used to implement a login endpoint.

next-auth x Microsoft Graph API: Where to get the accessToken

I'm building an email marketing automation tool using NextJS, next-auth and Microsoft Graph API. I'm using next-auth's Azure AD B2C provider to authenticate users, and I've been following their docs.
Within the Configuration (Advanced) section of the docs, I've followed the steps to setup an Azure AD api app to communicate with the Microsoft Graph API (to send email on our user's behalf). Now, when a user signs up, an access_token (jwt) is added to my accounts db table. Here it is decoded:
{
"iss": "https://something.b2clogin.com/b03...f94/v2.0/",
"exp": 1664588154,
"nbf": 1664584554,
"aud": "6eb...c5b",
"idp_access_token": "EwB...QI=",
"idp": "live.com",
"name": "Will Despard",
"sub": "1f7...d6c",
"emails": [
"willdespard#outlook.com"
],
"tfp": "B2C_1_signupsignin",
"scp": "mail.send",
"azp": "ff8...f5d",
"ver": "1.0",
"iat": 1664584554
}
The problem is, there is no example of how to setup the Microsoft Graph JS Client with next-auth. For example, according to Microsoft, to create a Microsoft Graph API client, you must do the following:
import { Client } from '#microsoft/microsoft-graph-client';
const client = Client.init({
authProvider: (done) =>
done(
null,
accessToken // WHERE DO WE GET THIS FROM?
),
});
const sendMail = {
message: {
subject: 'Meet for lunch?',
body: { contentType: 'Text', content: 'The new cafeteria is open.' },
toRecipients: [
{ emailAddress: { address: 'william.cm.despard#gmail.com' } },
],
},
};
const userDetails = await client.api('/me/sendMail').post(sendMail);
However, the following is unclear:
Where are we meant to get the accessToken used in this example from? I've tried using the idp_access_token in the decoded accessToken on my accounts db table (above), but this doesn't seem to work.
I'm assuming the accessToken we use to communicate with Microsoft Graph API is going to expire after a short amount of time. How do we handle getting a new token?
Help/code examples would be much appreciated!
I would try it like this. First, it looks that for graph access you should be looking for Azure AD provider, not Azure AD B2C that is a service that provides identity providers. I.e. looks like you need this one: https://next-auth.js.org/providers/azure-ad
To use Microsoft Graph to send mail you'll also need to request a non-default scope with "Send Mail" grant from your user. Means, when authorizing your app the user will be asked to consent that your app will send emails on behalf of him. Also you'll need to save the graph access token you get from the authentication flow. Something like this:
import AzureADProvider from "next-auth/providers/azure-ad"
export const authOptions: NextAuthOptions = {
providers: [
....
AzureADProvider({
clientId: process.env.AZURE_AD_CLIENT_ID,
clientSecret: process.env.AZURE_AD_CLIENT_SECRET,
authorization: {
params: {
scope:
"openid email profile Mail.Send",
},
},
// tenantId: process.env.AZURE_AD_TENANT_ID,
}),
],
callbacks: {
async jwt({ token, account }) {
if (account) {
token.accessToken = account.access_token;
}
return token
},
Please note that if you do not specify tenantId that would mean that your application will be available for users from any tenant, but that in turn would mean that you must be a verified publisher (i.e. must have a valid MPN ID associated with your app). If you do specify a tenantId, then your app will only work for users from that specified tenant.
Later on, you could just use the token from the API:
import { getToken } from 'next-auth/jwt';
import { Client } from '#microsoft/microsoft-graph-client';
// some API function
export default async function handler(req, res) {
const token = await getToken({ req })
if (token) {
const accessToken = token.accessToken;
const client = Client.init({
authProvider: (done) =>
done(null, accessToken)
});
const sendMail = {
message: {
subject: 'Meet for lunch?',
body: { contentType: 'Text', content: 'The new cafeteria is open.' },
toRecipients: [
{ emailAddress: { address: 'william.cm.despard#gmail.com' } },
],
},
};
const userDetails = await client.api('/me/sendMail').post(sendMail);
...

How to use my firebase authentication to work with external services?

Ok so I am using firebase as authentication for my iOS app. Now I plan on adding video calling to my app using an external service know as connectyCube. This service has their own authentication system and I cannot use their services unless a user is authenticated.
Option 1: I can use their own authentication which means my app would have two authentication systems - not very productive
Option 2: They say I can use an existing authentication to validate users
I understand that this is a common thing in the developers world and I see the word OAuth and JWT being thrown around but I am a rookie developer and I want to understand how I can use firebase and authenticate a user from an external service.
These are the questions they have asked when I opted for the "I have my own authentication" option:
What is your end point URL
Is it GET or POST
Request Headers
Request Params
Response Params
Where do I get all this information from firebase? Any help would be great
As an alternative to #Dharmaraj's answer, you could instead make use of a HTTP Event Cloud Function for this based on the code sample they've provided.
Using this method, you create the endpoint /verifyUserToken to be used by ConnectyCube.
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
admin.initializeApp();
export const verifyUserToken = functions.https.onRequest((req, res) => {
const idToken = req.query.token;
verifyUser(idToken)
.then(
(userData) => {
res.status(200).json(userData)
},
(err) => {
console.log("Token verification failed.", err.code || err.message);
res.status(422).json({error: "User token is invalid"})
}
)
.catch((err) => console.error("Unexpected crash", err));
});
async function verifyUser(token) {
if (!token)
throw new Error("token missing");
// using `true` here to force token to be checked against the Firebase
// Auth API rather than trusting its contents as-is
const { uid, email } = await admin.auth().verifyIdToken(token, true);
// pull the user's username from their user data
// at /users/{userId}/username
const username = (await admin.database().ref("users/" + uid + "/username")).val();
// use user's actual email if available, otherwise fallback
// to a userID based email
const uEmail = email || uid + "#users.noreply.yourapp.com";
// use user's username if available, otherwise fallback to
// the email address above.
const uLogin = username !== null ? username : uEmail;
return {
uid,
login: uLogin,
email: uEmail,
user: {id: uid, login: uLogin, email: uEmail}, // <- this part in particular is used by ConnectyCube
users: [{uid, login: uLogin, email: uEmail}]
};
}
Once deployed, you would use the following settings:
Setting
Value
API URL:
https://us-central1-PROJECT-ID.cloudfunctions.net/verifyUserToken
GET/POST
GET
Request params:
{"token": "#{login}"}
Response params:
{"uid": "#{user.id}", "email": #{user.email}, "login": "#{user.login}"}
It looks like ConnectyCube uses some sort of Session Tokens as mentioned in their documentation with their own username and password.
The most easiest way would be creating a ConnectyCube account whenever a new user signs up in your Firebase app using Firebase Auth Triggers for Cloud functions. Then you can generate username and password on behalf of your user and store them in a Database.
So whenever you need to create a ConnectyCube session, check for the currently logged in user and fetch their ConnectyCube credentials.
async function createCCSession() {
const userId = firebase.auth().currentUser.uid
const ccCrednetials = (await firebase.database().ref(`ccCreds/${userId}`).once('value')).val()
ConnectyCube.createSession(ccCredentials)
.then((session) => {
console.log(session)
return session
}).catch((error) => console.log(error));
}
You can protect the database using security rules so a user can access their credentials only.
{
"rules": {
"ccCreds": {
"$uid": {
".read": "$uid === auth.uid"
}
}
}
}
While I don't normally double-answer a question, in the course of exploring some other authentication related problems, I've managed to eliminate the Cloud Function from my other answer entirely and instead call the Authentication API directly.
Setting
Value
API URL:
https://www.googleapis.com/identitytoolkit/v3/relyingparty/getAccountInfo?key=FIREBASE_CONFIG_API_KEY
GET/POST
POST
Request params:
{"idToken": "#{login}"}
Response params:
{"uid": "#{users.0.localId}", "email": #{users.0.email}, "full_name": "#{users.0.displayName}"}
On your client, you just call the ConnectyCube Login API with the following data:
POST https://api.connectycube.com/login
login=<Firebase-ID-token>
password=<any-random-value-to-pass-the-validation>

Firebase client error: Custom token corresponds to a different audience

I'm using the Firebase Python AdminSDK to generate a custom token which a Javascript client uses to sign in to Firebase. When the JS client tries to authenticate with the custom token it gets the error "Custom token corresponds to a different audience".
The code given with the error: 'auth/custom-token-mismatch'.
Many Google'd answers regarding the "audience" mismatch reference Analytics. But I'm doing a Web project, not iOS or Android, so I can't use Analytics to manage audiences.
The SO answers I've read are listed at the end, below.
I captured the custom token and plugged it in to https://jwt.io/ and both the values and the instanciation/expiration times (an hour apart) look good:
Decoded custom token on jwt.io:
{
"claims": {},
"uid": "<myuniqueID",
"sub": "firebase-adminsdk-1knpr#firebase-<myproject>.iam.gserviceaccount.com",
"iss": "firebase-adminsdk-1knpr#firebase-<myproject>.iam.gserviceaccount.com",
"iat": 1540153710,
"aud": "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit",
"exp": 1540157310
}
Python server:
def getFirebaseInstance(): # For Firebase Python SDK
try:
currentDir_path = os.path.dirname(os.path.realpath(__file__))
cred = credentials.Certificate(currentDir_path + '/includeFirebaseServiceAccounts/firebase-<myprojectname>-firebase-adminsdk-1knpr-e1244dd261.json')
firebaseAdmin = firebase_admin.initialize_app(cred, { 'databaseURL': 'https://<myprojectname>.firebaseio.com', 'databaseAuthVariableOverride': {'uid':'<myuniqueServerID>'}})
if firebaseAdmin:
return(firebaseAdmin)
except:
raise
def firebaseClientToken(request):
try:
uid = "<myuniqueClientID>" # case sensitive
additional_claims = { }
token = auth.create_custom_token(uid,additional_claims)
return HttpResponse(token)
except Exception as err:
return HttpResponse("System error:" + str(err), status=406)
Javascript client:
(
function authClient2Firebase() {
$.ajax({
url: "firebaseClientToken/",
method: "POST",
success: function(response) { step2(response); },
error: function(xhr) { alert("There was an error loading a security check. Have you lost your internet connection? Detail:" + xhr.responseText); }
});
function step2(customToken) {
try {
firebase.auth().signInWithCustomToken(customToken).catch(function(error) {
var errorCode = error.code;
var errorMessage = error.message;
alert("There was an error with the secure login. \n\nDetail: " + errorMessage + '\nCode: ' + errorCode);
});
}
catch(err) {
alert(err);
}
console.log("authClient2Firebase.js: Firebase login succeeded!");
}
}
)();
My project under the Console "Settings" page does have a Web API key, but I don't see anywhere that it's used.
There's only one user, me, under the Console's "Settings"->"Users and Permissions" page.
There's only one service account listed on the Console "Settings"->"Service Accounts" page. I tried deleting all secrets on that page, generating a new one, then generating and installing a new blue-button "secret" (bad name, actually it generates a whole json credential object).
These are the domains listed in Console "Authentication" -> "Sign-in Method":
localhost Default
<myproject>.firebaseapp.com Default
127.0.0.1 Custom
auth.firebase.com Custom
The actual domain I'm using is localhost:8000, which can't be entered here.
SO answers consulted unsuccessfully:
The custom token corresponds to a different audience
(I'm not using a key, except what's stored in the ServiceAccount
credentials.)
Firebase token error, "The custom token corresponds to a different audience."
Firebase custom auth issue token different audienceenter
link description here (Close, but I'm not using a Node server and
not sure what he means by server "must belong to the same project"
since the Python server isn't registered in any way except through the
ServiceAccount credentials which I downloaded.)
Embarrassing but true, it turned out to be a simple oversight. When the JS client initialized itself as a Firebase app, before authenticating, it was using old credentials from a test environment.
// Initialize Firebase
var config = {
apiKey: "<WebAPI from Firebase console, 'Project Settings'>",
authDomain: "<myproject>.firebaseapp.com",
databaseURL: "https://<myproject>.firebaseio.com",
projectId: "<myproject>",
storageBucket: "<myproject>.appspot.com",
messagingSenderId: "<id from Console Project Settings>" // optional
};
firebase.initializeApp(config);

How to add External Service logins to an already existing account in Meteor?

Having created a profile page for my app, I would like to display a list of social services that the user is on. It struck me that the easiest way would be to use Meteor's built in accounts system for this.
Is there a good way to add external services to an existing account?
Also, will the user then be able to log in with either (e.g.) Facebook and his password from my app?
Another question that naturally follows: Is there a good way to add an application specific password to an account that was created with an external service?
Here's an alternate method. In this solution, I'm overriding a core function and adding some custom behavior. My goal is to associate the service data with the currently logged in user, then allow the core function to do its thing like normal.
orig_updateOrCreateUserFromExternalService = Accounts.updateOrCreateUserFromExternalService;
Accounts.updateOrCreateUserFromExternalService = function(serviceName, serviceData, options) {
var loggedInUser = Meteor.user();
if(loggedInUser && typeof(loggedInUser.services[serviceName]) === "undefined") {
var setAttr = {};
setAttr["services." + serviceName] = serviceData;
Meteor.users.update(loggedInUser._id, {$set: setAttr});
}
return orig_updateOrCreateUserFromExternalService.apply(this, arguments);
}
Pros:
Avoids creation of unnecessary accounts
Code is short and easy to understand
Code is easy to remove if this functionality is added to Meteor core
Cons:
Requires the user to be logged in. If a user logs in with twitter initially, logs out, and then logs in with facebook, then two seperate accounts will be created.
Users who share a computer may get their accounts merged unintentionally.
Relies on knowledge of how updateOrCreateUserFromExternalService works. This isn't terrible - because it's part of Meteor's public api it probably won't change drastically (not often anyway). But it's still risky.
Here is how I add credentials to existing user account: .../meteor-how-to-login-with-github-account.html
Yes, a user account can be associated with multiple services and have a password-based login at the same time. In the Meteor docs, you can see the structure of such a user account:
{
_id: "bbca5d6a-2156-41c4-89da-0329e8c99a4f", // Meteor.userId()
username: "cool_kid_13", // unique name
emails: [
// each email address can only belong to one user.
{ address: "cool#example.com", verified: true },
{ address: "another#different.com", verified: false }
],
createdAt: 1349761684042,
profile: {
// The profile is writable by the user by default.
name: "Joe Schmoe"
},
services: {
facebook: {
id: "709050", // facebook id
accessToken: "AAACCgdX7G2...AbV9AZDZD"
},
resume: {
loginTokens: [
{ token: "97e8c205-c7e4-47c9-9bea-8e2ccc0694cd",
when: 1349761684048 }
]
}
}
}
For adding a username/password login to an existing account, you can use Accounts.sendResetPasswordEmail on the server side. This also ensures the change happens authenticated and authorized.
Of course you can also just update the user record on the server side with a new password yourself, but this might create a security hole in your app. I would also advise against implementing your own crypto protocol for this if possible, as it is hard.
If you want to add other services than email, you could for example
call a server method that saves a random, long token in the current user's MongoDB document and returns it to the client.
re-login the user with another service using Accounts.loginWith[OtherService]. This logs the user out and in again, using a new account on the other service.
call a second server method with the returned token from the first method as parameter. This second method searches for the user account with the given token and merges its data into the current (new) account.
Check out the example and answer in this posting. It pretty much gives you the code to integrate multiple external and internal accounts. With minor tweaks, you can add the password fields for each account as you desire.
How to use Meteor.loginWithGoogle with mrt:accounts-ui-bootstrap-dropdown
Code:
isProdEnv = function () {
if (process.env.ROOT_URL == "http://localhost:3000") {
return false;
} else {
return true;
}
}
Accounts.loginServiceConfiguration.remove({
service: 'google'
});
Accounts.loginServiceConfiguration.remove({
service: 'facebook'
});
Accounts.loginServiceConfiguration.remove({
service: 'twitter'
});
Accounts.loginServiceConfiguration.remove({
service: 'github'
});
if (isProdEnv()) {
Accounts.loginServiceConfiguration.insert({
service: 'github',
clientId: '00000',
secret: '00000'
});
Accounts.loginServiceConfiguration.insert({
service: 'twitter',
consumerKey: '00000',
secret: '00000'
});
Accounts.loginServiceConfiguration.insert({
service: 'google',
appId: '00000',
secret: '00000'
});
Accounts.loginServiceConfiguration.insert({
service: 'facebook',
appId: '00000',
secret: '00000'
});
} else {
// dev environment
Accounts.loginServiceConfiguration.insert({
service: 'github',
clientId: '11111',
secret: '11111'
});
Accounts.loginServiceConfiguration.insert({
service: 'twitter',
consumerKey: '11111',
secret: '11111'
});
Accounts.loginServiceConfiguration.insert({
service: 'google',
clientId: '11111',
secret: '11111'
});
Accounts.loginServiceConfiguration.insert({
service: 'facebook',
appId: '11111',
secret: '11111'
});
}
Accounts.onCreateUser(function (options, user) {
if (user.services) {
if (options.profile) {
user.profile = options.profile
}
var service = _.keys(user.services)[0];
var email = user.services[service].email;
if (!email) {
if (user.emails) {
email = user.emails.address;
}
}
if (!email) {
email = options.email;
}
if (!email) {
// if email is not set, there is no way to link it with other accounts
return user;
}
// see if any existing user has this email address, otherwise create new
var existingUser = Meteor.users.findOne({'emails.address': email});
if (!existingUser) {
// check for email also in other services
var existingGitHubUser = Meteor.users.findOne({'services.github.email': email});
var existingGoogleUser = Meteor.users.findOne({'services.google.email': email});
var existingTwitterUser = Meteor.users.findOne({'services.twitter.email': email});
var existingFacebookUser = Meteor.users.findOne({'services.facebook.email': email});
var doesntExist = !existingGitHubUser && !existingGoogleUser && !existingTwitterUser && !existingFacebookUser;
if (doesntExist) {
// return the user as it came, because there he doesn't exist in the DB yet
return user;
} else {
existingUser = existingGitHubUser || existingGoogleUser || existingTwitterUser || existingFacebookUser;
if (existingUser) {
if (user.emails) {
// user is signing in by email, we need to set it to the existing user
existingUser.emails = user.emails;
}
}
}
}
// precaution, these will exist from accounts-password if used
if (!existingUser.services) {
existingUser.services = { resume: { loginTokens: [] }};
}
// copy accross new service info
existingUser.services[service] = user.services[service];
existingUser.services.resume.loginTokens.push(
user.services.resume.loginTokens[0]
);
// even worse hackery
Meteor.users.remove({_id: existingUser._id}); // remove existing record
return existingUser; // record is re-inserted
}
});

Resources