Angular 10 : ngOnInit two promises - javascript

I'm currently trying to implement in my Angular app the connection to Strava API.
To resume quickly:
User clicks on a button to connect to Strava
It is redirected to Strava for authentication(using PKCE)
Strava redirects to my app with a code
In the ngoninit I'm checking for route params and if I have the code, I launch two promises chained: the first one to get the Access Token from Strava then the recording into a DB(Firebase).
The problem is that sometimes the data is recorded in firebase and sometimes it is not. The behavior is not systematic. Strange thing is that I go into my postNewToken everytime because the console logs it.
If I just record to firebase (without strava token request) in ngOnInit(), it is created in 100% of the cases.
If I have a button that launches the token request and record into firebase, it seems to work everytime.
I have no idea how to solve it. It seems more a question of chaining promises into ngOnInit but I have no idea even how to bypass it.
The code from my component:
ngOnInit() {
const stravaCode = this.route.snapshot.queryParamMap.get('code');
if (stravaCode !== undefined) {
this.stravaService.handleStravaAuthorizationCode(stravaCode);
}
And in the service associated:
// Handle Strava Code received
handleStravaAuthorizationCode(authorizationCode: string) {
this.getStravaToken(authorizationCode).then(res => {
this.postNewToken(res).then(res => {
this.router.navigate(['encoding']);
});
});
}
// Get Strava access token to make the requests to the API -> only done once
getStravaToken(authorizationCode: string){
if (authorizationCode !== undefined){
console.log('Authorization code: ' + authorizationCode);
const data = {
client_id: environment.strava.client_id,
client_secret: environment.strava.client_secret,
code: authorizationCode,
grant_type: 'authorization_code'
};
return this.http.post<StravaToken>(this.stravaTokenURL, data)
.pipe(catchError(this.errorService.handleHttpError)).toPromise();
}
}
postNewToken(stravaToken: StravaToken) {
if (this.authService.isLoggedIn) {
console.log('Recording strava token into Firebase');
console.log(stravaToken);
return this.afs.collection('strava_tokens')
.add(stravaToken).then(res => console.log(res), err => console.log(err));
} else {
return Promise.reject(new Error('No User Logged In!'));
}
}

Finally, I understood.
I simply was not waiting for the connection to firebase to be established. So, I could not post any new data because I was not authenticated.

Related

Problem with JWT Refresh Token Flow with axios/axios-auth-refresh

(I've read a number of similar questions here, and most/all have said to use a different axios instance for the refresh token requests (versus the API requests). However, I'm not clear on how that would work, since I am using axios-auth-refresh for auto-refreshing the access tokens.)
I'm working on an app with a JWT-based authentication flow for back-end API requests. The general flow is working fine; upon login the user gets a long-term refresh token and short-term access token. Using the axios-auth-refresh plug-in for axios, I am able to auto-refresh the access token when it has expired.
My problem is, when the refresh token expires, I am not able to catch the error and redirect the user to re-authenticate. Nothing I've tried catches the error. The (current) code for the auto-refresh hook is:
const refreshAuth = (failed) =>
axios({ method: "post", url: "token", skipAuthRefresh: true })
.then(({ status, data: { success, accessToken } }) => {
console.warn(`status=${status}`);
if (!success) Promise.reject(failed);
processToken(accessToken);
// eslint-disable-next-line no-param-reassign
failed.response.config.headers.Authorization = `Bearer ${accessToken}`;
return Promise.resolve();
})
.catch((error) => console.error("%o", error));
createAuthRefreshInterceptor(axios, refreshAuth);
In cases of the refresh token being stale or missing, I see neither the status=xxx console line nor the dump of an error object in the catch() block.
The actual file this is in is on GitHub here, though it is slightly different than the working version above. Mainly, in the GH version the hook calls axios.post("token").then(...) where above I'm making a more explicit call to add the skipAuthRefresh parameter. Adding that got me more detailed error traces in the console, but I am still not catching the 401 response via the catch().
I've tried everything I can think of... anything jump out as something I'm missing?
Randy
(Edited to ensure the GitHub link points to the version of the file that has the issue.)
Since posting this, I have managed to work through the problem and come up with a working solution.
The key to the solution does in fact lie in using a different axios instance for the calls to renew the refresh token. I created a second module to encapsulate a second axios instance that would not get the interceptor created by the axios-auth-refresh module. After working around some inadvertent circular-dependency issues that this initially caused, I reached a point where I could see the exception being thrown by axios when the refresh token itself is stale or missing.
(Interestingly, this led to another problem: once I recognized that the refresh token was no longer valid, I needed to log the user out and have them return to the login screen. Because the application this is in is a React application, the authentication was being handled with custom hooks, which can only be called within a component. However, I had abstracted all the API calls into a non-React module so that I could encapsulate things like the addition of the Authorization header, the base URL, etc. At that level I could not run the auth hook to get access to the logout logic. I solved this by putting a default onError handler on the query object (a react-query object) that I use for all the API calls.)
I built upon the Request class from this SO answer to refresh the token and handle the refresh failures.
Now my Request looks like this:
import axios from "axios";
import {getLocalStorageToken, logOut, refreshToken} from "./authentication";
class Request {
ADD_AUTH_CONFIG_HEADER = 'addAuth'
constructor() {
this.baseURL = process.env.REACT_APP_USER_ROUTE;
this.isRefreshing = false;
this.failedRequests = [];
this.axios = axios.create({
baseURL: process.env.REACT_APP_USER_ROUTE,
headers: {
clientSecret: this.clientSecret,
},
});
this.beforeRequest = this.beforeRequest.bind(this);
this.onRequestFailure = this.onRequestFailure.bind(this);
this.processQueue = this.processQueue.bind(this);
this.axios.interceptors.request.use(this.beforeRequest);//<- Intercepting request to add token
this.axios.interceptors.response.use(this.onRequestSuccess,
this.onRequestFailure);// <- Intercepting 401 failures
}
beforeRequest(request) {
if (request.headers[this.ADD_AUTH_CONFIG_HEADER] === true) {
delete request.headers[this.ADD_AUTH_CONFIG_HEADER];
const token = getLocalStorageToken();//<- replace getLocalStorageToken with your own way to retrieve your current token
request.headers.Authorization = `Bearer ${token}`;
}
return request;
}
onRequestSuccess(response) {
return response.data;
}
async onRequestFailure(err) {
console.error('Request failed', err)
const {response} = err;
const originalRequest = err.config;
if (response.status === 401 && err && originalRequest && !originalRequest.__isRetryRequest) {
if (this.isRefreshing) {
try {
const token = await new Promise((resolve, reject) => {//<- Queuing new request while token is refreshing and waiting until they get resolved
this.failedRequests.push({resolve, reject});
});
originalRequest.headers.Authorization = `Bearer ${token}`;
return this.axios(originalRequest);
} catch (e) {
return e;
}
}
this.isRefreshing = true;
originalRequest.__isRetryRequest = true;
console.log('Retrying request')
console.log('Previous token', getLocalStorageToken())
try {
const newToken = await refreshToken()//<- replace refreshToken with your own method to get a new token (async)
console.log('New token', newToken)
originalRequest.headers.Authorization = `Bearer ${newToken}`;
this.isRefreshing = false;
this.processQueue(null, newToken);
return this.axios(originalRequest)
} catch (err) {
console.error('Error refreshing the token, logging out', err);
await logOut();//<- your logout function (clean token)
this.processQueue(err, null);
throw response;//<- return the response to check on component layer whether response.status === 401 and push history to log in screen
}
}
throw response;
}
processQueue(error, token = null) {
this.failedRequests.forEach((prom) => {
if (error) {
prom.reject(error);
} else {
prom.resolve(token);
}
});
this.failedRequests = [];
}
}
const request = new Request();
export default request;
My problem is, when the refresh token expires, I am not able to catch
the error and redirect the user to re-authenticate. Nothing I've tried
catches the error. The (current) code for the auto-refresh hook is:
What is the return code from your api if the access token expired ?
if it is different than 401 (default) you need to configure, see exanoke 403:
createAuthRefreshInterceptor(axios, refreshAuthLogic, {
statusCodes: [ 401, 403 ] // default: [ 401 ]
});

Expo AuthSession promptAsync - no way to wait for return when getting a new access token

We have a managed Expo app using AuthSession specifically to deploy our app to a web environment. Authenticating using Auth0 and a PKCE grant.
Have run into an issue trying to get a new access token when our current access token has expired. Before making an HTTP request we check to see if our access token has expired and if it has we use AuthSession.promptAysnc() to get a new one. The problem is we have no way of waiting for promptAsync to finish before making our HTTP request.
Have we missed something?
const [request, result, promptAsync] = AuthSession.useAuthRequest(
{
redirectUri,
clientId,
scopes,
prompt: AuthSession.Prompt.Login,
extraParams: {
audience
}
},
{ authorizationEndpoint, tokenEndpoint, revocationEndpoint }
);
const refreshAuthSessionAsync = async () => {
...
promptAsync({ useProxy });
// sleep hack - wait until we have something to return
await new Promise(r => setTimeout(r, 1000));
return authState;
};
const handleFetchAsync = async (url, request) => {
...
if (Date.parse(expiresAt) < (new Date()).getTime()) {
newAuthState = await refreshAuthSessionAsync();
return newAuthState;
}
...
}
I'm working through a similar problem with an Expo managed app trying to access a refresh token after authenticating through Auth0 using a PKCE flow.
I came across this repo
https://github.com/tiagob/create-full-stack/blob/master/packages/cfs-expo-auth0/src/index.tsx
while reading through the comments of this Expo issue about refresh tokens and Auth0
https://github.com/expo/examples/issues/209.
I haven't implemented anything from this repo yet, but it seems like some of its approach may solve the issue I'm having and hopefully your problem.
I used a useEffect hook to "wait" for the request to finish and a useState hook to save the current state.
useEffect(() => {
if (state !== stateMode.request_running) return;
if (request === null || response === null || response.type !== "success") {
// Request not done
}
else {
// Request done
onLogin();
setState(stateMode.request_done);
}
});
The state I used
const stateMode = {
initialized: 1,
request_running: 2,
request_done: 3
};
const [state, setState] = useState(stateMode.initialized);
And before calling promptAsync I set
setState(stateMode.request_running);

How to conditionally render page depending on express/mssql middle ware feedback

I have made a full stack application with a register activity successfully adding to db.
How would I conditionally render the home page dependent on if the login is correct.
In my login route I have an if statement which successfully logs "bad creds" if do not exist or "login: login successful.." if it does.
I added a redirect into the handle submit(this is triggered once the login form button is pressed) which was supposed to be triggered if successful (it technically is but it determines "bad creds successful as well").
I have attempted an if stametn but I am not sure how to use this with express middle ware.
the logic I would want the the portion of handle submit to do is something along the lines of
if (login successful){
window.location.href = "/home";
}
else {
window.location.href = "/login";
(preferably with a alert )
}
Login route
app.post("/login", async (req, response) => {
try {
await sql.connect(config);
var request = new sql.Request();
var Email = req.body.email;
var Password = req.body.password;
console.log({ Email, Password });
request.input("Email", sql.VarChar, Email);
request.input("Password", sql.VarChar, Password);
var queryString =
"SELECT * FROM TestLogin WHERE email = #Email AND password = #Password";
//"SELECT * FROM RegisteredUsers WHERE email = #Email AND Password = HASHBYTES('SHA2_512', #Password + 'skrrt')";
const result = await request.query(queryString);
if (result.recordsets[0].length > 0) {
console.info("/login: login successful..");
console.log(req.body);
req.session.loggedin = true;
req.session.email = Email;
response.send("User logined");
} else {
console.info("/login: bad creds");
response.status(400).send("Incorrect email and/or Password!");
}
} catch (err) {
console.log("Err: ", err);
response.status(500).send("Check api console.log for the error");
}
});
handleSubmit(e) {
e.preventDefault();
if (this.state.email.length < 8 || this.state.password.length < 8) {
alert(`please enter the form correctly `);
} else {
const data = { email: this.state.email, password: this.state.password };
fetch("/login", {
method: "POST", // or 'PUT'
headers: {
Accept: "application/json, text/plain, */*",
"Content-Type": "application/json"
},
body: JSON.stringify(data)
})
// .then(response => response.json())
.then(data => {
console.log("Success:", data);
// if ( ) {
// console.log("nice");
// } else {
// console.log("not nice");
// }
// window.location.href = "/home";
})
.catch(error => {
console.error("Error:", error);
});
}
}
catch(e) {
console.log(e);
}
You should have explained in the first place that you had a React app in the frontend. Talking at the same time about Express middleware and login route is a bit messy. :)
What you're doing is a login/sign in process through an API. This means your server should return JSON information regarding the login outcome. Then, your frontend should handle that in whatever way you want to. This means that your server should simply treat the login request as any other data request. Return a status code and some optional JSON data.
Authentication is a BIG subject and since you did't provide many details, I can only tell you how normally the overall process should go:
Send the user credentials to the server (like you do in your POST request)
Handle the response received from the server. If login was successful, you should receive some information from the server, like the user id, email, session id, either in the response JSON data or by HTTP headers. You should keep this information in the frontend, normally in localStorage, and use it for every request to the server to provide your identity. You should look up JSON Web Tokens.
In your React app, you want to check when starting the application if the user is already logged in or not (using the piece of information mentioned in step 2, or trying to fetch and endpoint that returns user information like /me). If you don't have that information or the request fails, redirect to Login.
In your React app, in your login page, handle the fetch result and redirect to home if the user is authenticated, or stay there and display whatever info you want.
I assume that since you're using user login some resources should be protected from being accessed by non logged in users or restricted depending on the logged in user. This is done with middleware on your Express server, that should check the user id / token / session id information your React app should be sending with every request.
To redirect using React Router, you don't want to use window.location. You want to use the Router itself to avoid reloading the full page. You can either use the injected history prop on your Login route component or wrap any component that needs it with withRouter HOC.
This article seem to lay out all options using React Router pretty well:
https://serverless-stack.com/chapters/redirect-on-login-and-logout.html
Hope this helps, this is a complex subject that you should split into smaller problems and tackle one at a time. ;)

