How to SSO in another domain using express server/react client and redirect to another domain? - node.js

Problem is to create a web app that has a form that sends it to my express server, generates a token from external endpoint post with form value, uses token from external domain to make a post to external endpoint to then get authenticated into external domain.
I am able to generate token on my express server, make the second post with token to external endpoint and able to login and see the response on express server.
However, when I try to redirect my react client to the external domain, it shows a timeout message.
I've tried making the 2nd post from client with Axios and Fetch and then redirect to external domain, but it gives me CORS errors, until I turn on CORS chrome plugin, then it still gives me the same timeout message.
I've tried adding different headers to my post call to allow redirects, but no success.
const router = require('express').Router();
const xml2js = require('xml2js');
const parseString = require('xml2js').parseString;
const request2 = require('request');
// const axios = require('axios')
var setCookie = require('set-cookie-parser')
var Cookies = require('js-cookie')
require('dotenv').config();
router.post('/sso', (req, response, next)=>{
// SETTING UP XML BODY FOR TOKEN GENERATOR
// USING TEMPLATE TO BUILD XML BODY FOR TOKEN GENERATOR
// SETTING UP POST TO TOKEN GENERATOR WITH XML TEMPLATE
// DECLARING TOKEN TO PASS TO SSO POST
// PROMISE FOR RESPONSE POST TO TOKEN GENERATOR
return new Promise((resolve)=>{
// ERROR CATCH BLOCK FOR POST TO TOKEN GENERATOR
try {
// POST TO TOKEN GENERATOR USING XML TEMPLATE
request2(TokenGenerator,
(err, res, body)=>{
// PARSE TOKEN GENERATOR BODY RESPONSE
// CONVERTING TO STRING SOAP BODY
// PARSING STRING INTO JSON TO TARGET TOKEN
// DECLARING TOKEN RESPONSE IN RES WITH TOKEN VALUE FROM POST TO TOKEN GENERATOR
// ASSIGNING IT TO GLOBAL VARIABLE
})
// TRYING POST FROM CLIENT HAS BEEN COMMENTED OUT
// // response.send(token)
// // next()
// SETTING UP POST TO PARTICIPANT SSO WITH TOKEN VALUE
const secondPostToSSO = {
method: 'POST',
url: 'externaldomain.com/sso.aspx',
followAllRedirects: true,
jar: true,
headers: {
'Content-Type': 'text/html'
},
form: {
'TOKEN': token
}
}
// POST TO PARTICIPANT SSO WITH TOKEN
request2.post(secondPostToSSO,(err, response2, body2)=>{
console.log(response2.request)
var cookies = setCookie.parse(response2, {
decodeValues: true,
map: true
})
console.log(cookies)
next()
})
})
} catch (e) {
console.error(`[FATAL ERROR] - KAPUT - ${e}`)
return res.redirect('/')
}
})
})
module.exports = router
I expect the output of the server post to then redirect the client to the externaldomain.com where I'm getting the token, and making post with token to authenticate client. The outcome should be that the client has been logged from my web app to the external domain.

