How do I complete authentication using the Spotify Web API - javascript

I'm trying to generate an access token to access the web player of the SpotifyWebApi and play music in the browser. Even after trying several things, including reading Spotify's authorization documentation for the Authorization Code Flow, I'm still stuck and I can't get an access code. The documentation outlines a 3-step process. Only the first part is working for me, and I'm stuck in the 2nd step. (Spotify Docs: https://developer.spotify.com/documentation/general/guides/authorization-guide/)
Here's what I've done:
Step 1:
export const authEndpoint = "https://accounts.spotify.com/authorize";
const redirectUri = "http://localhost:3000/";
const clientID = "[redacted]";
const clientSecret = "[redacted]";
const scopes = [
"user-library-read",
"user-library-modify",
"user-read-playback-state",
"user-read-currently-playing",
"user-read-recently-played",
"user-top-read",
"user-modify-playback-state"
];
export const loginUrl = `${authEndpoint}?client_id=${clientID}&response_type=code&redirect_uri=${redirectUri}&scopes=${scopes.join(
"%20"
)}&show_dialog=true`;
const ACCESS_CODE = getCodeFromUrl();
Step 2:
const POST_OBJECT_BODY = {
grant_type: "authorization_code",
code: ACCESS_CODE,
redirect_uri: redirectUri,
client_id: clientID.toString("base64"),
client_secret: clientSecret.toString("base64"),
};
fetch("https://accounts.spotify.com/api/token", {
mode: "no-cors",
method: "POST",
body: JSON.stringify(POST_OBJECT_BODY),
}).then((response) => {
console.log("Response: ", response);
});
I had to add the "no-cors" part because I was getting CORS errors.
I've tried other authentication methods and they seem to work, but I'm not getting a proper access token. I say this because I'm unable to play the music - it kept saying "Permissions missing" despite me having the proper scopes.
Any help with the above will be appreciated. Thank you!

Just a small remark, the redirect uri you are using in step 2 has to be encoded.

Related

Auth0 authentication with Cypress

I am trying to create a login command for Cypress and noticed their blog on how to do this does not match the expected values for the Auth0 React SDK. It appears they have used a custom express app to handle the login vs using the SDK to handle this (as per the offical Auth0 documentation).
The Cypress official documentation produces a local storage key value pair that looks like the below.
const item = {
body: {
decodedToken: {
claims,
user: { ... },
audience,
client_id,
},
},
expiresAt: exp,
}
window.localStorage.setItem('auth0Cypress', JSON.stringify(item))
However the one created by the Auth0 React SDK produces something similar to:
const item = {
body: {
access_token,
audience,
client_id,
decodedToken: {
claims,
user: { ... },
encoded,
header
},
expires_in,
id_token,
scope,
token_type
},
expiresAt: exp
}
window.localStorage.setItem(`##auth0spajs##::${client_id}::${audience}::${scope}`, JSON.stringify(item))
I am able to get the https://${auth)_domain}/oauth/token request working, however am not able to work out how to get the data from the response in a way for it to fit the data structure the Auth0 react SDK wants it in.
Has anyone had any success with this?
After doing some exploring, it appears the response I get back from the /oauth/token does not contain all of the fields that the value the Auth0 React SDK outputs when it signs in.
I have also noticed that Auth0 has a guide on how to integrate with Cypress however it does not use this SDK, instead it uses the SPA SDK. That guide also uses a custom login form, where I am using the LockUI.
One thing to note is that I am not using an backend to authenticate (like in most of the examples). I using the loginWithRedirect to login as per the offical recommendation.
After a bit of investigating and help from the Auth0 team, I was successful in making this work.
Here is the code I used:
Cypress.Commands.add("login", () => {
cy.clearLocalStorage();
const email = "";
const password = "";
const client_id = "";
const client_secret = "";
const audience = "";
const scope = "";
cy.request({
method: "POST",
url: "",
body: {
grant_type: "password",
username: email,
password,
audience,
scope,
client_id,
client_secret,
},
}).then(({ body: { access_token, expires_in, id_token, token_type } }) => {
cy.window().then((win) => {
win.localStorage.setItem(
`##auth0spajs##::${client_id}::${audience}::${scope}`,
JSON.stringify({
body: {
client_id,
access_token,
id_token,
scope,
expires_in,
token_type,
decodedToken: {
user: JSON.parse(
Buffer.from(id_token.split(".")[1], "base64").toString("ascii")
),
},
audience,
},
expiresAt: Math.floor(Date.now() / 1000) + expires_in,
})
);
cy.reload();
});
});
});
You have to make sure that the config you pass in is exactly the same as the config you use in the Auth0 Provider.
Once thing to note that tripped me up, was that I was also using refresh tokens. If this is the case make sure to add offline_access to your scope.
I have a public repo to download a working solution - https://github.com/charklewis/auth0-cypress.
There is an example in the Cypress Real World App, a payment application to demonstrate real-world usage of Cypress testing methods, patterns, and workflows in addition to a Auth0 Authentication Testing Strategies Guide which details the changes in to an Auth0 application and the Cypress Real World App.
For those who come across this in the future. We created an alternative approach of testing Auth0 with Cypress which doesn't require changing code in the actual application.
The approach that we use is to run a local service that exposes the same API as Auth0. We packages this service as an NPM package. You can read about it in our blog post https://frontside.com/blog/2022-01-13-auth0-simulator/
Here is what your test end up looking like,
import auth0Config from '../../cypress.env.json';
describe('log in', () => {
it('should get token without signing in', () => {
cy.createSimulation(auth0Config)
.visit('/')
.contains('Log out')
.should('not.exist')
.given({
email: 'bob#gmail.com'
})
.login()
.visit('/')
.contains('Log out')
.logout();
});
});

