I have a React app in Electron, and I'm trying to access the spotify API using the spotify-web-api-node library. However, I'm not sure exactly how the oauth flow is meant to work inside of an Electron app... Firstly, for the redirect URL, I used this question and added a registerFileProtocol call to my file. Then I added a specific ipcMain.on handler for receiving the spotify login call from a page, which I've confirmed works with console logs. However, when I get to actually calling the authorizeURL, nothing happens?
This is part of my main.js:
app.whenReady().then(() => {
...
protocol.registerFileProtocol(
"oauthdesktop",
(request, callback) => {
console.log("oauthdesktop stuff: ", request, callback);
//parse authorization code from request
},
(error) => {
if (error) console.error("Failed to register protocol");
}
);
});
ipcMain.on("spotify-login", (e, arg) => {
const credentials = {
clientId: arg.spotifyClientId,
clientSecret: arg.spotifySecret,
redirectUri: "oauthdesktop://test",
};
const spotifyApi = new SpotifyWebApi(credentials);
console.log("spapi: ", spotifyApi);
const authorizeURL = spotifyApi.createAuthorizeURL(
["user-read-recently-played", "playlist-modify-private"],
"waffles"
);
console.log("spurl: ", authorizeURL);
axios.get(authorizeURL);
}
I'd expect the typical spotify login page popup to show up, but that doesn't happen. I'd also expect (possibly) the registerFileProtocol callback to log something, but it doesn't. What am I meant to be doing here? The authorization guide specifically mentions doing a GET request on the auth url, which is what I'm doing here...
In a desktop app it is recommended to open the system browser, and the Spotify login page will render there, as part of creating a promise. The opener library can be used to invoke the browser.
When the user has finished logging in, the technique is to receive the response via a Private URI Scheme / File Protocol, then to resolve the promise, get an authorization code, then swap it for tokens. It is tricky though.
RESOURCES OF MINE
I have some blog posts on this, which you may be able to borrow some ideas from, and a couple of code samples you can run on your PC:
Initial Desktop Sample
Final Desktop Sample
The second of these is a React app and uses a Private URI scheme, so is fairly similar to yours. I use the AppAuth-JS library and not Spotify though.
Related
I am trying to create a login for my app using discord's Oauth2 currently I am displaying a separate BrowserWindow for the API call since discords Oauth2 requires that the user clicks authorize. my API call returns the raw JSON of the acess_tokens. In my app's current state the separate window only displays the JSON. I need a way to get the JSON from within the window or from the request in a variable. I can't seem to find any way to access the raw content.
function createAuthWindow(){
var authWindow = new BrowserWindow({
width: 400,
height: 600,
show: false,
'node-integration': false,
'web-security': false,
icon: getFile('f','/src/asset/instance.png'),
});
// This is just an example url - follow the guide for whatever service you are using
var authUrl = 'http://localhost:3001/api/discord/login'
authWindow.loadURL(authUrl, (res) => {
console.log(res)
console.log(authWindow);
});
authWindow.show();
// 'will-navigate' is an event emitted when the window.location changes
// newUrl should contain the tokens you need
authWindow.webContents.on('will-navigate', function (event, newUrl) {
// More complex code to handle tokens goes here
console.log(event.code);
authWindow.webContents.session.webRequest.onCompleted({ urls: [newUrl] }, (details) => {
// Access request headers via details.requestHeaders
// Access response headers via details.responseHeaders
console.log(authWindow.webContents.code)
});
});
Sounds like your auth URL is not correct, and that you should be sending an Authorization Code Flow message, so that you can get tokens back to your app.
The usual technique for a desktop app is to:
Format the Authorization Redirect URL
Open this URL in the System Browser, which will handle redirects for you
Receive the response via a Private URI Scheme or Loopback Notification
Swap the authorization code for tokens, which your app can then use to call APIs
The redirect URL will be a value something like this, though I have not used Discord as a provider, so this may not be 100% right:
https://login.discord.com/oauth2/v2.0/authorize?
client_id=y792f434f
&response_type=code
&redirect_uri=com.mycompany.myapp:/callback
&scope=...
&state=...
&code_challenge=...
&code_challenge_method=S256
If it helps I have a couple of blog posts on OAuth for Electron Desktop Apps. It is a tricky flow to implement though ...
Initial Desktop Code Sample
Final Desktop Code Sample
I'm working on an Ionic application.
On the one hand I have an auth basic form in which people fill in their username and password. On the other hand I'd like to implement authentification with JSON Web Tokens and Node JS.
The workflow would be this one : as soon as a user fills in his credentials, they will be sent with a POST request. If these credentials are correct, the user can access to the application and gets an access token as a response.
The thing is that I'm a little bit lost with all that concepts. I built a form and sent informations with a POST request. I managed to create some APIs with Node JS and that's ok. I see how to build a authentified webservice too (e.g : https://github.com/jkasun/stack-abuse-express-jwt/blob/master/auth.js).
But I concretely don't understand the links between the html form and the authorisation check part..
To be clearer, how is it possible to make the html part and the Node JS scripts communicate together ?
Before posting that question I made many researches and found many stuff on building an authentified API. But there was very few advice on how to make it communicate with the client part (I mean the form), which is what I have to do.
If anyone has any ressources (document, Github examples..) on that, I'll greatly appreciate. But I would be very happy too if someone try to make me understand these concepts. I guess I have to improve my knowledge on all that so that I could test some POCs.
Many thanks in advance !
JWT General flow:
1- Authenticate using a strategy (You done it)
2- Deliver an accessToken along with response (You done it)
3- The client MUST store this accessToken (LocalStorage is the best place, not cookies: They are vulnerable to csrf attacks)
4- On every request you are going to make to a protected area (where user is supposed to be authenticated and authorized), make sure to send you accessToken along with it, you can put it on Authorization header, a custom header, directly in body of the request... Basicaly just make sure to send it properly.
5- On the server receiving client requests, you NEED to verify that token (You verify it by checking the signature of the accessToken).
6- If he is authorized, great, if not, send back an HTTP Unauthorized Error.
Here is my implementation using an accessToken on a header + passportjs-jwt:
Client code
To store token:
localStorage.setItem('accessToken', myAccessToken);
To send it:
const myAccessToken = localStorage.getItem('accessToken');
{
headers: {'Authorization', `Bearer ${myAccessToken}`}
}
Server code
1- Configure passport
passport.use('jwt', new JwtStrategy({
jwtFromRequest: jwtPassport.ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: myAccessTokenSecret,
passReqToCallback: true
}, (req, payload, done: (err?, user?) => void): void {
User
.findOne({where: {id: req.params.id}})
.then((user: User) => {
if (!user) {
return done(new Error(`No user found with id: ${req.params.id}`), null);
}
return done(null, user);
})
.catch((e: Error) => done(e, null));
}));
Pay attention to callback: If your callback is called, it means that passport has successfuly verified the token (It is valid). In my example, i get the user details in database and this is the user that will be returned and put in req.user object passed to my controller below:
2- Finally, the controller route (protected area):
.get('/users/:id', passport.authenticate('jwt'), (req, res, next) => {
// do stuff in protected area.
}
And that's it. If you want more security, check refreshTokens implementation.
I used passport because i found it relevant in my case, but you can write your own handler, by using jsonwebtoken and just calling its "verify" function.
You can find documentation of passport jwt strategy here => http://www.passportjs.org/packages/passport-jwt/
I want to send a request to this Amazon Alexa API.
That page contains the last 50 activities I made with my Amazon Echo. The page returns JSON. Before you can request that page, you need to authorize your account, so the proper cookies are set in your browser.
If I do something simple as:
const rp = require("request-promise");
const options = {
method: "GET",
uri: "https://alexa.amazon.com/api/activities?startTime=&size=50&offset=-1",
json: true
};
rp(options).then(function(data) {
console.log(data);
}).catch(function(err) {
console.log(err);
});
I can send a GET request to that URL. This works fine, except Amazon has no idea it's me who's sending the request, because I haven't authorized my NodeJS application.
I've successfully copied ~10 cookies from my regular browser into an incognito tab and authorized that way, so I know copying the cookies will work. After adding them all using tough-cookie, it didn't work, unfortunately. I still got redirected to the signin page (according to the error response).
How do I authorize for this API, so I can send my requests?
I have been looking for a solution for this too. The best idea I have is to use account linking, but I haven't try it yet. Looks like ASK-CLI has interface for this also, but I can't figure it out how to use it (what is that URL?). For linking account to 3rd party server is not easy, but link it back to Amazon for the json API should not be that complicated.
[ Post has been edited: see below for answer ]
I am trying to make a Vue.js2 application using this boilerplate https://github.com/vuejs-templates/webpack
I am stuck on the Authentication process, using this library: https://github.com/websanova/vue-auth and attempting to use JWT as the authentication method. Never having rolled my own authentication before, I am slightly lost.
Packages I have installed which may be relevant: passport, passport-jwt, jsonwebtokens, passport-local, passport-local-mongoose
Looking at my logs I get a successful login response, but then it attempts to get /auth/user and responds with a 401 (unauthorized error). Perusing the auth library code, a GET req to /auth/user seems to be the expected behavior.
Here is the login code (client side):
methods: {
login() {
this.$auth.login({
body: this.data.body
success(res) {
console.log('Success: '+this.context);
this.localStorage.setItem('auth-token', res.body.token);
},
error(res) {
console.log("Error: " + this.context);
this.error = res.data;
},
rememberMe: true,
fetchUser: true
});
}
}
And here is the appropriate code server-side:
Removed Link | See Edits Section *
What I am sure of is this:
the server does in fact create a JWT which is valid (checked on jwt.io) during the login request. It does not appear to be set anywhere afterwards. It just sits there and then dies. There are mentions of an Authorization Bearer header in the response, which I am certain is not being set. Nor do I understand where or how to do this. There is no token set in localStorage after the login request. I'm not sure if this should exist, but think it likely that it should. In my console, searching local storage yields some strings and large integers, but no mention of a token in it.
Edits (8+ months later)
Gist to Solution here (slashes replaced by dashes in filenames):
https://gist.github.com/wcarron27/b0db7a16df9ceff924d4a75050093c55
The reason my login method originally did not work was that the localStorage token was not set correctly, and thus failed to pass the getData method on the client-side redirect. vue-auth does this by default. By editing the url it hits in the vue-auth config, I was able to direct it to the proper route(BUT only after I properly set the localstorage token. Use Vue.http.options.rootUrl (or something, it's in the main.js file in the gist) to set the Authorization header.
In the code, You must register vue-auth on the client side main.js, and call to it in the Login.vue "login" method. The client side config directs the http calls to the specified route in main.js. In the callback, the user and tokens are set in localStorage and the Vuex store.
The Http reqs go the the API side and hit the route in accounts.js. That route uses a passport strategy defined in ./util/passport.js, as well as a setJWT function defined in ./util/jwtLib.js.
After this, the client is redirected to a route of my choice, and data is populated by my store and ajax calls. Keep in mind, that while this should solve logins, i have not verified code correctness, as basically I would have needed to post the whole of two separate codebases.
Additionally, this does not account for refresh errors. State is dropped on refresh, so do not rely on vuex for persistence. A combination of localStorage and vuex does the trick, though.
I didn't verify this but, does remove the 'this' from your code on line 7 do the magic?
methods: {
login() {
this.$auth.login({
body: this.data.body
success(res) {
console.log('Success: '+this.context);
// original code here --> this.localStorage.setItem('auth-token', res.body.token);
localStorage.setItem('auth-token', res.body.token);
},
error(res) {
console.log("Error: " + this.context);
this.error = res.data;
},
rememberMe: true,
fetchUser: true
});
}
}
I'm using the Hapi framework (nodejs) with the Bell module, working with the Twitter provider.
It was pretty simple to get a working code with the example given in the github page. I access the /login route and I get redirected to Twitter, where I authorize the app and then I'm redirected back to /login?oauth_token=xxxxxxx&oauth_verifier=xxxxxxx where I can have access to the user profile in the request.auth.credentials.
The problem came when I tried to reject the app. Instead of clicking the "Sign In" button on Twitter, I clicked the "Cancel" button and then the "Return to site name" button. This last button redirects me to /login?denied=xxxxxx and then I'm redirected (again) to Twitter to approve the app.
I tried to handle this scenario using another example in the same page https://github.com/hapijs/bell#handling-errors but can't get it to work.
server.route({
method: ['GET', 'POST'],
path: '/login',
config: {
auth: {
strategy: 'twitter',
mode: 'try'
},
handler: function (request, reply) {
if (!request.auth.isAuthenticated) {
return reply('Authentication failed due to: ' + request.auth.error.message);
}
return reply.redirect('/home');
}
}
});
It seems that before checking the request.auth it interprets the /login route and redirects to Twitter. I still don't understand very well the Bell module but it might be that the Twitter strategy is expecting the oauth_token and oauth_verifier in the request.params, but the denied param is not interpreted by the strategy and that's why the redirect happens.
Has somebody managed to handle this scenario?
I found a workaround. It's not an optimal solution but at least allows me to handle the rejection from Twitter.
I had to modify a file inside the bell module. In bell/lib/oauth.js, before the verification of oauth_token
exports.v1 = function (settings) {
var client = new internals.Client(settings);
return function (request, reply) {
var cookie = settings.cookie;
var name = settings.name;
// Sign-in Initialization
// Verify if app (Twitter) was rejected
if (name=='twitter' && request.query.denied) {
return reply(Boom.internal('App was rejected'));
}
if (!request.query.oauth_token) {
// Obtain temporary OAuth credentials
var oauth_callback = request.server.location(request.path, request);
With that change I can catch and show the auth error in the handler, without the automatic redirect.
At least this is the way I managed to make it work. The cons of this modification is that if the bell module is updated then the modification is lost and the bug arise again, unless the updated module comes already with a fix for this. So, you have to keep an eye on that.
Here's the link off the Github issue I created on the Bell repository regarding this bug.