I was able to solve the CORS issue and complete the application a few weeks ago and I wanted to share my answer.
The way I solved the CORS issue was to remove React and use simple HTML client to the server side on the same port. I use the npm package cors (https://www.npmjs.com/package/cors). I added one line to the server.js file like this: app.use(cors());.
After that, I updated my SSO route to pass the token with response.send(token) instead of using next().
Once the token was received by the browser, remember, I have server and client on same port, client would trigger a hidden form POST once the token was received from the server.
VoilĂ , that solved it.
The API route:
const path = require('path');
const router = require('express').Router();
const app = require('express')
const xml2js = require('xml2js');
const parseString = require('xml2js').parseString;
const request2 = require('request');
const cors = require('cors')
require('dotenv').config();
// API routes
module.exports.routes = function (app) {
// POST API
app.post("/api/sso", (request, response, next)=>{
// SETTING UP XML BODY FOR TOKEN GENERATOR
const Template = {
// YOUR TEMPLATE
}
// USING TEMPLATE TO BUILD XML BODY FOR TOKEN GENERATOR
const builder = new xml2js.Builder()
const xml = builder.buildObject(Template)
// SETTING THE RIGHT ENVIRONMENT LINK FOR TOKEN GENERATOR BASED ON USER INPUT WITH IF STATEMENT
// SETTING UP POST TO TOKEN GENERATOR WITH XML TEMPLATE
const TokenGenerator = {
url: tokenGeneratorUrl,
method: 'POST',
headers: {
'Content-Type': 'text/xml'
},
body: xml
}
// DECLARING TOKEN TO PASS TO SSO POST
let TOKEN = ''
// PROMISE FOR RESPONSE POST TO TOKEN GENERATOR
return new Promise((resolve) => {
// ERROR CATCH BLOCK FOR POST TO TOKEN GENERATOR
try {
// POST TO TOKEN GENERATOR USING XML TEMPLATE
request2(TokenGenerator,
(err, res, body) => {
// PARSE TOKEN GENERATOR BODY RESPONSE
parseString(body,
(err, result) => {
// CONVERTING TO STRING SOAP BODY
const strBody = JSON.stringify(result['soap:Envelope']['soap:Body'])
// PARSING STRING INTO JSON TO TARGET TOKEN
// DECLARING TOKEN RESPONSE IN RES WITH TOKEN VALUE FROM POST TO TOKEN GENERATOR
// ASSIGNING IT TO GLOBAL VARIABLE
TOKEN = res.TokenResponse
})
// SENDING TOKEN TO CLIENT TO POST TO SSO
response.send(TOKEN)
})
} catch (e) {
console.error(`[FATAL ERROR] - KAPUT - ${e}`)
return res.redirect('/')
}
})
});
};
The hidden form on the client:
<form style="display: none;" novalidate autocomplete="off" name="hiddenForm" id="hiddenForm" action="https://api.endpoint.com/route" method="POST" class="hiddenForm">
<input type="hidden" id="TOKEN" name="TOKEN" value="">
</form>

Related

Slack OAuth HTTP Post Request returns "undefined" access token

I'm using the following code in a simple slash command app to handle OAuth for public distribution of my app:
const express = require("express");
const bodyParser = require("body-parser");
const fetch = require("node-fetch")
require('dotenv').config();
const app = express();
app.use(bodyParser.urlencoded({ extended: true }));
// App installation handling
app.get("/auth", async (req, res) => {
if (!req.query.code) {
console.log("Access denied!");
return;
}
var data = {form: {
client_id: process.env.SLACK_CLIENT_ID,
client_secret: process.env.SLACK_CLIENT_SECRET,
code: req.query.code,
redirect_uri: "https://6c0c-35-20-201-50.ngrok.io/auth"
}};
console.log(req.query.code);
// Send received code back to Slack and get Oauth2 access token
const config = {
method: "POST",
body: data,
headers: {'Content-type': 'application/x-www-form-urlencoded'}
};
console.log("We got something!");
try {
const slack_oauth_response = await fetch("https://slack.com/api/oauth.v2.access", config);
console.log("Access token granted!");
console.log(JSON.stringify(slack_oauth_response.access_token));
} catch (e) {
console.error(e);
}
res.sendStatus(200);
})
When I try using the Add to Slack button, I get a timeout error. My log results will look like this:
PS D:\Documents\SlackRegApp> node local_testing.js
1007612862405.3292595223126.481b3e25d2c29dc80af7dc21bcb84a8bc19c28ddec155a429c6651105903902f
We got something!
Access token granted!
undefined // where the access token should be
If I go ahead and just log the entirety of slack_oauth_response, it looks like this:
{"size":0, "timeout":0}
When I try to install the app via cURL, it works, like below:
curl -F code=1007612862405.3292595223126.481b3e25d2c29dc80af7dc21bcb84a8bc19c28ddec155a429c6651105903902f -F client_id=**SLACK_CLIENT_ID** -F client_secret=**SLACK_CLIENT_SECRET** https://slack.com/api/oauth.v2.access
Hoping for some help here, thanks!
I just used the Slack WebClient API to access the oauth.v2.access method instead of trying to make my own HTTP request.

Node.js pass Basic Authorization to odata service

I have a backend using Node.js with Express and it is reaching out to an Odata service to retrieve some data.
To do so it is using "odata": "^1.3.1" : https://www.npmjs.com/package/odata/v/1.3.1
However, the service requires I pass Basic Authentication in the request.
I do not see anywhere in their documentation on how to do so. Below is my call to the service within a route, how can I add Basic Authentication to the request?
const express = require("express");
const router = express.Router();
const o = require('odata').o;
process.env.NODE_TLS_REJECT_UNAUTHORIZED = 0;
router.get('/otasks', function(req, res) {
o(process.env.ODATA_SERVER)
.get()
.query()
.then(data => res.json({ payload: data }))
.catch(error => {
console.log(error);
return res.json({ payload: "error" });
});
});
module.exports = router;
Basic access authentication: When the user agent wants to send authentication credentials to the
server, it may use the Authorization header field.
Reference
According to the package document, you can provide headers to your request. Then your logic code will become like this:
...
o(process.env.ODATA_SERVER, {
headers: { 'Authorization': `Basic ${Buffer.from('username:password').toString('base64')}` },
})
...

Spotify API Authorization redirects too many times

I'm trying to use the Spotify API and following their instructions on authorization found here: https://github.com/spotify/web-api-auth-examples/blob/master/authorization_code/app.js.
Their version of the Authorization code directly uses routes in the server code, but I wanted to separate the Authorization into its own route. Here is my version of the code:
const authRouter = require("express").Router();
const config = require("../utils/config");
const request = require("request");
const querystring = require("querystring");
// Spotify client configurations
const client_id = config.CLIENT_ID;
const client_secret = config.CLIENT_SECRET;
const redirect_uri = config.REDIRECT_URI;
const stateKey = "spotify_auth_state";
//...
// #route GET /
// #desc Prompt User to Login into Spotify
authRouter.get("/", async (req, res) => {
try {
var state = generateRandomString(16);
res.cookie(stateKey, state);
// Request for user full name, profile image, and email address.
var scope = "user-read-private user-read-email";
// 1. Get the user's authorization to access data.
res.redirect(
"https://accounts.spotify.com/authorize?" +
querystring.stringify({
response_type: "code",
client_id: client_id,
scope: scope,
redirect_uri: redirect_uri,
state: state,
})
);
} catch {
console.log("error.");
}
});
// #route GET /callback
// #desc Spotify callback to request access and refresh tokens
authRouter.get("/callback", async (req, res) => {
try {
var code = req.query.code || null; // The authorization code returned by the first call.
var state = req.query.state || null;
var storedState = req.cookies ? req.cookies[stateKey] : null;
// Check the state parameter
if (state === null || state !== storedState) {
res.redirect(
"/#" +
querystring.stringify({
error: "state_mismatch",
})
);
} else {
res.clearCookie(stateKey);
const authOptions = getAuthOptions(
code,
redirect_uri,
client_id,
client_secret
);
// 2. Request an access token and refresh token
request.post(authOptions, function (error, response, body) {
if (!error && response.statusCode === 200) {
// Authorize successful. Access and Refresh Tokens granted.
var access_token = body.access_token,
refresh_token = body.refresh_token;
// Send the tokens to the client so the client can use them in requests to the Spotify API.
res.redirect(
"/#" +
querystring.stringify({
access_token: access_token,
refresh_token: refresh_token,
})
);
} else {
res.redirect(
"/#" +
querystring.stringify({
error: "invalid_token",
})
);
}
});
}
} catch {
console.log("error.");
}
});
module.exports = authRouter;
And in my app.js:
const express = require("express");
const authRouter = require("./controllers/auth");
const cors = require("cors");
var cookieParser = require("cookie-parser");
// initialize app with Express to create client to communicate with Spotify
const app = express();
app.use(cors());
app.use(cookieParser());
app.use("/", authRouter);
module.exports = app;
Now when I start my server, my browser returns: "accounts.spotify.com redirected you too many times.". When I tried starting my server in incognito mode, the Spotify login prompt appears. After I enter my credentials, it returns: "accounts.spotify.com redirected you too many times."
I've tried clearing my cookies and caches but that does not work.
In addition, I've confirmed my redirect URI for my server is the same as my redirect URI in my Spotify application's settings.
What can be the reasons the auth seems to be stuck in an infinite loop?
What's causing the infinite loop is where the code sends the access and refresh tokens back to the client:
// Send the tokens to the client so the client can use them in requests to the Spotify API.
res.redirect(
"/#" +
querystring.stringify({
access_token: access_token,
refresh_token: refresh_token,
})
);
Since I have defined the following route:
authRouter.get("/", async (req, res) => {
The access and refresh tokens are redirected to the login page, which will then lead to the callback which redirects to the login again, creating an infinite loop.
How I solved this was to redirect the access and refresh tokens to a different component, not just to "/#" + query.string... as coded in Spotify's example code.
Spotify's example code does not lead to an infinite loop since they defined a /login route for the login page, but I opted my website's root to be the login page since in my case, authenticating should be the first step.

Express server not working with the ionic frontend

I am trying to send HTTP POST request from the frontend to the backend. I first tried the backend with postman and it worked fine. However, when I tried it with the frontend it did not respond. There are no errors or warnings, it just does not post anything to the database and does not return a response. Here is my HTTP request from the frontend:
public addReg(UserOb) {
console.log("callingaddReg");
var headers = new Headers();
headers.append("content-type", "application/json");
console.log("headers appended");
return this.http
.post("http://localhost:3000/api/auth/register", UserOb)
.map(res => {
return res;
});
}
The above method logs headers appended in the console.
The backend method which handles the request is as follows:
router.post("/auth/register", isNotAuthenticated, authCtrl.register);
Here is the isNotAuthenticated middleware:
var isNotAuthenticated = function(req, res, next) {
// Check that the request doesn't have the JWT in the authorization header
var token = req.headers["authorization"];
if (token) {
return res.status(403).json({
error: null,
msg: "You are already logged in.",
data: null
});
}
next();
};
The end point location is in the index.js file
api
routes
index.js
Here is the URL to my monogdb: 'mongodb://localhost:27017/waterProject'

Dropbox api V2, get access token in query param instead of url hash (#) (Nodejs)

I'm using the official Dropbox API (V2) on my Nodejs app.
It sounds like a dumb question but I really can't find out how to get the given access token from the callback url. Actually, it is supposed to be in the hash (#) part of the url (according to their documentation and javascript client-side exemple), which is not visible by the server side...
I can't find any exemple for authentication from a nodejs app, using only the basic api.
Here is my authentication code:
My express app:
//Entry point, DC is a DropboxConnector object
app.get('/connect/Dropbox', function(req, res) {
console.log('/connect/Dropbox called');
res.redirect(DC.getConnexionURL());
});
// Callback from the authentication
app.get('/authDropbox', function(req, res) {
console.log("/authDropbox called");
console.log(url.format(req.protocol + '://' + req.get('host') + req.originalUrl));
// The above log is: 'http://localhost:8080/authDropbox'
// Here is the problem, the access token is unreachable by express
DC.getToken(req.query.code, res);
connectorList.push(DC);
});
DropboxConnector.js, my dropbox api wrapper:
var REDIRECT_URI = 'http://localhost:8080/authDropbox';
//The authentication url given by the dropbox api
getConnexionURL() {
dbx = new Dropbox({ clientId: CLIENT_ID});
var authUrl = dbx.getAuthenticationUrl(REDIRECT_URI);
console.log("AuthURL: " + authUrl);
return authUrl;
}
// #param code is supposed to be the access token...
getToken(code, res) {
if (!!code) {
dbx = new Dropbox({ accessToken: code });
console.log("Authenticated!");
res.redirect(CALLBACK_URL);
} else {
console.log("No code here");
}
}
Thanks for help !
That's correct, the contents of the fragment a.k.a. hash are not visible to the server, only the client (browser). The OAuth 2 "token" flow sends the access token on the fragment, and is mainly meant for client-side apps, e.g., JavaScript in the browser. The OAuth 2 "code" flow instead sends an authorization code as a URL parameter, for server-side apps.
If you're interested, you can find more information on the two different flows in the Dropbox /oauth2/authorize documentation.
The Dropbox API v2 JavaScript SDK unfortunately currently only supports the "token" flow, but we're tracking this as a feature request for support for the "code" flow.
If you do not want to call HTTP directly, you can use my tiny dropbox-v2-api wrapper package:
const dropboxV2Api = require(dropbox-v2-api');
const dropbox = dropboxV2Api.authenticate({
client_id: 'APP_KEY',
client_secret: 'APP_SECRET',
redirect_uri: 'REDIRECT_URI'
});
//generate and visit authorization sevice
const authUrl = dropbox.generateAuthUrl();
//after redirection, you should receive code
dropbox.getToken(code, (err, response) => {
//you are authorized now!
});
Full example (see here):
const dropboxV2Api = require(dropbox-v2-api');
const Hapi = require('hapi');
const fs = require('fs');
const path = require('path');
const Opn = require('opn');
const credentials = JSON.parse(fs.readFileSync(path.join(__dirname, 'credentials.json')));
//set auth credentials
const dropbox = dropboxV2Api.authenticate({
client_id: credentials.APP_KEY,
client_secret: credentials.APP_SECRET,
redirect_uri: 'http://localhost:5000/oauth'
});
//prepare server & oauth2 response callback
const server = new Hapi.Server();
server.connection({ port: 5000 });
server.route({
method: 'GET',
path: '/oauth',
handler: function (request, reply) {
var params = request.query;
dropbox.getToken(params.code, function(err, response){
console.log('user\'s access_token: ',response.access_token);
//call api
dropbox({
resource: 'users/get_current_account'
}, function(err, response){
reply({response: response});
});
});
}
});
server.start(function(){
//open authorization url
Opn(dropbox.generateAuthUrl());
});

Resources