Redirect_uri mismatch in fetch and gapi - google-oauth

working on connecting users to google, and we're trying to get their access and refresh tokens from the google api, and we're getting an issue exchanging the OAuth2 Code for tokens. Both sets of code have the same error.
I initialize the gapi client and fill in the information needed like so:
gapi.load('client:auth2', _ => {
gapi.client.init({
'apiKey': 'omitted for security',
clientId: 'omitted for security',
'scope': 'https://www.googleapis.com/auth/drive',
'discoveryDocs': ['https://www.googleapis.com/discovery/v1/apis/drive/v3/rest']
}).then(_ => {
gapi.auth2.getAuthInstance().grantOfflineAccess().then(resp => {
if(resp.code){
gapi.client.request({
path: 'https://www.googleapis.com/oauth2/v4/token',
method: 'post',
params: {code: resp.code},
body: {
code: resp.code,
client_id: opts.clientId,
client_secret: 'omitted for security',
grant_type: 'authorization_code',
redirect_uri: 'omitted for security',
access_type: 'offline'
},
}).then((onfulfill, onreject, context) => {
console.log('fulfilled', onfulfill);
console.log('rejected: ', onreject);
console.log('context', context);
}).catch(err => console.error(err.body));
}
});
});
});
What I'm trying to do in the .then() is to call the token endpoint to exchange the code in the response for a refresh and access token to store in my back end and the user's local storage.
I get this error response from both versions of the code. (better, more reliable code is provided here.)
{ "error": "redirect_uri_mismatch", "error_description": "Bad
Request" }
I also have a backend setup stashed as a last resort that accepts the code from gapi.auth2.getAuthInstance().grantOfflineAccess() calls the token endpoint, and returns the access_token and refresh_token to the client.
This code is similar, but not quite. instead of using the google api library, I used fetch, and it works fine. (Fetch and XHR on the front end have the same issues as the gapi.client.request function....)
const gConfig = require('./basic.json');
const scopes = ['https://www.googleapis.com/auth/drive'];
const { client_id, client_secret, redirect_uris } = gConfig.web;
const authClient = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);
app.post('/', (req, res) => {
const { code } = req.body;
console.log('Received Code From Request: ', code);
let data = { code , client_id, client_secret,redirect_uri: redirect_uris[0], grant_type: 'refresh_token'};
let encodedParams = Object.keys(data).map(k => encodeURIComponent(k) + '=' + encodeURIComponent(data[k])).join('&');
fetch(
`https://www.googleapis.com/oauth2/v4/token?code=${code}`,
{ method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: encodedParams }
).then((res) => {
console.log('called the api with fetch');
console.dir(res.json());
});
authClient.getToken(code, (err, token) => {
if (err) {
console.error(err);
res.status(500).json(err);
}
// console.dir(token);
console.log('TOKEN: =>', token);
res.json(token);
});
});
Is there anyone that's done this on the front end successfully?

You can't get a refresh token in a browser. Your example code would only work on a server. To do oauth at the client you should request "token" instead of "code".

Related

Spotify returning 200 on token endpoint, but response data is encoded

