FB.login returns Invalid OAuth access token - authorization

I've been developing an app for the past few weeks and up until now there have been no issues. Just a couple days ago a strange bug has started occurring:
My application uses the PHP SDK and implements the Javascript SDK for user authorization. The user is allowed to roam the application freely, but when they click on a video, FB.login is called to request permissions from the user and get an access token.
jQuery Code
FB.login(function(response) {
if (response.authResponse) {
//Set global vars
fb_uid = response.authResponse.userID;
fb_token = response.authResponse.accessToken;
//If user has already authorized the app
if (response.status === 'connected') {
//Create the user record
$.ajax(site_url + '/facebook/create_fb_user', {
type: 'POST',
dataType: 'json',
data: {fb_uid: fb_uid, token: fb_token},
success: function (data) {
user = data.resp.fb_user;
viewVideo(item);
}
});
};
};
}, {scope: "publish_stream"});
PHP Code
try {
$this->_fb->setAccessToken($this->request->post('token'));
$data = $this->_fb->api("/me");
$model = new Model_Fbuser;
$model->data = array(
'fb_uid' => $data['id'],
'fb_token' => $extended_token
);
$resp = $model->update();
return $this->render_json(array(
'success' => TRUE,
'resp' => $resp
));
} catch (Exception $e) {
return $this->render_json(array(
'success' => FALSE,
'error' => $e->getMessage(),
'token' => $this->request->post('token')
));
}
The first time the user does this, the FB.login call returns a valid access token, the PHP SDK is able to set the token, and everything works as expected.
However, should the user revoke the application's access in their App Settings, and then return to the application, they are presented with the FB.login once more, but this time, the call returns the same access token they were previously given, which has already had its access revoked. Trying to set the access token with the PHP SDK throws the exception: "Invalid OAuth access token."
Yet if I then check the token in the Facebook debugger, is says it is valid.
Edit:
Further investigation reveals that the user is issues the same access token every time in the same session. If the user logs out, then logs back in, then they will receive a new valid token. But if they try to get a new token without logging out first, Facebook reissues them the same invalid one. When trying to use this access token to query information about the user, this is the response:
{"error":{"type":"OAuthException","message":"Error validating access token: The session was invalidated explicitly using an API call."}}

If you get the "Invalid OAuth access token" error in this way, I usually call FB.logout() and then immediately call FB.login() in the callback.
FB.logout(function(response) {
// user is now logged out
FB.login(...)
});
Not ideal, but the best way to fix such a use-case.

Related

How to get new access token with Google Identity Services after login

I'm currently using google.accounts.oauth2.initTokenClient and then tokenClient.requestAccessToken() to prompt the user to select an account. Then, I'm using the access_token from the TokenResponse of https://developers.google.com/identity/oauth2/web/reference/js-reference#TokenResponse and sending that param to my server to login.
The issue I have is for google.accounts.oauth2.revoke(), since that now requires a valid access token as a parameter.
However, since the access token expires after an hour, is there any way to get a new access token without making the user go through the UX flow again with requestAccessToken()?
_googleClient = google.accounts.oauth2.initTokenClient({
client_id: _clientid,
scope: 'https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email',
callback: function (tokenResponse) {
_accessToken = tokenResponse.access_token; additionalOptions);
},
error_callback: function (err) {
console.log('err:', err)
}
});
_googleClient.requestAccessToken();

Google Identity Services : How to refresh access_token for Google API after one hour?

