Node Express Passport with Angular - not compatible? - javascript

I'm working on a simple web app running Node/Express on the server using Passport to authenticate via Google, etc. The client is using Angular.
I'm unable to get Angular to play nice with Passport to log into Google though. My setup is like below:
Node/Express/Passport REST endpoint:
app.get("/auth/google",
function(req, res, next) {
passport = req._passport.instance;
passport.authenticate("google", { scope: ['https://www.googleapis.com/auth/userinfo.profile', 'https://www.googleapis.com/auth/userinfo.email'] })(req, res, next);
});
The client side...
myModule.controller("myController", function($q, $scope, $http) {
$scope.loginWithGoogle = function() {
$http.get("myDomain/auth/google").then(function(response) {
doStuff();
});
}
...called by a simple:
<button ng-click="loginWithGoogle ()">Login</button>
The error I'm getting is 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'myDomain' is therefore not allowed access.'
However, if I access the REST endpoint directly, by going to myDomain/auth/google in the browser, it works fine. Or I can make a simple HTML Login and get it to work too. So I'm guessing something is going wrong in the wire between my client and server.
I've searched for solutions and could not find any that worked. For instance, I read a suggestion to use $http.jsonp instead of $http.get. That yielded some progress - if a user is already logged into Google (via gmail, other apps, etc.) then they authenticate fine. But if the user isn't already logged into Google, I get an error 'Uncaught SyntaxError: Unexepected token <'. I think this is the HTML that Google is sending back for their login page.
Is there a way I can get Angular to play nice with Node/Express/Passport here? Thanks in advance!

Set your CORS headers everytime while you're sending your response, even if you've handled them in your main file.

Related

Add SameSite Attribute to Passport.js Session Cookie

I am authenticating against an OIDC service using Passport.js with the OAuth2Strategy strategy. The application makes some cross-domain requests to services that need the connect.sid cookie that is set by Passport. Chrome is promising to stop supporting these requests unless the SameSite attribute of the cookie is set to "Lax".
I'm not sure how to do that as the setting of the cookie is internal to Passport. Any suggestions? Below is the relevant function call that lives in the callback route given to the OIDC service.
passport.authenticate("oauth2", function (err, user, info) {
if (err) {
req.flash('error', err);
res.redirect('/login_error/');
}
else if (!user) {
req.flash('error', 'Unable to locate user account.');
res.redirect('/login_error/');
}
else {
req.logIn(user, (err) => {
if (err) { return next(err); }
return res.redirect('/user_profile/);
});
}
})(req, res, next);
Answer: Try using href. Or set-up proxy on the client app to the server app.
I had faced the same issue. Have researched far and wide. Summary below.
Use-case and tech-stack: Using Passport.js to implement Google OAuth authentication with React and Express. Using CORS in express. Also, set-up a middleware in Express to add Access-Control-Allow-Origin, Access-Control-Allow-Headers and Access-Control-Allow-Credentials to each response. Still did not work.
Solution: Only solution that seems to work is to use an href from the front-end to call the authentication URL (e.g. /auth/google). Or to set-up a proxy from the client to redirect requests to the server (Have not tried or tested this yet).
Long-term solution: Passport.js needs to add same-site=none to the cookie it sends to the client for chrome to allow redirecting the requests with fetch.
Core issues: axios doesn't work due to redirect issue. OAuth API need to redirect to client URL after authentication. Fetch doesn't work due to Chrome same-site=lax cookie issue. XHR faces issues as well. Perhaps a combination of redirect, cookies and CORS challenges.
P.S. Also learned that setting res.header('Access-Control-Allow-Origin', '*') in Express doesn't work for axios. axios seems to require a specific domain

Wrong audience for JWT using ADAL.js

I'm using the adal-angular library (but not with Angular) in my SPA to try to acquire an access token that I can use to call the SharePoint APIs (https://<my-tenant>.sharepoint.com/_api/).
I've registered an application in Azure AD and enabled the implicit flow in the manifest, and I'm now running my SPA locally, which is why there's a localhost redirect URI. The code below is being executed on startup:
const context = new AuthenticationContext({
clientId: '<my-client-id>',
redirectUri: 'http://localhost:3000/signin',
popUp: true,
loginResource: 'https://<my-tenant>.sharepoint.com',
callback: () => window.location.reload()
});
const user = context.getCachedUser();
if (!user) {
context.login();
} else {
context.acquireToken('https://<my-tenant>.sharepoint.com', (error, token) => {
console.log(error, token);
});
}
I'm already logged into the SharePoint site, so with this config everything happens automatically and I see a JWT access token logged to the console. However, when I inspect the token, I see that the audience is <my-client-id>. When making a call to https://<my-tenant>.sharepoint.com/_api/v1.0/me using the access token, I then get a 401 response with the following error message:
{"error_description": "Exception of type 'Microsoft.IdentityModel.Tokens.AudienceUriValidationFailedException' was thrown."}
I'm pretty sure this all boils down to me not understanding the OAuth2 flow properly, but... how can I acquire a token that SharePoint can actually be used with SharePoint? Am I thinking about this the wrong way? It kinda defeats the purpose if the token retrieved by my app can only be used to authenticate against my own app.
Getting an access token to SharePoint is well described here:
OneDrive for Business authentication and sign in
You should consider first getting a token to the Discovery Endpoint:
Using an access token received for resource
https://api.office.com/discovery/ you can make a request to the
discovery API to learn which services are available
If the call is successful, the response body contains JSON data with
information about the services available for the user and your app.
{
"#odata.context": "https:\/\/api.office.com\/discovery\/v1.0\/me\/$metadata#allServices",
"value": [
{
"#odata.type": "#Microsoft.DiscoveryServices.ServiceInfo",
"capability": "MyFiles",
"serviceApiVersion": "v2.0",
"serviceEndpointUri": "https:\/\/contoso-my.sharepoint.com\/_api\/v2.0",
"serviceResourceId": "https:\/\/contoso-my.sharepoint.com\/"
}
]
}
There you should get your valid Resource ID... but the issue here may just be that you did not include a forwardslash (/) at the end of the Resource URL in your sample code.

Origin 'http://localhost:3000' is therefore not allowed access Twitter API

I'm developing a web application that uses the Twitter API REST. I have a page with Twitter accounts and a button for adding a new account. When the button is pressed, the function in the Angular controller is executed:
// Calls token to add a new Twitter user
$scope.callToken = function () {
$http.get('/token')
.then(function (response) {
console.log(response);
});
};
This is the code in the backend to serve de request. It simply redirects to Twitter to add a new user.
// routes.js
var token = express.Router();
token.route('/token')
.get(cuentasCtrl.getToken);
// Account file
exports.getToken = function(req, res) {
twitter.getRequestToken(function(err, requestToken, requestSecret) {
if (err)
res.status(500).send(err);
else {
_requestSecret = requestSecret;
res.setHeader('Access-Control-Allow-Origin', '*');
res.redirect("https://api.twitter.com/oauth/authenticate?oauth_token=" + requestToken);
}
});
};
But I get the next error:
Origin 'http://localhost:3000' is therefore not allowed access Twitter API
My server is running on localhost:3000 and If I put localhost:3000/token in my browser there is no problem. I have read the solutions for using CORS and tested other browsers but it hasn't worked for me. What have i done wrong?
The redirect you are sending in your express response is caught by the http client you use in the frontend and the redirect happens there. You are in a classical CORS situation and of course this is not how you can interact with the Twitter rest APIs.
You have to make http calls to Twitter on the node side (server to server) or use a client side client library for Twitter (https://dev.twitter.com/web/javascript)

Meteor: Get query parameters from url on server? Use case Instagram oAuth

This is easy to do with a client side router or JS on the client. But since window is not an object on the server how would one get a query parameter or even read a url from the server? I checked node examples but couldn't find anything that didn't pertain to express js.
My use case is Instagram, It sends me back a code that needs to be read on the server and then I send a http request with that code from the server to retrieve an access token.
Has to be on page load, not load then send to server via client.
Basically I want to retrieve the query of "code" http://localhost:3000/?code=5e04c2e304f24f8b8380c2ec81202139 on the server.
I read the Instagram instruction for authentication, it seems that you are using the Server-side flow. I do not know why you choose this over the Client-side authentication flow which seems to be more appropriate, I think you should re-consider which method to use. Anyway, to answer your question you could use WebApp package to define a server route in Meteor:
WebApp.connectHandlers.use("/", function(req, res, next) {
if (req._parsedUrl.pathname === '/') {
const code = req.query.code;
console.log(code);
// get ACCESS_TOKEN with code
// if you need to redirect user after authorization
// res.writeHead(302, {
// Location: '/route/after/authorization'
// });
// res.end();
}
// comment this if you redirect to another route
next();
});

PassportJS callback switch between http and https

Right now, when I visit my page at https://example.com and click login, it goes to https://example.com/auth/facebook, which then does the facebook stuff and ends up calling back http://example.com/auth/facebook/callback. I can't seem to get it to call back using the https scheme (but only when the request cycle started in https).
right now, when viewed through an https iframe (facebook canvas app), I get the error
[blocked] The page at
'https://apps.facebook.com/example/?fb_source=notification&ref=notif&notif_t=app_notification'
was loaded over HTTPS, but ran insecure content from
'http://example.com/auth/facebook/callback?code=AQD5TUeTP…yXC0ZM8S45V2iTta629IaquCpAqVUbhAvNCFveaDBlbKg4J4#=':
this content should also be loaded over HTTPS.
passport.use(new FacebookStrategy({
clientID: process.env.FB_CLIENT,
clientSecret: process.env.FB_SECRET,
callbackURL: "/auth/facebook/callback",
profileFields: ['id']
},...
app.get('/auth/facebook',
passport.authenticate('facebook', {
scope: ["read_stream"]
})
);
app.get('/auth/facebook/callback',
passport.authenticate('facebook', {
failureRedirect: '/#'
}),
function(req, res) {
res.redirect('/#');
});
I'm running this on heroku, where it handles the details on https.
EDIT
Apparently node provides req.connection.encrypted with information as to whether the request is https. Since I am running on heroku behind nginx where that handles all of the https before node, req.connection.encrypted will always be undefined.
Still don't know how to solve this though.
I looked into the Passport Oauth2 strategy code and checked that it uses req.connection.encrypted to check if it is in a secure connection.
It also checks for proxies in case the server code runs behind one. It is possible to tell passport to trust a proxy if you know that you are behind one.
It seems that since SSL is handled by nginx on Heroku, req.connection.encrypted is always "undefined". (groups.google.com/forum/#!topic/express-js/Bm6yozgoDSY)
Nginx handles all of the HTTPS on Heroku so node never sees req.connection.encrypted being anything other than "undefined".
To solve the problem you have to tell passport to trust the proxy adding the line
app.enable("trust proxy");
to your express server.
I've also learned that we can accomplish the same thing by adding another property called "proxy:true" to the googleStrategy such as below:
passport.use(new GoogleStrategy({
clientID: keys.googleClientID,
clientSecret: keys.googleClientSecret,
callbackURL: '/auth/google/callback',
proxy: true }
Nginx handles all of the HTTPS on Heroku so node never sees req.connection.encrypted being anything other than undefined. By digging through the passportjs repositories I found that there is a check for the app to have "trust proxy" enabled. To solve this problem, add the line
app.enable("trust proxy");
to your express server.
I am sorry, this answer is kind of lame, I cannot really give you a specific answer based on the nature of the thing.
I believe that your callback url in the facebook api console is probably http, if you change it to https it should work.
If that does not work, you could try changing your callbackURL to a full url ("https://example.com/auth/facebook/callback")
Trying passport-facebook and passport-disqus I had the same problem, with Google there's no problem since it let you use http for callback but the others don't.
Setting "app.enable("trust proxy");" on main app file solved it!

Categories