Getting authorized access error from Spotify with spotify-wrapper

I'm trying to use the Spotify wrapper to use my app, I manage to hit Spotify's Account Service and the page to authorize the app to use a users spotify account information but I get an internal 500 server error:
index.js:6 POST https://accounts.spotify.com/en/authorize/accept 500 (Internal Server Error)
// Set up Spotify API wrapper
const scopes = ['user-read-private', 'user-read-email'];
const STATE_KEY = 'spotify_auth_state';
app.get('/login', (req, res) => {
const state = generateRandomString(16);
res.cookie(STATE_KEY, state);
res.redirect(spotifyApi.createAuthorizeURL(scopes, state));
});
app.get('/callback', (req, res) => {
const { code, state } = req.query;
const storedState = req.cookies ? req.cookies[STATE_KEY] : null;
if (state === null || state !== storedState) {
res.redirect('/#/error/state mismatch');
} else {
res.clearCookie(STATE_KEY);
spotifyApi
.authorizationCodeGrant(code)
.then(data => {
const { expires_in, access_token, refresh_token } = data.body;
// Set the access token on the API object to use it in later calls
spotifyApi.setAccessToken(access_token);
spotifyApi.setRefreshToken(refresh_token);
spotifyApi.getMe().then(({ body }) => {
console.log(body);
});
res.redirect('/search');
})
.catch(err => {
res.redirect('/#/error/invalid token');
});
}
});
This is my endpoint on that page:
https://accounts.spotify.com/en/authorize?client_id=CLIENT_ID&response_type=code&redirect_uri=http:%2F%2Flocalhost:8888%2Fcallback%2F&scope=user-read-private%20user-read-email&state=k4ogwe4h53i00000
Not entirely sure what's happening. I thought it was initially a state mismatch but the state generated is fine. Having trouble pinpointing where the error is. Anyone have any suggestions?
You need to set your redirect URI in the developer dashboard, ensuring that it's exactly the same as the one you're using in your app.
We're aware of the poor error handling for the invalid redirect URI error and it's being fixed.