I have implemented the new Google Identity Services to get an access_token to call the Youtube API.
I try to use this on an Angular app.
this.tokenClient = google.accounts.oauth2.initTokenClient({
client_id: googleApiClientId,
scope: 'https://www.googleapis.com/auth/youtube.readonly',
callback: (tokenResponse) => {
this.accessToken = tokenResponse.access_token;
},
});
When I call this.tokenClient.requestAccessToken(), I can get an access token and use the Youtube API, that works.
But after one hour, this token expires. I have this error : "Request had invalid authentication credentials."
How can I get the newly refreshed access_token transparently for the user ?
There are two authorization flows for the Google Identity Services (GIS) library:
The implicit flow, which is client-side only and uses .requestAccessToken()
The authorization code flow, which requires a backend (server-side) as well and uses .requestCode()
With the implicit flow (which is what you are using), there are no refresh tokens. It is up to the client to detect tokens aging out and to re-run the token request flow. Here is some sample code from google's examples for how to handle this:
// initialize the client
tokenClient = google.accounts.oauth2.initTokenClient({
client_id: 'YOUR_CLIENT_ID',
scope: 'https://www.googleapis.com/auth/calendar.readonly',
prompt: 'consent',
callback: '', // defined at request time in await/promise scope.
});
// handler for when token expires
async function getToken(err) {
if (err.result.error.code == 401 || (err.result.error.code == 403) &&
(err.result.error.status == "PERMISSION_DENIED")) {
// The access token is missing, invalid, or expired, prompt for user consent to obtain one.
await new Promise((resolve, reject) => {
try {
// Settle this promise in the response callback for requestAccessToken()
tokenClient.callback = (resp) => {
if (resp.error !== undefined) {
reject(resp);
}
// GIS has automatically updated gapi.client with the newly issued access token.
console.log('gapi.client access token: ' + JSON.stringify(gapi.client.getToken()));
resolve(resp);
};
tokenClient.requestAccessToken();
} catch (err) {
console.log(err)
}
});
} else {
// Errors unrelated to authorization: server errors, exceeding quota, bad requests, and so on.
throw new Error(err);
}
}
// make the request
function showEvents() {
// Try to fetch a list of Calendar events. If a valid access token is needed,
// prompt to obtain one and then retry the original request.
gapi.client.calendar.events.list({ 'calendarId': 'primary' })
.then(calendarAPIResponse => console.log(JSON.stringify(calendarAPIResponse)))
.catch(err => getToken(err)) // for authorization errors obtain an access token
.then(retry => gapi.client.calendar.events.list({ 'calendarId': 'primary' }))
.then(calendarAPIResponse => console.log(JSON.stringify(calendarAPIResponse)))
.catch(err => console.log(err)); // cancelled by user, timeout, etc.
}
Unfortunately GIS doesn't handle any of the token refreshing for you the way that GAPI did, so you will probably want to wrap your access in some common retry logic.
The important bits are that the status code will be a 401 or 403 and the status will be PERMISSION_DENIED.
You can see the details of this example here, toggle to the async/await tab to see the full code.
To refresh the access token in a transparent way for the end-user you have to use the Refresh Token, This token will also come in the response to your call.
With this token, you can do a POST call to the URL: https://www.googleapis.com/oauth2/v4/token with the following request body
client_id: <YOUR_CLIENT_ID>
client_secret: <YOUR_CLIENT_SECRET>
refresh_token: <REFRESH_TOKEN_FOR_THE_USER>
grant_type: refresh_token
refresh token never expires so you can use it any number of times. The response will be a JSON like this:
{
"access_token": "your refreshed access token",
"expires_in": 3599,
"scope": "Set of scope which you have given",
"token_type": "Bearer"
}
#victor-navarro's answer is correct, but I think the URL is wrong.
I made a POST call to https://oauth2.googleapis.com/token with a body like this and it worked for me:
client_id: <YOUR_CLIENT_ID>
client_secret: <YOUR_CLIENT_SECRET>
refresh_token: <REFRESH_TOKEN_FOR_THE_USER>
grant_type: refresh_token

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.

nodejs robinhood api login