I'm working through this tutorial on creating an app that uses the Spotify API. Everything was going great until I got to the callback portion of authenticating using the authentication code flow.
(I do have my callback URL registered in my Spotify app.)
As far as I can tell, my code matches the callback route that this tutorial and others use. Significantly, the http library is axios. Here's the callback method:
app.get("/callback", (req, res) => {
const code = req.query.code || null;
const usp = new URLSearchParams({
code: code,
redirect_uri: REDIRECT_URI,
grant_type: "authorization_code",
});
axios({
method: "post",
url: "https://accounts.spotify.com/api/token",
data: usp,
headers: {
"content-type": "application/x-www-form-urlencoded",
Authorization: `Basic ${new Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString("base64")}`,
},
})
.then(response => {
console.log(response.status); // logs 200
console.log(response.data); // logs encoded strings
if (response.status === 200) {
res.send(JSON.stringify(response.data))
} else {
res.send(response);
}
})
.catch((error) => {
res.send(error);
});
Though the response code is 200, here's a sample of what is getting returned in response.data: "\u001f�\b\u0000\u0000\u0000\u0000\u0000\u0000\u0003E�˒�0\u0000Ee�uS\u0015��\u000e�(\b\u0012h\u0005tC%\u0010\u0014T\u001e�����0��^޳:���p\u0014Ѻ\u000e��Is�7�:��\u0015l��ᑰ�g�����\u0"
It looks like it's encoded, but I don't know how (I tried base-64 unencoding) or why it isn't just coming back as regular JSON. This isn't just preventing me logging it to the console - I also can't access the fields I expect there to be in the response body, like access_token. Is there some argument I can pass to axios to say 'this should be json?'
Interestingly, if I use the npm 'request' package instead of axios, and pass the 'json: true' argument to it, I'm getting a valid token that I can print out and view as a regular old string. Below is code that works. But I'd really like to understand why my axios method doesn't.
app.get('/callback', function(req, res) {
// your application requests refresh and access tokens
// after checking the state parameter
const code = req.query.code || null;
const state = req.query.state || null;
const storedState = req.cookies ? req.cookies[stateKey] : null;
res.clearCookie(stateKey);
const authOptions = {
url: 'https://accounts.spotify.com/api/token',
form: {
code: code,
redirect_uri: REDIRECT_URI,
grant_type: 'authorization_code',
},
headers: {
Authorization: `Basic ${new Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64')}`,
},
json: true,
};
request.post(authOptions, function (error, response, body) {
if (!error && response.statusCode === 200) {
const access_token = body.access_token;
const refresh_token = body.refresh_token;
var options = {
url: 'https://api.spotify.com/v1/me',
headers: { Authorization: 'Bearer ' + access_token },
json: true,
};
// use the access token to access the Spotify Web API
request.get(options, function(error, response, body) {
console.log(body);
});
// we can also pass the token to the browser to make requests from there
res.redirect('/#' + querystring.stringify({
access_token: access_token,
refresh_token: refresh_token,
}));
} else {
res.redirect(`/#${querystring.stringify({ error: 'invalid_token' })}`);
}
});
});
You need to add Accept-Encoding with application/json in axios.post header.
The default of it is gzip
headers: {
"content-type": "application/x-www-form-urlencoded",
'Accept-Encoding': 'application/json'
Authorization: `Basic ${new Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString("base64")}`,
}

How to redirect to login page if JWT is not valid, using Express with API

I've built an Express app that contains an API and a front end. By using Axios the front end can request data (e.g. a user-object or a todo-object) from the API, which will validate the offered JWT with its middleware. If the jwt.verify() errs, the protected routes won't fire. This all works fine.
My question is: how do I set up the front end such that any page-request will redirect to a login page if the browser-stored JWT is not valid (excluding the login and register pages, to prevent circular redirection)? Do I have to preface every .ejs-file with an Axios.post() that sends the browser-stored JWT for verification, or is there a best practice that I am missing?
My goal, when an invalid JWT is offered, is to have the API routes return a json-object (e.g. { err: "invalid token offered" }), and to have all the front end routes redirect the user to the login page.
Some sample code below.
server.js
// API Routes
app.use('/api/todos', CheckToken, APITodosRouter)
app.use('/api/auth', APIAuthRouter)
// Front-end Routes
app.use('/', indexRouter)
app.use('/todos', todosRouter)
app.use('/auth', authRouter)
todos.ejs (This works fine)
// get todos from db
let todosData
const getTodos = async () => {
let response = await axios.get('/api/todos/all', {
headers: {
'Content-Type': 'application/json',
'authorization': `Bearer ${localStorage.access_token}`
}
})
if (!response) return console.log({ msg: "no response received."})
if (!response.data) return console.log({ msg: "no data received."})
if (!response.data.payload) return console.log({ msg: "no todos found."})
todosData = response.data.payload
}
// boot page
;(async () => {
await getTodos()
renderTodos() // a function that reads todosData updates the DOM accordingly
})()
checkToken.js (Middleware)
const jwt = require('jsonwebtoken')
const checkToken = (req, res, next) => {
const ah = req.headers.authorization
const token = ah && ah.split(' ')[1]
if (!token) return res.json({ msg: "No token offered."})
jwt.verify(token, process.env.TOKEN_SECRET, (err, user) => {
if (err) return res.json({ msg: "Invalid token offered."})
req.user = user
next()
})
}
module.exports = checkToken

Authentication for protected routes using express/jwt

When the user logins, the access token is created and sent to the user, it is then stored in sessionStorage. Everything before this works fine. My problem is that I do not know how to use the access token to gain access to protected routes.
express app.js (smoothies is the protected route)
app.get('/smoothies', requireAuth, (req, res) => res.render('smoothies'));
authMiddleware.js
const User = require('../models/User');
const requireAuth = (req, res, next) => {
const authHeader = req.headers['authorization']
const token = authHeader && authHeader.split(' ')[1]
// check json web token exists & is verified
if (token) {
jwt.verify(token, 'night of fire', (err, decodedToken) => {
if (err) {
console.log(err.message);
res.redirect('/login?err=auth');
} else {
console.log(decodedToken);
next();
}
});
} else {
res.redirect('/login?err=auth');
}
};
// check current user
module.exports = { requireAuth };
smoothies.ejs
var myHeaders = new Headers();
myHeaders.append("Authorization", `Bearer ${token}`);
var requestOptions = {
method: 'GET',
headers: myHeaders,
redirect: 'follow'
};
fetch("http://localhost:3000/smoothies", requestOptions)
.then(response => response.text())
.then(result => console.log(result))
.catch(error => console.log('error', error));
//Should I be even doing this fetch GET request on smoothie.ejs?
})
Smoothies is the protected route. When I try to use Postman and send a GET request to (/smoothies) using authorization : bearer token, it works and I am able to access /smoothies. However, if I try on the real application, I was denied access even with access token in my sessionStorage. When I console.log req.headers['authorization'], it was undefined so I am guessing my GET request from smoothie.ejs does not work. Does anyone know what is the solution?