Error using AWS Cognito for authentication with Hasura

i'm having some problems using lambda enviroment.
Looking to set a function that make a mutation to Hasura so I can relate Auth users of Cognito with my app information.
I set the following function Post Authentication in Lamba but it does not work.
function Add(event, context, callback) {
const userId = event.user_id;
const hasuraAdminSecret = "xxx";
const url = "xxx";
const upsertUserQuery = `
mutation($userId: String!){
insert_RegistroAnimal_users(objects: [{ id: $userId }], on_conflict: { constraint: users_pkey, update_columns: [] }) {
affected_rows
}
}`
const graphqlReq = { "query": upsertUserQuery, "variables": { "userId": userId } }
request.post({
headers: {'content-type' : 'application/json', 'x-hasura-admin-secret': hasuraAdminSecret},
url: url,
body: JSON.stringify(graphqlReq)
}, function(error, response, body){
console.log(body);
callback(null, user, context);
});
}
Followed this tutorial : https://hasura.io/docs/latest/graphql/core/guides/integrations/aws-cognito.html#introduction
What do you think is wrong with the code?
I don't think anything is wrong with the code, but to make it work with Cognito you'd need to provide your Hasura setup with a JWT claims function as shown in that same guide, https://hasura.io/docs/latest/graphql/core/guides/integrations/aws-cognito.html#create-a-lambda-function-to-add-claims-to-the-jwt. If you'd like to do it as the guide suggests, you need to create a lambda function like so;
exports.handler = (event, context, callback) => {
event.response = {
"claimsOverrideDetails": {
"claimsToAddOrOverride": {
"https://hasura.io/jwt/claims": JSON.stringify({
"x-hasura-user-id": event.request.userAttributes.sub,
"x-hasura-default-role": "user",
// do some custom logic to decide allowed roles
"x-hasura-allowed-roles": ["user"],
})
}
}
}
callback(null, event)
}
You then need to pick this function as the PreTokenGeneration trigger from your user pool settings. Then AWS Cognito will trigger this function before token generation, allowing you to add Hasura required claims to your token.
The next step is to tell Hasura where to lookup for the JWT claims by providing HASURA_GRAPHQL_JWT_SECRET during the setup, which is essentially an URL pointing to your Cognito setup, generated using the pool id.
Finally, you can obtain the idToken from your user session after a successful login, and pass that token as an Authentication header for your Hasura requests. Described here.
All of these steps were actually described in the guide you linked, but may not be as clear. I believe the reason your current setup does not work is that your Hasura setup is missing the HASURA_GRAPHQL_ADMIN_SECRET, which needs to be the same as the x-hasura-admin-secret you're using in your requests.
Mind you, if you use x-hasura-admin-secret in your app and expose it to your users which gives them admin access, that creates a potential security issue and anyone with that secret can wipe up your data. x-hasura-admin-secret should be reserved for your admin tasks and not used in an app where AWS Cognito authentication is planned to be used.

Spotify PKCE code_verifier was incorrect