I am trying to login to the robinhood API, I turned 2fa and sms off in the app but am still getting an error does this look correct below or is robinhood just slow at updating when 2fa is turned off.
var credentials = {
username: '*****',
password: '*****'
};
var Robinhood = require('robinhood')(credentials, function(){
console.log(Robinhood.auth_token());
// <authenticated alphanumeric token>
})
the error
Error: token not found {"statusCode":400,"body":{"detail":"Request blocked, challenge type required.","accept_challenge_types":{"sms":"SMS"}},"headers":{"date":"Mon, 24 May 2021 22:44:07 GMT","content-type":"application/json","content-length":"93","connection":"close","server":"openresty","allow":"POST, OPTIONS","x-robinhood-api-version":"0.0.0","content-security-policy":"default-src 'none'","x-frame-options":"SAMEORIGIN","x-content-type-options":"nosniff","x-xss-protection":"1; mode=block","access-control-allow-origin":"https://robinhood.com","vary":"Origin","trace-uuid":"56ccb9cc-8bca-4dbd-be6f-4a6d86171354"},"request":{"uri":{"protocol":"https:","slashes":true,"auth":null,"host":"api.robinhood.com","port":443,"hostname":"api.robinhood.com","hash":null,"search":null,"query":null,"pathname":"/oauth2/token/","path":"/oauth2/token/","href":"https://api.robinhood.com/oauth2/token/"},"method":"POST","headers":{"Host":"api.robinhood.com","Accept":"*/*","Accept-Encoding":"gzip, deflate","Referer":"https://robinhood.com/","Origin":"https://robinhood.com","content-type":"application/x-www-form-urlencoded","content-length":214}}}
Robinhood recently required 2fa to be enabeld if using the api. It is mentioned in the detail of the request body.
"detail":"Request blocked, challenge type required.","accept_challenge_types":{"sms":"SMS"}}
Go ahead and turn it on and then you can access the api with this snippet
let Robinhood = require('robinhood')(credentials, function(data) {
if (data && data.mfa_required) {
var mfa_code = ""; //Notice this is blank.
Robinhood.set_mfa_code(mfa_code, () => {
console.log(Robinhood.auth_token());
Robinhood.positions((error, response, body) => {
console.log(body);
})
})
}
})
Once you make a request you'll get an error, but the 2fa challenge will be sent to whatever you set your account with. Once you received the 2fa code, set the mfa_code and re-run the snippet. Once you ran the snippet again with a valid 2fa code, then you've successfully logged in. Copy the authorization token and you can use that without the need of going through 2fa again for i believe 24 hrs

vue-authenticate "Error: Auth popup window closed"

I'm trying to authenticate using oauth2 provider of vue-authenticate with custom axios response interceptor as follow:
bindRequestInterceptor() {
this.$http.interceptors.request.use((config) => {
console.log('Axios intercetping request')
console.log(`Before checking Token : ${this.getToken()}`)
if (this.isAuthenticated()) {
console.log('Authenticated')
config.headers.Authorization = [
this.options.tokenType, this.getToken(),
].join(' ')
} else {
console.log('Not authenticated')
delete config.headers.Authorization
let this_ = this;
this.authenticate('oauth2').then(function(authResponse){
console.log(authResponse)
this_.isAuthenticated = this_.$auth.isAuthenticated();
})//This fires an oauth popup window
}
console.log(`After checking Token : ${this.getToken()}`)
return config
})
},
When running an ajax call:
- I get redirected to my authorize url (/oauth/authorize) in new window
- After approving, I get successfully redirected to my custom redirect_uri with authorization code.
My problem is that, why am I not able to be authenticated after this flow.
In fact, if I re-call an ajax via axios, it reloop me from the authorization flow (approval window, etc)
Is anyone here familiar with this tools, and how to solve this issue ?
Note: when close the approval window, I get this error in the page which called the ajax
Possible Unhandled Promise Rejection: Error: Auth popup window closed
Note2: The console log according to above code
Axios intercetping request
app.js?id=01021d92b5ff42f0b696:30471 Before checking Token : null
app.js?id=01021d92b5ff42f0b696:30479 Not authenticated
app.js?id=01021d92b5ff42f0b696:30487 After checking Token : null