How to get user email using Google Sign In expo Auth Session?

At moment im using this snippet of code to sign in to google, but i cant get user email… anyone know how to do this?
var LoginGoogle = () => {
const [request, response, promptAsync] = Google.useAuthRequest({
androidClientId: 'xxxxxxx.apps.googleusercontent.com',
expoClientId: 'xxxxxxx.apps.googleusercontent.com'
},{
scopes: ["email"]
},{});
React.useEffect(() => {
if (response?.type === 'success') {
const { authentication } = response;
console.log(response);
}
}, [response]);
return (
<GoogleSocialButton disabled={!request} onPress={() => {promptAsync()}} />
)
}
response returns object with links instead of email
I wish this is written in the expo docs. I would like to add a few points from the first answer:
First if you need code snippets on how to fetch user data after getting the access token, you can refer to this github issue: https://github.com/expo/expo/issues/8384
access token can be received by the following code after receiving the response object:
const { authentication: { accessToken } } = response;
then, you can create a function like this:
async function fetchUserInfo(token) {
const response = await fetch('https://www.googleapis.com/oauth2/v3/userinfo', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
},
});
return await response.json();
}
and get the user object (which contains the user email, profile, photo, etc) by something like this inside an async function:
const user = await fetchUserInfo(accessToken);
But NOTE for the user object, using https://www.googleapis.com/oauth2/v2/userinfo and https://www.googleapis.com/oauth2/v3/userinfo, will yield slightly different result/object ; in particular for v3, since Google implements the OpenID Connect API, there is no "id" attribute anymore, "id" will be called "sub".
sources:
How to identify a Google OAuth2 user?
https://developers.google.com/assistant/identity/google-sign-in-oauth
https://github.com/expo/expo/issues/8384
Example of a user object in v3:
Object {
"email": "xxxxx#gmail.com",
"email_verified": true,
"family_name": "John Deer",
"given_name": "John",
"hd": "gmail.com",
"locale": "en",
"name": "John Deer",
"picture": "https://lh3.googleusercontent.com/a/asdfjasdklfjaslkf",
"sub": "10998837733652322",
}
Hope this helps someone in the future...!
EDIT: if you need the id_token checkout this one:
expo-auth-session/providers/google Google.useAuthRequest
I am using AuthSession as well in my RN app and I stumbled with this problem. After going through Google API Docs, found out you can pass the access token from the useAuthRequest response to https://www.googleapis.com/oauth2/v3/userinfo?access_token= ACCESS_TOKEN.

