How to implement the best OAuth authentication? - javascript

I'm using zendesk OAuth for authorization. I'm using the MERN stack and the current implementation works like this,
User clicks login and redirected to zendesk
once the user signs I get redirected back to /callback path
Where I sent another request to get an auth token
After I get the token I redirect the user to frontend as ?token=XXXX attached to the URL
Is this the correct way? How should I proceed with the token should I keep it in session storage? It's not a good idea to expose the token?
export const authCallback = (req: Request, res: Response): void => {
const body = {
grant_type: 'authorization_code',
code: req.query.code,
client_id: process.env.ZENDESK_CLIENT_ID,
client_secret: process.env.ZENDESK_SECRET,
}
axios
.post(`https://${process.env.SUBDOMAIN}.zendesk.com/oauth/tokens`, body, {
headers: {
'Content-Type': 'application/json',
}
})
.then((response) => {
const token = response.data.access_token
return res.redirect(`${process.env.ORIGIN}?token=${token}`)
})
.catch((err) => {
return res.status(400).send({ message: err.message })
})
}

Either use express-session and store the token on the server in req.session.token:
(response) => {
req.session.token = response.data.access_token;
req.session.save(function() {
res.redirect(`${process.env.ORIGIN}`)
});
}
Or send the token in a session cookie directly:
(response) => {
res.cookie("token", response.data.access_token, {
httpOnly: true,
secure: true,
sameSite: "None"
});
res.redirect(`${process.env.ORIGIN}`)
}

Related

Spotify api returning invalid refresh token even though the refresh token is new