I was excited to hear that I can now use the Spotify web API without having a backend application via PKCE. Unfortunately, I seem to have some sort of misunderstanding and have been unable to get it to work.
I am likely making some minor mistake along the way, but I did it once to no avail and I wiped the slate clean and tried again but still without luck. From this I gather that I must be misunderstanding the documentation.
I will explain what I am doing and hopefully someone here can point out what I'm missing or doing wrong. I'm assuming I have a fundamental conceptual misunderstanding.
I first generate a cryptographically random string using an npm package called crypto-random-string. I store that in the browser's local storage before using js-sha256 to hash it and then using another npm package called base64url to encode it.
let verifier = cryptoRandomString({length: 50})
window.localStorage.setItem('verifier', verifier)
let params = {
client_id: '[MY CLIENT ID]',
response_type: 'code',
redirect_uri: 'http://localhost:3000/callback',
code_challenge_method: 'S256',
code_challenge: base64url(sha256(verifier))
}
let endpoint = new URL('https://accounts.spotify.com/authorize');
endpoint.search = new URLSearchParams(params);
window.location = endpoint.toString();
From here, I redirect to the /authorize endpoint with the proper url parameters. I have gotten this far successfully and then been redirected accordingly to my provided redirect_uri, where I grab the given code from the url parameters.
At this point, I try the fetch to the /api/token endpoint with the client_id, grant_type, the code I got from the url params, my redirect_uri, and the locally stored code_verifier.
let params = new URLSearchParams(window.location.search);
console.log(params.get('code'));
let newParams = {
client_id: '[MY CLIENT ID]',
grant_type: 'authorization_code',
code: params.get('code'),
redirect_uri: 'http://localhost:3000/callback',
code_verifier: window.localStorage.getItem('verifier')
}
let endpoint = new URL('https://accounts.spotify.com/api/token');
endpoint.search = new URLSearchParams(newParams);
fetch(endpoint.toString(), {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}).then(data => data.json()).then(console.log)
At this point, after both of my attempts I received the error:
{ error: "invalid_grant", error_description: "code_verifier was incorrect" }
Is there anything that I am obviously doing wrong? The error leads me to believe I'm doing something wrong as far as the actual generation of the code_verifier, but I am at a loss to what that issue may be.
Someone on the Spotify forum pointed me to this answer. Not sure why exactly, but doing the encoding the following way does work:
async function sha256(plain) {
const encoder = new TextEncoder()
const data = encoder.encode(plain)
return window.crypto.subtle.digest('SHA-256', data)
}
function base64urlencode(a){
return btoa(String.fromCharCode.apply(null, new Uint8Array(a))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
}
const hashed = await sha256(verifyCode)
const codeChallenge = base64urlencode(hashed)
Previous answers and comments, in addition to OP, has documented most information needed so I will only add what helped me:
The verifier itself most be encoded as a Base64-URL.
Pseduo-code (as I myself code in C#):
verifier = Base64UrlEncode(GetRandomString(length: 50))
challenge = Base64UrlEncode(HashWithSha256(verifier))

How to get OAuth token from ebay API using express, node, javascript

What combination of requests and responses are needed to get an Oauth token from eBay? What is a runame and what headers do I need to keep eBay happy?
After three frustrating days of trying to get Ebay's oauth to give me an access token, I have finally worked it out. As the docs are pain and there is little to no help online, I have decided to post my solution here in the hope that it will help others. I am no good at StackOverflow so let me know if I need to improve my formatting.
app.get("/login/ebay", (req, res) => {
res.redirect(`https://auth.sandbox.ebay.com/oauth2/authorize?client_id=DeanSchm-TestApp-SBX-b843acc90-fd663cbb&redirect_uri=Dean_Schmid-DeanSchm-TestAp-kqmgc&response_type=code`
);
});
The first thing you need to do is redirect to this URL.
The format is like this
https://auth.sandbox.ebay.com/oauth2/authorize?client_id=&redirect_uri=&response_type=code
There is also a scope property, but I don't understand that yet, and I got back a token without is so me.
That URL takes you to the eBay login page. If you are using the sandbox, you need to create a sandbox user and login with sandbox credentials.
Once you log in, eBay will redirect you to a URL of your choosing. You enter the URL you want to be redirected to here.
It's in the ebay developer section under Get A Token From Ebay Via your Application.
This URL can be anything. you just have to handle it in node or express or whatever, because as soon as someone signs in that URL is where they are heading.
Here is how I handled it
app.get("/auth/ebay/callback", (req, res) => {
axios("https://api.sandbox.ebay.com/identity/v1/oauth2/token", {
method: "post",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization:
"Basic " +
btoa(
`client public key:client secret keys`
)
},
data: qs.stringify({
grant_type: "authorization_code",
// parsed from redirect URI after returning from eBay,
code: req.query.code,
// this is set in your dev account, also called RuName
redirect_uri: "Dean_Schmid-DeanSchm-TestAp-kqmgc"
})
})
.then(response => console.log(response))
.catch(err => console.log(err));
});
A few gotchas that got me.
Make sure you have space after "Basic " in the authorisation
header.
bota is a 3rd party library that base 64 encodes your public and
secret keys. There are many ways to do this. I just did it this way because I stole a bunch of code.
With Axios, the request body is called data but with fetch and other
methods it might be called something else like body or param
The Axios method is in a get request because of the redirect from ebay
defaults to an http get.
ebay now uses https. Make sure you are using
sandbox URLs
We also had to use JS for the eBay API and solved your mention problem with developing a new Lib. It's available here. This lib will also automatically try to refresh the token if it's expires.
This is how we obtain the oAuth token:
import eBayApi from 'ebay-api';
const eBay = new eBayApi({
appId: '-- or Client ID --',
certId: '-- or Client Secret',
sandbox: false,
siteId: eBayApi.SiteId.EBAY_US,
ruName: '-- eBay Redirect URL name --' //in this case: Dean_Schmid-DeanSchm-TestAp-kqmgc
});
// This will generate the URL you need to visit
const url = eBay.oAuth2.generateAuthUrl();
// After grant access, eBay will redirect you to RuName page and set the ?code query.
// Grab the ?code and get the token with:
eBay.oAuth2.getToken(code).then((token) => {
console.log('Token', token);
ebay.oAuth2.setCredentials(token);
// Now you can make request to eBay API:
eBay.buy.browse.getItem('v1|382282567190|651094235351')
.then(item => {
console.log(JSON.stringify(item, null, 2));
})
.catch(e => {
console.log(e);
});
});
Another example with scope can we found here.
Some hints:
with "scope" you tell eBay what you plan to use. You can find the
Descriptions here, under Sandbox/Production Keys Box. (OAuth
Scopes)
if you use axios you can use the auth config, so you dont't
need btoa:
axios("https://api.sandbox.ebay.com/identity/v1/oauth2/token", {
// ...
auth: {
username: 'appId',
password: 'certId'
}
});
To use sandbox without https, e.g. localhost, you can setup a redirect on a https site and redirec/pass the code to non-https site.

Creating a YouTube playlist with React using Google's API

I would like to create a YouTube playlist on a users account, but I have struggled to authenticate a POST to the YouTube v3 api.
I'll start by showing how far I have got with this problem.
YouTube API Documentation
The Youtube API Documentation provides details on creating a playlist, and has a working example in the API Explorer
I entered the following code into the request body:
{
"snippet":
{
"title":"Test Playlist"
}
}
This successfully created a playlist on my YouTube account with the same title. So from this I could tell that, a title was required within the body and it would require OAuth 2.0 authentication (an error is displayed if it is not enabled) using one the scopes: youtube, youtube.force-ssl, youtubepartner.
First attempt in react
The First thing I tried was similar to this:
fetch('/youtube/v3/playlists', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer' + api.youtube,
},
body: JSON.stringify({
"snippet":
{
"title":"Test"
}
})
}).then(response => response.json()).then(data => {
console.log(data)
})
api.youtube contains my YouTube api key.
Most of the formatting for this came from another API I have in the same program for getting data from spotify which works.
The response I got from this would say "Login failed" or "Authentication Error" (something along those lines)
Anyway, this is relevant because I know that my first hurdle is getting authentication.
Authentication
The YouTube API Documentation contains a guide titled Implementing OAuth 2.0 Authorization I followed the guide for client side web apps.
The first thing I noticed is that they are using a library, I found this on npm under googleapis and installed it.
When I tried to call this in React using
const {google} = require('googleapis');
I won't get deep into the error but react said "Can't convert undefined to object" and found an issue which said that googleapis is intended for server side not client side, I tried building the react app and putting it on herokuapp but got the same error. Someone else suggested using gapi-client on npm which is a node wrapper for googleapis.
The next thing I did was try the example on the npm page, which is very similar to the google example for configuring the client object. I have it so the import part and function are at the top of my app.js and then the gapi.load part activates after a button is pressed (this could be useless info but w/e)
import gapi from 'gapi-client';
//On load, called to load the auth2 library and API client library.
gapi.load('client:auth2', initClient);
function initClient() {
gapi.client.init({
discoveryDocs: ["https://www.googleapis.com/discovery/v1/apis/drive/v3/rest"],
clientId: 'YOUR_CLIENT_ID',
scope: 'https://www.googleapis.com/auth/drive.metadata.readonly'
}).then(function () {
// do stuff with loaded APIs
console.log('it worked');
});
}
I copied my client ID in from the API Console and this is the exact response I got:
FireFox
Loading failed for the with source
“https://apis.google.com//scs/apps-static//js/k=oz.gapi.en.WcpMzqgmJZU.O/m=auth2,client/rt=j/sv=1/d=1/ed=1/am=AQ/rs=AGLTcCNsTS1p4dx0iMhlrwEpiaXw4iMjOg/cb=gapi.loaded_0”.
Chrome
GET
https://apis.google.com//scs/apps-static//js/k=oz.gapi.en.WcpMzqgmJZU.O/m=auth2,client/rt=j/sv=1/d=1/ed=1/am=AQ/rs=AGLTcCNsTS1p4dx0iMhlrwEpiaXw4iMjOg/cb=gapi.loaded_0
net::ERR_ABORTED 404
That's about as far as I got and I'm not sure what to do from here, so any help is much appreciated. I hope this didn't get too convoluted but I've tried to convey my problem as clearly as possible.
So I was able to authorize the YouTube API and create a playlist.
I have a backend hosted on localhost:8888 (doesn't matter just not what react is hosted on).
here is sample code for what I put in the server.js file (for the backend)
var express = require('express');
var app = express();
var passport = require('passport');
app.use(passport.initialize());
var YoutubeV3Strategy = require('passport-youtube-v3').Strategy;
passport.use(new YoutubeV3Strategy({
clientID: YOUR_CLIENT_ID,
clientSecret: YOUR_CLIENT_SECRET,
callbackURL: 'http://localhost:8888/redirect',
scope: ['https://www.googleapis.com/auth/youtube']
},
function (accessToken, refreshToken, profile, cb) {
var user = {
accessToken: accessToken,
refreshToken: refreshToken
};
return cb(null, user)
}
));
passport.serializeUser(function(user, cb) {
cb(null, user);
});
passport.deserializeUser(function(obj, cb) {
cb(null, obj);
});
app.get('/authenticate', passport.authenticate('youtube'))
app.get('/redirect', passport.authenticate('youtube', { failureRedirect: '/login' }),
function(req, res) {
res.redirect('http://localhost:3000' + '?access_token=' + req.user.accessToken)
})
app.listen(8888)
This is using Passport.js to do oauth for me, lots of documentation can be found on the site.
In react I have it so a button will open localhost:8888/authenticate and then that will redirect back to my application. If you are using this you need to make sure that on your google API credentials you have the javascript origin as http://localhost:8888 and the redirect URI as http://localhost:8888/redirect and the correct scope and application type.
This is the function I use in my app.js (react) to make the POST
getAPIdata() {
let parsed = queryString.parse(window.location.search);
let accessToken = parsed.access_token
fetch('https://www.googleapis.com/youtube/v3/playlists?part=snippet', {
method: 'POST',
headers: {
'Content-type': 'application/json',
'Authorization': 'Bearer ' + accessToken,
},
body: JSON.stringify({
'snippet':
{
'title':this.state.inputTitle
}
})
}).then(response => response.json()).then(data => {
console.log(data)
window.alert('https://www.youtube.com/playlist?list=' + data.id)
})
}
I was actually mostly correct with the first attempt I just had the authorization incorrect.
Here's a couple sources that helped me make my solution:
Passport.js oauth tutorial
Googles OAuth 2.0 Playground
Passport.js Documentation
Passport.js facebook oauth example
Hopefully this is helpful to someone, You can use the same code i used in server.js to authenticate most services by just changing the strategy.
A live version of my application can be found here. In the console it shows the response from the POST request, this should help if you have any issues. I know the alert is bad ui but this wasn't the intended use.
Thanks for reading :)

Categories

Resources