Parse Server / JS SDK, error 206 when saving a user object

I am having trouble using the Parse Server JS SDK to edit and save a user.
I am signing in, logging in and retrieving the user just fine, I can call without exception user.set and add/edit any field I want, but when I try to save, even when using the masterKey, I get Error 206: Can t modify user <id>.
I also have tried to use save to direcly set the fields, same result.
A interesting thing is that in the DB, the User's Schema get updated with the new fields and types.
Here is my update function:
function login(user, callback) {
let username = user.email,
password = user.password;
Parse.User.logIn(username, password).then(
(user) => {
if(!user) {
callback('No user found');
} else {
callback(null, user);
}
},
(error) => {
callback(error.message, null);
}
);
}
function update(user, callback) {
login(user, (error, user) => {
if(error) {
callback('Can t find user');
} else {
console.log('save');
console.log('Session token: ' + user.getSessionToken());
console.log('Master key: ' + Parse.masterKey);
user.set('user', 'set');
user.save({key: 'test'}, {useMasterKey: true}).then(
(test) => {
console.log('OK - ' + test);
callback();
}, (err) => {
console.log('ERR - ' + require('util').inspect(err));
callback(error.message);
}
);
}
});
}
And a exemple of the error:
update
save
Session token: r:c29b35a48d144f146838638f6cbed091
Master key: <my master key>
ERR- ParseError { code: 206, message: 'cannot modify user NPubttVAYv' }
How can I save correctly my edited user?
I had the exact same problem when using Parse Server with migrated data from an existing app.
The app was created before March 2015 when the new Enhanced Sessions was introduced. The app was still using legacy session tokens and the migration to the new revocable sessions system was never made. Parse Server requires revocable sessions tokens and will fail when encountering legacy session tokens.
In the app settings panel, the Require revocable sessions setting was not enabled before the migration and users sessions were not migrated to the new system when switching to Parse Server. The result when trying to edit a user was a 400 Bad Request with the message cannot modify user xxxxx (Code: 206).
To fix the issue, I followed the Session Migration Tutorial provided by Parse which explain how to upgrade from legacy session tokens to revocable sessions. Multiple methods are described depending on your needs like enableRevocableSession() to enable these sessions on a mobile app, if you're only having a web app, you can enforce that any API requests with a legacy session token to return an invalid session token error, etc.
You should also check if you're handling invalid session token error correctly during the migration to prompt the user to login again and therefore obtain a new session token.
I had the same error and neither useMasterKey nor sessionToken worked for me either. :(
Here's my code:
console.log("### attempt 1 sessionToken: " + request.user.getSessionToken());
var p1 = plan.save();
var p2 = request.user.save(null, {sessionToken: request.user.getSessionToken()});
return Parse.Promise.when([p1, p2]).then(function(savedPlan) {
...
}
I see the matching session token in log output:
2016-08-21T00:19:03.318662+00:00 app[web.1]: ### attempt 1 sessionToken: r:506deaeecf8a0299c9a4678ccac47126
my user object has the correct ACL values:
"ACL":{"*":{"read":true},"PC7AuAVDLY":{"read":true,"write":true}}
I also see a bunch of beforeSave and afterSave logs with user being "undefined". not sure whether that's related.
beforeSave triggered for _User for user undefined:
I'm running latest parser-server version 2.2.18 on Heroku (tried it on AWS and results are the same)
function login(logInfo, callback) {
let username = logInfo.email,
password = logInfo.password;
Parse.User.logIn(username, password).then(
(user) => {
if(!user) {
callback('No user found');
} else {
callback(null, user);
}
},
(error) => {
callback(error.message, null);
}
);
}
function update(userInfo, data, callback) {
login(userInfo, (error, user) => {
if(error) {
callback('Can t find user');
} else {
getUpdatedData(user.get('data'), data, (error, updateData) => {
if(error) {
callback(error);
} else {
user.save({data: updateData}, /*{useMasterKey: true}*/ {sessionToken: user.get("sessionToken")}).then(
(test) => {
callback();
}, (err) => {
callback(error.message);
}
);
}
});
}
});
}
For some reason, retrying to use sessionToken worked.
This is not how asynchronous functions work in JavaScript. When createUser returns, the user has not yet been created. Calling user.save kicks off the save process, but it isn't finished until the success or error callback has been executed. You should have createUser take another callback as an argument, and call it from the user.save success callback.
Also, you can't create a user with save. You need to use Parse.User.signUp.
The function returns long before success or error is called.

Categories