I'm trying to get new access token from spotify by sending the refresh token to spotify token endpoints but it's returning this {error: 'invalid_grant', error_description: 'Invalid refresh token'}
this is my code:
const basic = Buffer.from(
`${import.meta.env.VITE_CLIENT_ID}:${import.meta.env.VITE_CLIENT_SECRET}`
).toString("base64");
const params = new URLSearchParams();
params.append("grant_type", "refresh_token");
params.append("refresh_token", import.meta.env.VITE_REFRESH_TOKEN);
const response = await fetch("https://accounts.spotify.com/api/token", {
method: "POST",
headers: {
Authorization: `Basic ${basic}`,
"Content-Type": "application/x-www-form-urlencoded"
},
body: params.toString()
});
const result = await response.json();
return result;
It's suppose to return a new access token but it's returning error for some reasons i don't understand.
Note: I got the access token and refresh token from this website https://alecchen.dev/spotify-refresh-token/ after inputting my client id and client secret. If i use the access token directly to make a request to spotify api it works but i need to refresh it to get a new one but it's returning error
You needs to call this format in body of POST.
grant_type = refresh_token
refresh_token = <received refresh_token>
access_token= <received access_token>
The website https://alecchen.dev/spotify-refresh-token/ has a potential leak your credential.
I will shows getting refresh token in local and update refresh token.
Demo Code.
Save as get-token.js file.
const express = require("express")
const axios = require('axios')
const cors = require("cors");
const app = express()
app.use(cors())
CLIENT_ID = "<your client id>"
CLIENT_SECRET = "<your client secret>"
REDIRECT_URI = '<your redirect URI>' // my case is 'http://localhost:3000/callback'
SCOPE = [
"user-read-email",
"playlist-read-collaborative"
]
app.get("/login", (request, response) => {
const redirect_url = `https://accounts.spotify.com/authorize?response_type=code&client_id=${CLIENT_ID}&scope=${SCOPE}&state=123456&redirect_uri=${REDIRECT_URI}&prompt=consent`
response.redirect(redirect_url);
})
app.get("/callback", async (request, response) => {
const code = request.query["code"]
await axios.post(
url = 'https://accounts.spotify.com/api/token',
data = new URLSearchParams({
'grant_type': 'authorization_code',
'redirect_uri': REDIRECT_URI,
'code': code
}),
config = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
params: {
'grant_type': 'client_credentials'
},
auth: {
username: CLIENT_ID,
password: CLIENT_SECRET
}
})
.then(resp1 => {
axios.post(
url = 'https://accounts.spotify.com/api/token',
data = new URLSearchParams({
'grant_type': 'refresh_token',
'refresh_token': resp1.data.refresh_token,
'access_token': resp1.data.access_token
}),
config = {
auth: {
username: CLIENT_ID,
password: CLIENT_SECRET
}
}
).then(resp2 => {
return response.send(JSON.stringify([resp1.data, resp2.data]));
})
});
})
// your port of REDIRECT_URI
app.listen(3000, () => {
console.log("Listening on :3000")
Install dependencies
npm install express axios cors
Run a local server and access it
node get-token.js
Open your browser and enter this address
http://localhost:3000/login
It will get code and both tokens then exchange the exchanged token.
It Will display both tokens and exchanged token in Browser.
Result
First red box is get access-token and refresh-token
Second red box is to grant the refresh-token

How to bypass Spotify API CORS redirection error

I'm trying to make a project that utilizes the Spotify web api, and am trying to implement a login/logout function using React and Express.
I'm trying to show a user's top songs only if the user is signed in - this means that there is a working access_token set in the cookies.
I'm a bit new when it comes to web development, so my understanding isn't super comprehensive, but this is my approach to doing this.
Check if there is an active access_token in cookies
If there is, proceed. If there isn't, send a post request to the backend /login route, which redirects the page to the spotify authorization, which eventually, places a token in the cookies.
When I do this, however, I get a CORS error saying that cross-origin redirection has been denied: origin null is not allowed by access-control-allow-origin. I ran the server in Firefox as well (instead of safari), and got a different error.(Reason: CORS header ‘Access-Control-Allow-Origin’ missing)
I saw this post (Spotify API CORS error with React front-end and Node back-end) where the user kind of has the same problem I do, and was a bit confused by the response. They were told to not redirect responses to API calls. Am I supposed to do everything on the client-side?
Here is some of my server-side and client code.
app.post('/refresh', (req, res) => {
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,PATCH,OPTIONS');
const refresh_token = req.body.headers.refresh_token;
const authOptions = {
url: 'https://accounts.spotify.com/api/token',
headers: {
'Authorization': 'Basic ' + (Buffer.from(client_id + ':' + client_secret).toString('base64')),
'Content-Type': 'application/x-www-form-urlencoded'
},
form: {
grant_type: 'refresh_token',
refresh_token: refresh_token},
json: true
};
request.post(authOptions, function(error, response, body) {
if (!error && response.statusCode === 200) {
const access_token = body.access_token;
res.cookie('access_token', access_token);
res.send({
'access_token': access_token
});
}
});
});
app.get('/login', (req, res) => {
console.log('logging in 3');
const scope = [
'ugc-image-upload',
'user-read-playback-state',
'user-modify-playback-state',
'user-read-currently-playing',
'streaming',
'app-remote-control',
'user-read-email',
'user-read-private',
'playlist-read-collaborative',
'playlist-modify-public',
'playlist-read-private',
'playlist-modify-private',
'user-library-modify',
'user-library-read',
'user-top-read',
'user-read-playback-position',
'user-read-recently-played',
'user-follow-read',
'user-follow-modify'
];
const state = Math.random().toString(36).slice(2,18);
const auth_query_parameters = new URLSearchParams({
response_type: "code",
client_id: client_id,
scope: scope,
redirect_uri: 'http://localhost:3001/access',
state: state
});
res.redirect('https://accounts.spotify.com/authorize?' +
auth_query_parameters.toString());
});
useEffect(() => {
console.log(Cookies.get('refresh_token'))
console.log('getting refresh token');
const refresh_token = Cookies.get('refresh_token');
setAccessToken(Cookies.get('access_token'));
axios.post('http://localhost:3001/login')
.then(res => {
console.log(res)
})
}, [])

Server gets JWT from API, how can the client get the JWT from the server?

I'm working on authentication using Next.js and Strapi.
The client sends a post request with credentials to the Next.js server. Then, the server sends a post request to the Strapi API, passing the credentials, to log the user in. The server gets a JWT token and sets it as an HTTPonly cookie.
My question is: How do I also receive the JWT on the client side to do some fetching? I'm not really sure how to do this.
Client sends post request with credentials to server:
// -- /pages/account/login.js
const handleLogin = (e) => {
e.preventDefault()
axios.post("/api/login", {
identifier: `${credentials.email}`,
password: `${credentials.password}`,
remember: stayLoggedIn,
})
.then(response => {
// I need to access the JWT here
Router.push("/")
response.status(200).end()
}).catch(error => {
console.log("Error reaching /api/login ->", error)
})
}
Server calls Strapi API and logs the user in, receiving a JWT token and setting it as a cookie:
// -- /pages/api/login.js
export default (req, res) => {
const {identifier, password, remember} = req.body;
// Authenticate with Strapi
axios.post(`${API_URL}/api/auth/local`, {
identifier, password
})
.then(response => {
const jwt = response.data.jwt; // I need to pass this back to client
console.log("Got token: ", jwt)
// Set HTTPonly cookie
if (remember === false) {
res.setHeader(
"Set-Cookie",
cookie.serialize("jwt", jwt, {
httpOnly: true,
secure: process.env.NODE_ENV !== "development",
maxAge: 60 * 60 * 24, // Logged in for 1 day
sameSite: "strict",
path: "/",
})
)
} else {
//...
}
console.log("Login successful")
res.status(200).end()
})
.catch(error => {
console.log("Error logging in", error)
res.status(400).end()
})
}
The JWT the server receives from the request must be send back to the client. How do I do this? I seem to be lost...
Thanks!
Figured it out. I can just use res.status(200).end(dataFromServer). I knew it was something simple.

HttpOnly Cookies not found in Web Inspector

I am working on user authentication for a website built using the MERN stack and I have decided to use JWT tokens stored as HttpOnly cookies. The cookie was sent in a "Set-Cookie" field in response header when I used Postman to make the request but not in the Safari Web Inspector as shown in the image below. There are no cookies found in the storage tab either.
I have simplified my React login form to a button that submits the username and password of the user for the sake of debugging
import React from "react";
const sendRequest = async (event) => {
event.preventDefault();
let response;
try {
response = await fetch("http://localhost:5000/api/user/login", {
method: "POST",
body: { username: "Joshua", password: "qwerty" },
mode: "cors",
// include cookies/ authorization headers
credentials: "include",
});
} catch (err) {
console.log(err);
}
if (response) {
const responseData = await response.json();
console.log(responseData);
}
};
const test = () => {
return (
<div>
<input type="button" onClick={sendRequest} value="send" />
</div>
);
};
export default test;
I am using express on the backend and this is my index.js where all incoming requests are first received
const app = express();
app.use(bodyParser.json());
app.use("/images", express.static("images"));
app.use((req, res, next) => {
res.set({
"Access-Control-Allow-Origin": req.headers.origin,
"Access-Control-Allow-Credentials": "true",
"Access-Control-Allow-Headers": "Content-Type, *",
"Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE",
});
next();
});
app.use(cookieParser());
// api requests for user info/ login/signup
app.use("/api/user", userRoutes);
This is the middleware that the login request is eventually directed to
const login = async (req, res, next) => {
const { username, password } = req.body;
let existingUser;
let validCredentials;
let userID;
let accessToken;
try {
existingUser = await User.findOne({ username });
} catch (err) {
return next(new DatabaseError(err.message));
}
// if user cannot be found -> username is wrong
if (!existingUser) {
validCredentials = false;
} else {
let isValidPassword = false;
try {
isValidPassword = await bcrypt.compare(password, existingUser.password);
} catch (err) {
return next(new DatabaseError(err.message));
}
// if password is wrong
if (!isValidPassword) {
validCredentials = false;
} else {
try {
await existingUser.save();
} catch (err) {
return next(new DatabaseError(err.message));
}
userID = existingUser.id;
validCredentials = true;
accessToken = jwt.sign({ userID }, SECRET_JWT_HASH);
res.cookie("access_token", accessToken, {
maxAge: 3600,
httpOnly: true,
});
}
}
res.json({ validCredentials });
};
Extra information
In the login middleware, a validCredentials boolean is set and returned to the client. I was able to retrieve this value on the front end hence I do not think it is a CORS error. Furthermore, no errors were thrown and all other API requests on my web page that do not involve cookies work fine as well.
Another interesting thing is that despite using the same data (A JS object containing {username:"Joshua", password:"qwerty"}) for both Postman and the React code, validCredentials evaluates to true in Postman and false in the Web Inspector. It is an existing document in my database and I would expect the value returned to be true, which was the case before I added cookies
May I know what I have done wrong or do you have any suggestions on how I can resolve this issue? I am a beginner at web-development
EDIT
With dave's answer I can receive the "Set-Cookie" header on the frontend. However it does not appear in the Storage tab in the web inspector for some reason.
This is the response header
This is the Storage tab where cookies from the site usually appears
If you're trying to send the request as json, you need to set the content type header, and JSON.stringify the object:
response = await fetch("http://localhost:5000/api/user/login", {
method: "POST",
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: "Joshua", password: "qwerty" }),
mode: "cors",
// include cookies/ authorization headers
credentials: "include",
});
Right now you're probably getting the equivalent of
existingUser = User.findOne({ username: undefined})
and so when you do:
if (!existingUser) {
validCredentials = false;
} else { /* ... */ }
you get the validCredentials = false block, and the cookie is set in the other block.
You can not see it because you have made it httpOnly cookie.