Get AWS Cognito user from ID Token retrieved from Token Endpoint

I am building a React Native app using Expo and AWS Cognito with AWS Amplify, and I am trying to enable signing in with Facebook, Google, etc. using AWS
I can create a user and sign in using Cognito APIs without any issue.
Using third-parties, though, requires using the Expo AuthSession functionality.
The functionality itself works fine, and I am able to get all the way to retrieving the proper tokens from my /oauth2/token endpoint.
However, as far as Amplify is concerned (and I am aware), the user is not signed in, so when I try to get Auth.currentAuthenticatedUser(), null is returned.
// Open URL in a browser
openURL = async (url) => {
let result = await AuthSession.startAsync({ authUrl: url })
this.getTokenbyCode(result.params.code)
};
getTokenbyCode = async (code) => {
const details = {
grant_type: 'authorization_code',
code,
client_id: '10eavoe3ufj2d70m5m3m2hl4pl',
redirect_uri: AuthSession.getRedirectUrl()
}
const formBody = Object.keys(details)
.map(
key => `${encodeURIComponent(key)}=${encodeURIComponent(details[key])}`
)
.join("&");
await fetch(
'https://presentor.auth.us-west-2.amazoncognito.com/oauth2/token',
{
method: "POST",
headers: {
'Content-type': 'application/x-www-form-urlencoded;charset=UTF-8'
},
body: formBody
}
)
.then(async (res) => {
console.log('res: ', res);
let resJSON = await res.json();
let idToken = await resJSON.id_token;
let decodedToken = jwt(idToken);
let userData = {
Username : decodedToken["cognito:username"],
Pool : Auth.userPool
}
})
.catch(error => {
console.log('error: ', error);
});
}
When I decode the token, I see the payload as I expect, but if I want to, for example, utilize the APIs to refresh the token if it expires, I have to workaround manually (check for expiration and retrieve a new token if it's expired).
Am I missing something basic?
Ok, I figured it out. Not sure if this is the right path, but it's pretty clean and it works, so I'm good with it.
Create CognitoIdToken, CognitoAccessToken, and CognitoRefreshToken objects using amazon-cognito-identity-js
Create a user session from those tokens
Create a user from that user session
await fetch(
'TOKEN ENDPOINT',
{
method: "POST",
headers: {
'Content-type': 'application/x-www-form-urlencoded;charset=UTF-8'
},
body: formBody
}
)
.then(async (res) => {
const IdToken = new CognitoIdToken({ IdToken: tokenRequestJson.id_token });
const AccessToken = new CognitoAccessToken({ AccessToken: tokenRequestJson.access_token });
const RefreshToken = new CognitoRefreshToken({ RefreshToken: tokenRequestJson.refresh_token })
try {
let userSession = new CognitoUserSession({ IdToken, AccessToken, RefreshToken });
console.log('userSession: ', userSession);
const userData = {
Username: userSession.idToken.payload.email,
Pool: userPool
};
console.log('userData: ', userData);
cognitoUser = new CognitoUser(userData);
cognitoUser.setSignInUserSession(userSession);
cognitoUser.getSession((err, session) => { // You must run this to verify that session (internally)
if (session.isValid()) {
console.log('session is valid');
this.setState({user: cognitoUser})
this.props.navigation.navigate('AuthLoading')
} else {
console.log('session is not valid: ', session);
}
})
}
catch (FBSignInError) {
console.log('FBSignInError: ', FBSignInError)
}
})
.catch(fetchError => console.log('fetchError: ', fetchError))