I'm authenticating users iva JWT Auth on WP Rest API from a React Native app, so, something like that:
export function userAuth(username, password) {
return dispatch => {
dispatch(requestLogin());
const appAuth = new FormData();
appAuth.append('username', username);
appAuth.append('password', password);
return fetch(wp.jwtEndpoint, {
method: 'POST',
body: appAuth
})
.then(function(res) {
return res.json();
})
.then(function(body) {
if(body.token){
getUserDataFromUsername(body.user_nicename, body.token, dispatch);
return dispatch(userSuccessLogin(body));
}
else {
return dispatch(userFailLogin(body));
}
});
}
}
The response for this request is:
{ token: 'eyJ0eXAiOiJKXXXQ',
user_email: 'supertest#gmail.com',
user_nicename: 'super-test-avatar',
user_display_name: 'TEST TEST' }
My issue is: as I can't get user ID from there, how can I retrieve user data form a request like https://www.wp.com/wp-json/wp/v2/users/${userId}?
I tried using https://github.com/dest81/wp-api-get-user-by-username, which would allow me to do that based on username, but its endpoints goes to 404, so I think it's outdated.
Related
I'm using the following interceptors in a Vuejs v2 website to push a firebase token to my node backend. There in the backend, I detect/verify the token, pull some data using the uid from a database and then process any api calls.
Even though I am using the firebase onIdTokenChanged to automatically retrieve new ID tokens, sometimes, if the user is logged in, yet inactive for an hour, the token expires without refreshing. Now, this isn't a huge deal - I could check in the axios response interceptor and push them to a login page, but that seems annoying, if I can detect a 401 token expired, resend the axios call and have a refreshed token, the user won't even know it happened if they happen to interact with a component that requires data from an API call. So here is what I have:
main.js
Vue.prototype.$axios.interceptors.request.use(function (config) {
const token = store.getters.getSessionToken;
config.headers.Authorization = `Bearer ${token}`;
return config;
});
Vue.prototype.$axios.interceptors.response.use((response) => {
return response }, async function (error) {
let originalRequest = error.config
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
let user = auth.currentUser;
await store.dispatch("setUser", {user: user, refresh: true}).then(() => {
const token = store.getters.getSessionToken;
Vue.prototype.$axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;
return Vue.prototype.$axios.request(originalRequest);
});
}
return Promise.reject(error); });
let app;
auth.onAuthStateChanged(async user => {
await store.dispatch("setUser", {user: user, refresh: false}).then(() => {
if (!app) {
app = new Vue({
router,
store,
vuetify,
render: h => h(App)
}).$mount('#app')
}
})
.catch(error => {
console.log(error);
});
});
vuex
setUser({dispatch, commit}, {user, refresh}) {
return new Promise((resolve) => {
if(user)
{
user.getIdToken(refresh).then(token => {
commit('SET_SESSION_TOKEN', token);
this._vm.$axios.get('/api/user/session').then((response) => {
if(response.status === 200) {
commit('SET_SESSION_USER', response.data);
resolve(response);
}
})
.catch(error => {
dispatch('logout');
dispatch('setSnackbar', {
color: "error",
timeout: 4000,
text: 'Server unavailable: '+error
});
resolve();
});
})
.catch(error => {
dispatch('logout');
dispatch('setSnackbar', {
color: "error",
timeout: 4000,
text: 'Unable to verify auth token.'+error
});
resolve();
});
}
else
{
console.log('running logout');
commit('SET_SESSION_USER', null);
commit('SET_SESSION_TOKEN', null);
resolve();
}
})
},
I am setting the token in vuex and then using it in the interceptors for all API calls. So the issue I am seeing with this code is, I'm making an API call with an expired token to the backend. This returns a 401 and the axios response interceptor picks it up and goes through the process of refreshing the firebase token. This then makes a new API call with the same config as the original to the backend with the updated token and returns it to the original API call (below).
This all seems to work, and I can see in dev tools/network, the response from the API call is sending back the correct data. However, it seems to be falling into the catch of the following api call/code. I get an "undefined" when trying to load the form field with response.data.server, for example. This page loads everything normally if I refresh the page (again, as it should with the normal token/loading process), so I know there aren't loading issues.
vue component (loads smtp settings into the page)
getSMTPSettings: async function() {
await this.$axios.get('/api/smtp')
.then((response) => {
this.form.server = response.data.server;
this.form.port = response.data.port;
this.form.authemail = response.data.authemail;
this.form.authpassword = response.data.authpassword;
this.form.sendemail = response.data.sendemail;
this.form.testemail = response.data.testemail;
this.form.protocol = response.data.protocol;
})
.catch(error => {
console.log(error);
});
},
I have been looking at this for a few days and I can't figure out why it won't load it. The data seems to be there. Is the timing of what I'm doing causing me issues? It doesn't appear to be a CORS problem, I am not getting any errors there.
Your main issue is mixing async / await with .then(). Your response interceptor isn't returning the next response because you've wrapped that part in then() without returning the outer promise.
Keep things simple with async / await everywhere.
Also, setting common headers defeats the point in using interceptors. You've already got a request interceptor, let it do its job
// wait for this to complete
await store.dispatch("setUser", { user, refresh: true })
// your token is now in the store and can be used by the request interceptor
// re-run the original request
return Vue.prototype.$axios.request(originalRequest)
Your store action also falls into the explicit promise construction antipattern and can be simplified
async setUser({ dispatch, commit }, { user, refresh }) {
if(user) {
try {
const token = await user.getIdToken(refresh);
commit('SET_SESSION_TOKEN', token);
try {
const { data } = await this._vm.$axios.get('/api/user/session');
commit('SET_SESSION_USER', data);
} catch (err) {
dispatch('logout');
dispatch('setSnackbar', {
color: "error",
timeout: 4000,
text: `Server unavailable: ${err.response?.data ?? err.message}`
})
}
} catch (err) {
dispatch('logout');
dispatch('setSnackbar', {
color: "error",
timeout: 4000,
text: `Unable to verify auth token. ${error}`
})
}
} else {
console.log('running logout');
commit('SET_SESSION_USER', null);
commit('SET_SESSION_TOKEN', null);
}
}
I allow users to login with facebook on my app, backed by firebase authentication.
In around 20% of the facebook logins, I don't receive the user's email. I need the email address in my app, and can't figure out why I don't receive it.
Since I get the email address 80% of the time, I assume I have the right permissions setup to retrieve it.
I also enforced "One account per email address" in firebase-auth, so it seems to be a different issue than that raised in Firebase Auth missing email address.
Relevant extracts of my code:
export const FacebookSignUp: React.FC<SocialAuthProps & { title?: string }> = ({ onError, onSetWaiting, title }) => {
async function onFacebookButtonPress() {
onSetWaiting(true);
const { email, first_name, accessToken } = await getFacebookUserData();
const couldLogin = await tryLoginWithFacebook(email, accessToken);
if (!couldLogin) {
// Create a Firebase credential with the AccessToken
const facebookCredential = FacebookAuthProvider.credential(accessToken);
const userCredential = await firebaseAuth.signInWithCredential(facebookCredential);
if (userCredential.user === null) {
throw new Error("Null user");
}
const signupUser: SignupUserData = {
userId: userCredential.user.uid,
email,
pseudo: first_name || undefined
};
await createSignupUser(signupUser).then(() => {
onSetWaiting(false);
});
}
}
return (
<SocialButton
iconName="facebookIcon"
title={title || "S'inscrire avec Facebook"}
onPress={() =>
onFacebookButtonPress().catch((err) => {
onSetWaiting(false);
if (err instanceof SocialAuthError) {
onError(err);
} else if (err instanceof Error) {
const { message, name, stack } = err;
serverError("Unexpected signup error", { message, name, stack });
}
})
}
/>
);
};
import { LoginManager, AccessToken, GraphRequest, GraphRequestManager } from "react-native-fbsdk";
export async function getFacebookUserData(): Promise<FacebookInfo> {
LoginManager.logOut();
const result = await LoginManager.logInWithPermissions(["public_profile", "email"]);
if (result.isCancelled) {
throw "User cancelled the login process";
}
// Once signed in, get the users AccesToken
const { accessToken } = (await AccessToken.getCurrentAccessToken()) || {};
if (!accessToken) {
throw "Something went wrong obtaining access token";
}
return new Promise((resolve, reject) => {
let req = new GraphRequest(
"/me",
{
httpMethod: "GET",
version: "v2.5",
parameters: {
fields: {
string: "email,first_name"
}
}
},
(err, res) => {
if (err || res === undefined) {
reject(err);
} else {
const { first_name, email } = res as { first_name: string; email: string };
resolve({ first_name, email, accessToken });
}
}
);
new GraphRequestManager().addRequest(req).start();
});
}
Facebook allows you to opt out of passing your email along to third-party apps. You can request it, but the user can deny it.
If I ever log in with Facebook I always opt out of passing my email along - most of the time, the third-party app doesn't need it for legitimate purposes.
"I need the email address in my app" - why? email marketing? account duplication prevention?
In cases where you did not get an email, assume the user has opted-out and/or doesn't have an email tied to their account. If you need one, ask the user to input a contact email address and explain what you are using it for. Expect some users to still opt out and plan around it.
You could always convert their username into a non-existent email like theirusername#noreply.users.yourapp.com depending on your use case.
Firebase question
My goal is to keep a user signed in a desktop app made primarily with NodeJS
User signs in
Store some kind of token to sign user in later
When user re-opens app, sign user in with that token if available
Token can expire / be invalidated
Currently, it seems Firebase relies on cookies of some sort. Is there a way to sign in a user without their email/password again?
I am currently trying to use Firebase authentication as part of my Electron app where I cannot have the Firebase authentication code be part of a renderer process which would normally have access to browser-level storage.
The ideal situation would be something like this that works:
const auth_token = await user.getIdToken();
firebase
.auth()
.signInWithCustomToken(auth_token) // But maybe some other method?
.then((user) => {
console.log(user);
})
How would you do this in a NodeJS app that has no access to browser storage such that the user does not need to continuously login?
Referencing this issue: Persist Firebase user for Node.js client application
fwiw, I don't wish to share these tokens with another application.
Electron question
A slightly different question: Can the main process in Electron store cookies that Firebase could access?
Recently read this: https://github.com/firebase/firebase-js-sdk/issues/292
My solution to this was:
Get a refresh token via email/password (long-lived ~1 yr)
Get an id_token with the refresh token (short-lived)
Get a custom token with an id_token (short-lived: ~1 hour)
Sign in with custom token
Save refresh token locally - never share it
So, something like this:
import Store from 'electron-store';
import firebase from 'firebase';
import * as request from 'request-promise';
const store = new Store({ name: 'config' });
function logout() {
const user = firebase.auth().currentUser;
store.delete('refresh_token');
if (user) {
return firebase.auth().signOut();
}
}
// https://firebase.google.com/docs/reference/rest/auth#section-create-email-password
function signup(email, password) {
return request
.post({
headers: { 'content-type': 'application/x-www-form-urlencoded' },
url: `https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=${firebase_config['apiKey']}`,
body: `email=${email}&password=${password}&returnSecureToken=true`,
json: true,
})
.then((res) => {
store.set({ refresh_token: res.refreshToken });
return login_with_id_token(res.idToken);
});
}
// Generates a refresh_token that we later use & save
async function login_with_email(email: string, password: string) {
const res = await request.post({
headers: { 'content-type': 'application/x-www-form-urlencoded' },
url: `https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=${firebase_config['apiKey']}`,
body: `email=${email}&password=${password}&returnSecureToken=true`,
json: true,
});
const refresh_token = res.refreshToken;
store.set({ refresh_token });
console.log(store.path);
return login_with_refresh_token(refresh_token);
}
/**
* Needed to acquire a refresh_token
* #param refresh_token string
*/
function get_id_token(refresh_token) {
return request.post({
headers: { 'content-type': 'application/x-www-form-urlencoded' },
url: `https://securetoken.googleapis.com/v1/token?key=${firebase_config['apiKey']}`,
body: `grant_type=refresh_token&refresh_token=${refresh_token}`,
json: true,
});
}
/**
* Generates a custom token we can use to sign in given an id_token
* #param id_token string
*/
function get_custom_token(id_token) {
return request.get({
url: `https://us-central1-${firebase_config['projectId']}.cloudfunctions.net/create_custom_token?id_token=${id_token}`,
json: true,
});
}
function login_with_id_token(id_token) {
if (id_token) {
return get_custom_token(id_token).then((token) => {
// console.log(`Retrieved custom token: ${custom_token}`);
return firebase.auth().signInWithCustomToken(token);
});
}
}
/**
* If token is null, it attempts to read it from disk otherwise
* it will use the one supplied to login
* #param token string || null
*/
async function login_with_refresh_token(token = null) {
let id_token = null;
let refresh_token = token;
if (!refresh_token) {
refresh_token = store.get('refresh_token');
store.get('refresh_token', null);
// console.log('Using a cached refresh token...');
}
if (refresh_token) {
const res = await get_id_token(refresh_token);
if (res) {
id_token = res['id_token'];
return login_with_id_token(id_token);
}
}
}
// Purposely attempt to login without a refresh_token in case it's on disk
function attempt_login() {
return login_with_refresh_token(null);
}
Firebase cloud function:
exports.create_custom_token = functions.https.onRequest(async (req, res) => {
const id_token = req.query.id_token;
const user = await admin.auth().verifyIdToken(id_token);
if (user) {
const custom_token = await admin.auth().createCustomToken(user.uid);
res.setHeader('Content-Type', 'application/json');
res.status(200).send(custom_token);
} else {
res.sendStatus(500);
}
});
I have been working on a oauth2 flow for spotify by following this similar tutorial by the Firebase team for Instagram HERE
I am able to submit my credentials and return the user code and state in the url, but when I run the method to submit the code to return an auth token, the auth token that I print to console in the Firebase functions returns: Auth Token Error Not Found. Here's my workflow:
Here's the Spotify docs
FIRST, I have a function to configure my spotifyOAuth:
function spotifyOAuth2Client() {
// Spotify OAuth 2 setup
const credentials = {
client: {
id: functions.config().spotify.clientid,
secret: functions.config().spotify.clientsecret,
},
auth: {
tokenHost: 'https://accounts.spotify.com',
authorizePath: '/authorize'
},
};
return require('simple-oauth2').create(credentials);
}
I use that function in this Firebase function that is called using https://us-central1-<my project string>.cloudfunctions.net/redirect:
exports.redirect = functions.https.onRequest((req, res) => {
const oauth2 = spotifyOAuth2Client();
cookieParser()(req, res, () => {
const state = req.cookies.state || crypto.randomBytes(20).toString('hex');
console.log('Setting verification state:', state);
res.cookie('state', state.toString(), {
maxAge: 3600000,
secure: true,
httpOnly: true,
});
const redirectUri = oauth2.authorizationCode.authorizeURL({
redirect_uri: OAUTH_REDIRECT_URI,
//scope: OAUTH_SCOPES,
state: state,
});
console.log('Redirecting to:', redirectUri);
res.redirect(redirectUri);
});
});
The code above returns a url string with the proper parameters, the following code block is where my code breaks, I have another cloud function that runs after being redirected from the res.redirect(redirectUri) above. And when I try to run the getToken() method, it appears to not return anything because I hit the catch block instead? This is where I observe the Auth Token Error Not Found.
const oauth2 = spotifyOAuth2Client();
try {
return cookieParser()(req, res, async () => {
console.log('Received verification state:', req.cookies.state);
console.log('Received state:', req.query.state);
if (!req.cookies.state) {
throw new Error('State cookie not set or expired. Maybe you took too long to authorize. Please try again.');
} else if (req.cookies.state !== req.query.state) {
throw new Error('State validation failed');
}
console.log('Received auth code:', req.query.code);
console.log(OAUTH_REDIRECT_URI);
// Get the access token object (the authorization code is given from the previous step).
const tokenConfig = {
code: req.query.code,
redirect_uri: 'http://localhost:8100/popup'
};
// Save the access token
try {
const result = await oauth2.authorizationCode.getToken(tokenConfig)
const accessToken = oauth2.accessToken.create(result);
console.log('inside try');
console.log(result);
console.log(accessToken);
} catch (error) {
console.log('Access Token Error', error.message);
}
I've double checked my spotify client/secret credentials in the config, what is going wrong with this OAuth2 flow?
Resolved my issue, I was not using the correct endpoints:
const credentials = {
client: {
id: functions.config().spotify.clientid,
secret: functions.config().spotify.clientsecret,
},
auth: {
tokenHost: 'https://accounts.spotify.com',
authorizePath: '/authorize',
tokenPath: '/api/token'
},
};
I have a Sign Up/Sign in/Sign Out issue. I am creating a React/Redux project, and am using Axios for my GET & POST requests. I am newer to Axios and have read through the documentation to no avail.
Here is the issue.
1. I can successfully create a user with Sign Up in a sign up form.
2. I can make a Post request(a couple of strings) with a basic Redux form.
3. The user and post both are persisting in MongoDB, and I am getting token back.
4. When I log out of that new user, and sign back in with that new user, I am able to sign in, but my post shows up as a 401. Intuition and research tell me that this is an issue with the token, but then again, I'm not sure why I'm able to sign in with the newly created user, but not able to make another post. Any help would be greatly appreciate. Here are my Axios methods:
var config = {
headers: { authorization: localStorage.getItem('token') }
}
export function signinUser({ email, password }){
return function(dispatch){
axios.post(`${ROOT_URL}/signin`, {email, password})
.then(response => {
dispatch({ type: AUTH_USER });
localStorage.setItem('token', response.data.token);
browserHistory.push('/newitem');
})
.catch(response => dispatch(authError("There was a something wrong with your request.")));
}
}
export function signoutUser(){
localStorage.removeItem('token');
return {type: UNAUTH_USER};
}
export function signupUser({ email, password }) {
return function(dispatch) {
// Submit email/password to the server
axios.post(`${ROOT_URL}/signup`, { email, password })
.then(response => {
dispatch({type: AUTH_USER});
//update the token
localStorage.setItem('token', response.data.token);
browserHistory.push('/newitem');
})
.catch(response => dispatch(authError(response.data.error)));
}
}
export function createPost(props) {
return function(dispatch){
axios.post(`${ROOT_URL}/newitem`, { props }, config )
.then(request => {
dispatch({
type: CREATE_POSTS,
payload: request
})
browserHistory.push('/items');
});
}
}