Email only authentication with Vue.js and Vuex on Firebase - api

I want user to be automatically authenticated (temporarily) on Firebase just by sending Email then be redirected to a welcome page asking to complete the auth process by following a link received by email.
The first part is ok, I can authenticate by just inserting email and generating a random password like the following (Vuex store action):
this.$store.dispatch('userSignUp', { email: this.email, password: this.generatePassword })
which is called by component method button v-on:click="userSignUp
Vuex action is like :
userSignUp ({commit}, payload) {
commit('setLoading', true)
firebase.auth().createUserWithEmailAndPassword(payload.email, payload.password)
.then(firebaseUser => {
commit('setUser', firebaseUser)
commit('setLoading', false)
commit('setError', null)
router.push('/welcome')
})
.catch(error => {
commit('setError', error.message)
commit('setLoading', false)
})
}
So far so good, the user put the email, an helper function this.generatePassword generate a random password and the user is logged in and created on firebase.
Now this user is logged in, is on a welcome page, but it doesn't know his own random password (because I don't want to).
I want this to be one shot login and if the user want to come back, has to follow the link sent by email by Firebase.
There is a firebase function [sendPasswordResetEmail][1], which seems right for the case but I connot find the way to make it working.
I did Vuex action like before :
export const actions = {
sendPasswordReset ({commit}, payload) {
commit('setLoading', true)
firebase.auth().sendPasswordResetEmail(payload.email)
.then(firebaseUser => {
commit('setUser', firebaseUser)
commit('setLoading', false)
commit('setError', null)
router.push('/welcome')
})
.catch(error => {
commit('setError', error.message)
commit('setLoading', false)
router.push('/error')
})
},
...
which is called by component method button v-on:click="userSignUp
methods: {
userSignUp () {
this.$store.dispatch('userSignUp', { email: this.email, password: this.generatePassword })
this.$store.dispatch('sendPasswordReset', { email: this.email })
}
},
I only get response code
{
"error": {
"errors": [
{
"domain": "global",
"reason": "invalid",
"message": "EMAIL_NOT_FOUND"
}
],
"code": 400,
"message": "EMAIL_NOT_FOUND"
}
}
while the Request Payload seems ok anyway :
{requestType: "PASSWORD_RESET", email: "luca.soave#gmail.com"}
email
:
"luca.soave#gmail.com"
requestType
:
"PASSWORD_RESET"
Any idea ?

The provider you're using is called the password provider. As its name implies it is heavily dependent on the user having (and knowing) a password. Since you are looking for passwordless authentication, I'd recommend against using the email+password provider as the basis.
Instead consider implementing a custom authentication provider. While this involves a few more components, it is not as difficult as you may think. You'll need to run trusted code, which you can do either on a server you already have, or on Cloud Functions. In either of those cases, you'll use one of the Admin SDKs to implement the sensitive parts of the authentication flow.
A quick list of steps that I think you'll need:
Create an endpoint (e.g. a HTTP triggered Cloud Function) for the user to request an authentication email.
Implement the code for this endpoint to:
Generate a random one-time code in there, which you're going to send to the user. Firebase Authentication calls this the out-of-band (or OOB) code, since it's sent to the user on a different medium than your app.
Store this code and the user's email address somewhere where only your server-side code can read it, e.g. in the Firebase Database or Cloud Firestore.
Send an email to the user, with the code or a link to a page that includes the code and their email address.
Create an endpoint (e.g. again a HTTP function, or web page) where the user enters (e.g. by clicking on a link in the email) the OOB code and their email address.
Compare the code the user entered, to the one you stored before.
If the codes match, generate a custom token for the user and send it to them.
The user/app now signs into Firebase with the custom token.

Related

Give web app permanent control of google email

I recently got a domain from Google Domain and it provides professional emails for the domain. So, I made a support#domain email to send and receive emails. Right now, I built a web app that will use this email to send email-verification to new users who sign up.
I got the code working, however, I used OAuth2.0 from Google to allow the web app to sign into the support email. This causes the Access token to expire and forces me to into my .env file and replace BOTH tokens. How tedious! If I were to publish this web app, I can't just go into my heroku vars and replace the tokens every hour. Thats extremely impractical.
I looked into service accounts by google, but they seem to STILL need OAuth anyways as a 3 legged OAuth. As I am using Express.js, I wanted to know if there was a way to set the tokens and be done with it permanently. Essentially giving the web app permanent control of the google account. What do I do? And what do I use? All advice is greatly appreciated.
Code for sending email verification:
const nodemailer = require("nodemailer")
const dotenv = require("dotenv")
const {generateVerifyToken} = require("./auth")
const { print } = require("#AlecRuin/color-logger")
dotenv.config()
let transporter = nodemailer.createTransport({
service:"gmail",
auth: {
type: 'OAuth2',
user: process.env.EMAIL,
clientId: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
refreshToken: process.env.REFRESH_TOKEN,
accessToken: process.env.ACCESS_TOKEN
}
})
module.exports= async function(email){
try {
let mailOptions={
from: "support#domain.us",
to:email,
subject:"Verify your email",
text:`
Thank you for signing up with redacted!!
To verify your email (and make sure you're not some kind of annoying bot), simply follow this link:
${process.env.NODE_ENV === "production"?"www.redacted.com":"localhost:3001"}/api/user/verify/?token=${generateVerifyToken(email)}
`
}
print("sending email",new Error,{isClient:false})
transporter.sendMail(mailOptions,(err,data)=>{
if (err){
console.error(err);
return err
}else{
print("Email sent!",new Error,{isClient:false})
return true
}
})
} catch (error) {
print(error,new Error, {isClient:true,severity:4})
return error
}
}

PassportJS OAuth2Strategy: authenticate returns 400 instead of redirecting

I'm trying to setup discord oauth2 pkce using passportjs and the passport-oauth2
const discordStrategy = new OAuth2Strategy({
authorizationURL: 'https://discord.com/api/oauth2/authorize',
tokenURL: 'https://discord.com/api/oauth2/token',
clientID: DISCORD_CLIENT_ID,
clientSecret: DISCORD_CLIENT_SECRET,
callbackURL: DISCORD_CALLBACK_URL,
state: true,
pkce: true,
scope: ['identity', 'scope'],
passReqToCallback: true,
},
(req: Request, accessToken: string, refreshToken: string, profile: DiscordUserProfile, cb: any) => {
prisma.user.findUnique({ where: { email: profile.email ?? '' }}).then(foundUser => {
if (foundUser === null) {
// Create a new user with oauth identity.
} else {
cb(null, foundUser)
}
}).catch(error => {
cb(error, null);
})
});
I've been following the google example as well as some others, these examples indicate that, I should be able to use:
passport.use('discord', discordStrategy);
and
authRouter.get('/discord', passport.authenticate('discord'));
and this should redirect to the OAuth2 providers login page, but instead, I get a 400 Bad Request "The request cannot be fulfilled due to bad syntax." The response body contains an object:
{"scope": ["0"]}
Why is this happening instead of the expected redirect?
My intention is that, once the user logs in, I should get a code, then I can post that code and the code verifier to get an access token, then once the access token is obtained, the actual authenticate call can be made
Edit: I put breakpoints in the passport.authenticate function and I stepped through it. It does actually get through everything and it calls the redirect. The parsed URL it generates, even if I copy it and manually navigate to the URL, it gives me the same, just gives:
{"scope": ["0"]}
and no login page, why?
If you add a version number to the base api url, e.g. /v9 it gives a full error message.
It turned out I had typo'd the scopes, I had 'identity' instead of 'identify' - now this part of the process is working as expected.

How to include TOTP MFA in AWS Cognito authentication process

I'm using Cognito user pools to authenticate my web application. I've got it all working right now but now I need to enable MFA for it. This is how I do it right now (all the code provided are server-side code):
Signing up the user:
const cognito = new AWS.CognitoIdentityServiceProvider();
cognito.signUp({
ClientId,
Username: email,
Password,
}).promise();
An email is sent to the user's address (mentioned as username in the previous function call) with a code inside.
The user reads the code and provides the code to the next function call:
cognito.confirmSignUp({
ClientId,
ConfirmationCode,
Username: email,
ForceAliasCreation: false,
}).promise();
The user logs in:
const tokens = await cognito.adminInitiateAuth({
AuthFlow: 'ADMIN_NO_SRP_AUTH',
ClientId,
UserPoolId,
AuthParameters: {
'USERNAME': email,
'PASSWORD': password,
},
}).promise();
I'm pretty happy with this process. But now I need to add the TOTP MFA functionality to this. Can someone tell me how these steps will be changed if I want to do so? BTW, I know that TOTP MFA needs to be enabled for the user pool while creating it. I'm just asking about how it affects my sign-up/log-in process.
Alright, I found a way to do this myself. I must say, I couldn't find any documentation on this so, use it at your own risk!
Of course, this process assumes you have a user pool with MFA enabled (I used the TOTP MFA).
Signing up the user:
const cognito = new AWS.CognitoIdentityServiceProvider();
cognito.signUp({
ClientId,
Username: email,
Password,
}).promise();
An email is sent to the user's address (mentioned as username in the previous function call) with a code inside.
The user reads the code and provides the code to the next function call:
cognito.confirmSignUp({
ClientId,
ConfirmationCode: code,
Username: email,
ForceAliasCreation: false,
}).promise();
The first log in:
await cognito.adminInitiateAuth({
AuthFlow: 'ADMIN_NO_SRP_AUTH',
ClientId,
UserPoolId,
AuthParameters: {
'USERNAME': email,
'PASSWORD': password,
},
}).promise();
At this point, the return value will be different (compared to what you'll get if the MFA is not enforced). The return value will be something like:
{
"ChallengeName": "MFA_SETUP",
"Session": "...",
"ChallengeParameters": {
"MFAS_CAN_SETUP": "[\"SOFTWARE_TOKEN_MFA\"]",
"USER_ID_FOR_SRP": "..."
}
}
The returned object is saying that the user needs to follow the MFA_SETUP challenge before they can log in (this happens once per user registration).
Enable the TOTP MFA for the user:
cognito.associateSoftwareToken({
Session,
}).promise();
The previous call is needed because there are two options and by issuing the given call, you are telling Cognito that you want your user to enable TOTP MFA (instead of SMS MFA). The Session input is the one return by the previous function call. Now, this time it will return this value:
{
"SecretCode": "...",
"Session": "..."
}
The user must take the given SecretCode and enter it into an app like "Google Authenticator". Once added, the app will start showing a 6 digit number which is refreshed every minute.
Verify the authenticator app:
cognito.verifySoftwareToken({
UserCode: '123456',
Session,
}).promise()
The Session input will be the string returned in step 5 and UserCode is the 6 digits shown on the authenticator app at the moment. If this is done successfully, you'll get this return value:
{
"Status": "SUCCESS",
"Session": "..."
}
I didn't find any use for the session returned by this object. Now, the sign-up process is completed and the user can log in.
The actual log in (which happens every time the users want to authenticate themselves):
await cognito.adminInitiateAuth({
AuthFlow: 'ADMIN_NO_SRP_AUTH',
ClientId,
UserPoolId,
AuthParameters: {
'USERNAME': email,
'PASSWORD': password,
},
}).promise();
Of course, this was identical to step 4. But its returned value is different:
{
"ChallengeName": "SOFTWARE_TOKEN_MFA",
"Session": "...",
"ChallengeParameters": {
"USER_ID_FOR_SRP": "..."
}
}
This is telling you that in order to complete the login process, you need to follow the SOFTWARE_TOKEN_MFA challenge process.
Complete the login process by providing the MFA:
cognito.adminRespondToAuthChallenge({
ChallengeName: "SOFTWARE_TOKEN_MFA",
ClientId,
UserPoolId,
ChallengeResponses: {
"USERNAME": config.username,
"SOFTWARE_TOKEN_MFA_CODE": mfa,
},
Session,
}).promise()
The Session input is the one returned by step 8 and mfa is the 6 digits that need be read from the authenticator app. Once you call the function, it will return the tokens:
{
"ChallengeParameters": {},
"AuthenticationResult": {
"AccessToken": "...",
"ExpiresIn": 3600,
"TokenType": "Bearer",
"RefreshToken": "...",
"IdToken": "..."
}
}

Meteor Accounts obtaining the SAML Response (Token / User after log in)

i have a workin SAML logon with Meteor.accounts and MS Azure. I am using this https://github.com/steffow/meteor-accounts-saml library for SAML which is derived from https://github.com/bergie/passport-saml
The procedure goes like this:
Click saml login -> popup appears
Enter user/pw data in popup -> popup closes without error
Logged in success
So now i want to get the SAML Token for further processing (or at least the information about the logged in user which meteor has taken from the IDP).
Since i dont have a clue where the SAML Token is stored by Meteor or can be fetched in the code, can someone help me getting the SAML Response?
Probably solved by now but here it goes..
According to the code the saml response is retrieved in "saml_server.js" at row 70 and the token should be in there also (loginRequest.credentialToken)
loginResult should be fairly easy to save into the Meteor.users collection
var loginResult = Accounts.saml.retrieveCredential(loginRequest.credentialToken);
var email = loginResult.profile.email;
var user = Meteor.users.findOne({username: email});
if (!user) {
Accounts.createUser({
"username": email,
"profile": StufffromloginResult
});
} else {
Meteor.users.update(
{
"username": email
},
{ "$set": {
"profile": StufffromloginResult,
}
});
}
And retrieved with:
Meteor.user().profile

Retrieve Google+ activity list of an user

I need to get the list of activities of an user in Google+. My coding platform is node.js Express framework and I'm using google-api-nodejs-client package.
var googleapis = require('googleapis');
var auth = new googleapis.OAuth2Client();
var accessToken="XXXXXX......";
googleapis
.discover('plus', 'v1')
.execute(function (err, client) {
if (err) {
console.log('Problem during the client discovery.', err);
return;
}
auth.setCredentials({
access_token: accessToken
});
client
.plus.people.get({ userId: 'me' })
.withAuthClient(auth)
.execute(function (err, response) {
console.log('My profile details------>', response);
});
client
.plus.activities.list({
userId: 'me',
collection: 'public',
maxResults: 100
})
.withAuthClient(auth)
.execute(function (err, response) {
console.log('err----------------------------->',err);//EDIT
console.log('activities---------------------->', response.items);
});
});
I got my profile details. But the activity is returning value: null. I checked my Google+ page to make sure that I have public posts. Also, I shared some posts to 'public' myself. Please help me find the bug in my code.
EDIT
Actually, there is an error. I found it by logging the value of err object in console as advised by Ryan Seys.
err--------------->
{
"error": {
"errors": [
{
"domain": "global",
"reason": "insufficientPermissions",
"message": "Insufficient Permission"
}
],
"code": 403,
"message": "Insufficient Permission"
}
}
It would help if you provide the value of the err object but here's a few thoughts:
Do you have Google+ API turned on for your project? See https://console.developers.google.com/ and the APIs and auth section of your project to enable the API.
Are you requesting the right scopes for user profile data. See https://developers.google.com/apis-explorer/#p/plus/v1/plus.activities.list to try out the request. Click the OAuth button on that page to see the different types of scopes you may like to try requesting from the user. Some of the scopes I see right now are:
https://www.googleapis.com/auth/plus.login (Know your basic profile info and list of people in your circles).
https://www.googleapis.com/auth/plus.me (Know who you are on Google)
https://www.googleapis.com/auth/userinfo.email (View your email address)
https://www.googleapis.com/auth/userinfo.profile (View basic information about your account)
Try adding an empty body field to the API request. This is a caveat of the current API client and some requests require you to enter a default empty {} after the parameter object.
client
.plus.activities.list({
userId: 'me',
collection: 'public',
maxResults: 100
}, {}) // <--- see the extra {} here!
.withAuthClient(auth)
.execute(function (err, response) {
console.log('activities---------------------->', response.items);
});
I think the problem is that you are specifying an empty fields parameter to client.plus.activities.list() instead of not providing a fields parameter at all. This tells it to return no fields for the results. Since the fields parameter is optional, you can omit it completely.
Try something like:
client
.plus.activities.list({
userId: 'me',
collection: 'public',
maxResults: 100
})