React Native Fetch Requests are not getting sent to my Node Js backend for some users

I've built a react native app that has a Node js backend. Users can sign In, sign up and view a profile page.
All my users can sign In but some of them can't view the profile page.
When I look at the request made to my backend, I get:
POST /UserRouter/SignIn 200 212.537 ms - 130342
Signing in works, it finds the user, returns the JWT token. When it's in the app no other requests are made. I get JSON Parse error: Unexpected EOF
Once you sign in, its supposed to immediately make a request to get your profile. With some accounts, this doesn't happen
My initial hypothesis of this problem is that the token for some users has expired, so they are not able to access protected routes. I use p***assport-jwt*** for my tokens. Hence, the backend not registering any requests.
Please find my code below:
_fetchData = () => {
AsyncStorage.getItem('jwt', (err, token) => {
fetch(`${backendUri }/UserRouter/Profile`, {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: token
}
})
.then((response) => response.json())
.then((json) => {
this.setState({name:json.name})
})
.catch((error) => {
console.log(error)
alert('There was an error ')
})
.done()
})
}
Here is my node JS code
app.get('/UserRouter/profile', passport.authenticate('jwt1', { session: false }), function (req, res) {
const token = req.headers.authorization
const decoded = jwt.decode(token.substring(4), config.secret)
User.findOne({
_id: decoded._id
},
function (err, user) {
if (err) throw err
res.json({ email: user.email, name: user.fName})
})
})
Thank you
This was the answer: https://stackoverflow.com/a/33617414/6542299
Turns out I was encoding my token with the users' document. the users' document was too large. so I just needed to reduce it

Categories