I'm creating an application for YouTube that utilizes some of the Analytics APIs for Content Owners. The APIs require a user with sufficient permissions to be logged in, who can then retrieve reports for all the users of our application.
Currently our application can get YouTube User IDs, which is fine, but we need a separate account (other than the current user) to make requests to the API using the logged in user's ID.
How can I implement such a setup? I know it would involve using offline authentication and periodically refreshing the access tokens, but I'm not quite sure how to do it.
I've done a Google Analytics dashboard that refreshes the token in an interval. The admin chooses the GA Profile and it plots things. I needed to use a bunch of stuff to do that:
Npm Integration - So easy to use. Just take a look how to make methods calls sync.
google-api-nodejs-client [alpha] - Integrate it with the Npm above. It refreshes the token for you automatically when you Make Authenticated Requests
If you don't want to use google-apis-nodejs-client to refresh your token, you can use this code I've made to refresh the token by yourself:
var googleAccount = Accounts.loginServiceConfiguration.findOne({service: 'google'});
CLIENT_ID = googleAccount.clientId;
CLIENT_SECRET = googleAccount.secret;
REDIRECT_URL = '/_oauth/google?close';
var googleapis = Meteor.require('googleapis'),
OAuth2Client = googleapis.OAuth2Client,
client = getClient();
function getClient () {
var client = Meteor.sync(function (done) {
googleapis.discover('analytics', 'v3').execute(function (err, client) {
done(err, client);
});
});
if (client.err)
throw new Meteor.Error(400, 'Client not received');
return client.result;
}
function getOAuth2Client (user) {
var accessToken = user.services.google.accessToken,
refreshToken = user.services.google.refreshToken,
oauth2Client = new OAuth2Client(CLIENT_ID, CLIENT_SECRET, REDIRECT_URL);
if (user.services.google.expiresAt < +(new Date())) {
var res = Meteor.http.call("POST", "https://accounts.google.com/o/oauth2/token",
{params: {
grant_type : 'refresh_token',
refresh_token : refreshToken,
client_id : CLIENT_ID,
client_secret : CLIENT_SECRET
}, headers: {
"content-type": "application/x-www-form-urlencoded"
}});
accessToken = res.data.access_token;
Meteor.users.update({_id: user._id}, {$set: {
'services.google.accessToken': accessToken,
'services.google.expiresAt': +(new Date()) + (1000 * res.data.expires_in)
}});
}
oauth2Client.credentials = {
access_token: accessToken,
refresh_token: refreshToken
};
return oauth2Client;
}
Meteor.methods({
'getAccounts': function () {
var user = Meteor.users.findOne({_id: this.userId}),
oauth2Client = getOAuth2Client(user),
accounts = getAccounts(oauth2Client, client);
return accounts;
}
});
Related
I have been using NextJS, Twitter-lite to build an application using the twitter app, I basically am trying to implement the functionality in which users can post a tweet from the app to their twitter accounts. I have also used Next-Auth for implementing oAuth for twitter.
So after some working, I have managed to make it work and highlight my error - Access tokens. I want to fetch access tokens from the user after they have logged in,
In the below example - I run this script and a post is updated to my account indeed
Here is my code for the api/twitter/post.js
import Twitter from 'twitter-lite';
import { getSession } from 'next-auth/react'
import { getToken } from 'next-auth/jwt';
export default async (req, res) =>{
var Twit = require('twit')
const session = await getSession({ req });
const token = await getToken({
req,
secret: process.env.NEXTAUTH_SECRET
});
console.log(token.twitter.accessToken)
var T = new Twit({
consumer_key: process.env.TWITTER_CONSUMER_KEY,
consumer_secret: process.env.TWITTER_CONSUMER_SECRET,
access_token: 'process.env.TWITTER_ACCESS_TKN',
access_token_secret: 'process.env.TWITTER_ACCESS_TKN_SCRT'
});
T.post('statuses/update', { status: 'hello worldj!' }, function(err, data, response) {
console.log(data)
})
return res.status(200).json({ status: 'ok'});
}
Now this does work, when I hit my api route a new tweet is posted but only to 'my' account because I provided my access_tokens, I would like to know how I can get users access tokens, I have already signed them in using NextAuth so I'm sure I'm just missing a few things.
So I figured this one out,
In the call backs functions in the [...nextauth].js, I was just fetching the access_tokens with an outdated name and the new name is account.oauth_token
For anyone who ever encounters this issue - (Trying to fetch logged in users access tokens to access restricted twitter endpoints)
callbacks: {
async jwt({ token, user, account = {}, profile, isNewUser }) {
if (account.provider && !token [account.provider]) {
token[account.provider] = {};
}
if(account.oauth_token) {
token[account.provider].accessToken = account.oauth_token;
}
if (account.oauth_token_secret) {
token [account.provider].refreshToken = account.oauth_token_secret;
}
return token
},
This is how you get the logged in users access_token and access_token_secret
When I send mail then, it successfully sends the mail. But after a few days, it says 'invalid_grant'. Then when I again generate a new refresh token from https://developers.google.com/oauthplayground and use it, it works again. So what's the problem that access token doesn't work after few days.
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET
const GOOGLE_REDIRECT_URI = process.env.GOOGLE_REDIRECT_URI
const GOOGLE_REFRESH_TOKEN = process.env.GOOGLE_REFRESH_TOKEN
const oAuth2Client = new google.auth.OAuth2(GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REDIRECT_URI)
oAuth2Client.setCredentials({refresh_token: GOOGLE_REFRESH_TOKEN})
export async function sendMail(payload: IMailDTO): Promise<SMTPTransport.SentMessageInfo>{
const data = MailDTO.receiver(payload)
const accessToken = await oAuth2Client.getAccessToken()
const transport = nodemailer.createTransport({
// #ts-ignore
service: 'gmail',
auth: {
type: 'OAUTH2',
user: 'user#gmail.com',
clientId: GOOGLE_CLIENT_ID,
clientSecret: GOOGLE_CLIENT_SECRET,
refreshToken: GOOGLE_REFRESH_TOKEN,
accessToken,
}
})
const mailOptions = {
from: 'no-reply <no-reply#gmail.com>',
to: data.to,
subject: data.subject,
text: data.text,
html: data.html
}
let result = await transport.sendMail(mailOptions)
if(!result){
throw new Error("Email not sent. Try again.")
}
return result
}
The issue is not with your access token the issue is with your refresh token.
The first is the OAuth Playground will automatically revoke refresh tokens after 24h. You can avoid this by specifying your own application OAuth credentials using the Configuration panel.
The second being that if you have supplied your own OAuth Credentials and your application is still in testing then it will expire after seven days.
A Google Cloud Platform project with an OAuth consent screen configured for an external user type and a publishing status of "Testing" is issued a refresh token expiring in 7 days.
To have a refresh token that will not expire you need to create it using your own credentials and make sure to set your application in production.
I'm trying to use ably.io with Angular and Azure Functions using the JWT way of authenticating since it's secure, but I'm having issues with configuring the angular side of it. The use case is for a live auction site to update bids in realtime. There isn't a specific angular tutorial for this so I'm trying to piece it together. Also this code
realtime.connection.once('connected', function () {
console.log('Client connected to Ably using JWT')
alert("Client successfully connected Ably using JWT auth")
});
never throws the alert so I don't think it's working right. I used to have it working where I wasn't using ably JWT, but had the API key on the client-side in a component like this
let api = "<api key>";
let options: Ably.Types.ClientOptions = { key: api };
let client = new Ably.Realtime(options); /* inferred type Ably.Realtime */
let channel = client.channels.get(
"auctions"
);
and I could subscribe to that channel and update auctions accordingly by their id inside ngOnInit()
channel.subscribe(message => {
const auction = this.posts.find(action => {
return action.id === message.data.auctionId;
});
if (auction) {
auction.currentBid = message.data.lastBid;
}
});
but I need to switch this logic for JWT and somehow feed that JWT token into different components as well.
Ably.io JWT tutorial reference
I put the following in my angular login service
login(email: string, password: string) {
const authData: AuthDataLogin = { email: email, password: password };
return this.http
.post<{
token: string;
expiresIn: number;
userId: string;
}>(environment.azure_function_url + "/POST-Login", authData)
.pipe(takeUntil(this.destroy)).subscribe(response => {
//JWT login token. Not Ably JWT Token
const token = response.token;
this.token = token;
if (token) {
console.log('Fetching JWT token from auth server')
var realtime = new Ably.Realtime({
authUrl: "http://localhost:7071/api/AblyAuth"
});
realtime.connection.once('connected', function () {
console.log('Client connected to Ably using JWT')
alert("Client successfully connected Ably using JWT auth")
});
...
}
With my azure function already configured, When I login, the browser console outputs
GET wss://realtime.ably.io/?access_token=<token was here>&format=json&heartbeats=true&v=1.1&lib=js-web-1.1.22
SO it returns my token, but
the alert never happens
I'm not sure how to grab that JWT token that's returned to the browser. I was thinking I could store it in localStorage to share between components and clear out localStorage when user logs out, but I need to be able to subscribe to response and assign the token to a variable, but I didn't see in ably javascript tutorial how to get variable assigned to JWT Token response since it's being called with this syntax.
I appreciate any help with this!
var realtime = new Ably.Realtime({
authUrl: "http://localhost:7071/api/AblyAuth"
});
My azure function looks like
const checkAuth = require('../middleware/check-auth');
var jwt = require("jsonwebtoken")
var appId = '<APP ID>'
var keyId = '<key ID>'
var keySecret = '<key secret>'
var ttlSeconds = 60
var jwtPayload =
{
'x-ably-capability': JSON.stringify({ '*': ['publish', 'subscribe'] })
}
var jwtOptions =
{
expiresIn: ttlSeconds,
keyid: `${appId}.${keyId}`
}
console.log("JwtPayload");
console.log(jwtPayload);
console.log("jwtOptions");
console.log(jwtOptions);
module.exports = function (context, req) {
console.log("INSIDE ABLY AUTH")
// checkAuth(context, req);
console.log('Sucessfully connected to the server auth endpoint')
jwt.sign(jwtPayload, keySecret, jwtOptions, function (err, tokenId) {
if (err) {
console.log("ERR")
console.log(err)
console.trace()
return
}
context.res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate')
context.res.setHeader('Content-Type', 'application/json')
console.log('Sending signed JWT token back to client')
console.log(tokenId)
context.res = {
status: 200,
body: JSON.stringify(tokenId),
headers: {
"Access-Control-Allow-Credentials": "true",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Set-Cookie",
"Access-Control-Max-Age": "86400",
"Vary": "Accept-Encoding, Origin",
"Content-Type": "application/json"
}
};
context.done();
})
}
I'd recommend if you're wanting to intercept the JWT prior to passing it to Ably (so as to verify the contents, and also use the JWT for other components), you make use of authCallback instead of authUrl. You can use a function instead of a direct URL, within which you can call the endpoint, and do anything you like with the response, prior to passing the JWT back to the Ably constructor. I've made a JavaScript example of using the authCallback for normal Token Authentication, but the same principle applies.
As to why you're not seeing the alert, it looks like you're sending an invalid JWT for what Ably is expecting, and thus you're not successfully connecting to Ably. For example, you're specifying 'expiresIn' rather than 'exp'. For a token to be considered valid, it expected certain elements in a very specific structure, see the documentation. I'd recommend for this sort of situation where you're not certain what's breaking that you make use of verbose logging, which you can enable in the connection constructor as "log": 4.
I'm using the Meteor framework to implement Google Drive API. I have generated clientId, clientSecret and redirectUrl.
In this method I have to get the url and when I clicked the Allow button, its redirect url which I have given in the redirect url. It's give the code in the url, I have save that code.
checkForAuthorization = function() {
return new Promise(function(resolve, reject) {
console.log("checkForAuthorization method is running......");
var clientId, clientSecret, redirectUrl, oauth2Client;
clientId = "XYZ";
clientSecret = "ABC";
redirectUrl = "http://localhost:3000/home";
oauth2Client = new google.auth.OAuth2(clientId, clientSecret,
redirectUrl);
getGoogleDriveAccessToken(oauth2Client).then(function(result) {
resolve(result);
}).catch(function(error) {
reject();
});
});
};
This code is for uploading the file. The login gives me an error saying that a login is required.
var uploadFileOnGoogleDrive = function(token) {
return new Promise(function(resolve, reject) {
var fileMetadata = {
'name': 'Claremont-Coral-Pillow-12x241.jpeg'
};
var media = {
mimeType: 'image/jpeg',
body: fs.createReadStream
('/home/administrator/Pictures/Claremont-
Coral-Pillow-12x241.jpeg')
};
drive.files.create({
auth: token,
resource: fileMetadata,
media: media,
fields: 'id'
}, function (err, file) {
if (err) {
console.log("The error is ", err);
} else {
console.log('File Id: ', file.id);
}
});
});
};
What am I doing wrong?
You are using the code that is made only for the terminal (command prompt).
As the documentation says itself on the below link
https://developers.google.com/drive/api/v3/quickstart/nodejs.
The authorization flow in this example is designed for a command line
application. For information on how to perform authorization in other
contexts, see the Authorizing and Authenticating. section of the
library's README.
For your answer, you should go through and use the code given in the below API Doc link:-
https://github.com/google/google-api-nodejs-client/#authorizing-and-authenticating
Thanks
Make sure you have followed the OAuth 2.0 general process which is applicable to all applications.
When you create your application, you register it using the Google
API Console. Google then provides information you'll need later,
such as a client ID and a client secret.
Activate the Drive API in the Google API Console. (If the API isn't
listed in the API Console, then skip this step.)
When your application needs access to user data, it asks Google for
a particular scope of access.
Google displays a consent screen to the user, asking them to
authorize your application to request some of their data.
If the user approves, then Google gives your application a
short-lived access token.
Your application requests user data, attaching the access token to
the request. If Google determines that your request and the token
are valid, it returns the requested data.
For additional reference, you can follow this SO post.
I had same problem integrating with JavaScript using googleapis due to version issues not supported greater than 25.0.0.1 but when I was passing credential like this it resolved my problem
function uploadFile(tmp_path,_folderID,file_name,contentType,cb){
var __fileData = fs.createReadStream(tmp_path);
var OAuth2 = google.auth.OAuth2;
var oauth2Client = new OAuth2(
client_id,
secretKey,
redirect_url
);
// Retrieve tokens via token exchange explained above or set them:
oauth2Client.credentials = {
access_token: body.access_token,
refresh_token: refreshToken
};
var drive = google.drive({version: 'v3', auth: oauth2Client});
drive.files.create({
resource: {
name: file_name,
parents: _folderID,
mimeType: contentType
},
media: {
mimeType: contentType,
body: __fileData
}
}, function (err, response) {
if(err) cb(err,null);
cb(null,'success');
})
};
I write an example about google api using. Google NodeJS Client library. I have followed the instruction set access_type : 'offline', however the object return doesn't contains refresh_token.
My Code:
var http = require('http');
var express = require('express');
var Session = require('express-session');
var google = require('googleapis');
var plus = google.plus('v1');
var OAuth2 = google.auth.OAuth2;
const ClientId = "251872680446-rvkcvm5mjn1ps32iabf4i2611hcg086e.apps.googleusercontent.com";
const ClientSecret = "F1qG9fFS-QwcrEfZbT8VmUnx";
const RedirectionUrl = "http://localhost:8081/oauthCallback";
var app = express();
app.use(Session({
secret: 'raysources-secret-19890913007',
resave: true,
saveUninitialized: true
}));
function getOAuthClient () {
return new OAuth2(ClientId , ClientSecret, RedirectionUrl);
}
function getAuthUrl () {
var oauth2Client = getOAuthClient();
// generate a url that asks permissions for Google+ and Google Calendar scopes
var scopes = [
'https://www.googleapis.com/auth/plus.me'
];
var url = oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: scopes // If you only need one scope you can pass it as string
});
return url;
}
app.use("/oauthCallback", function (req, res) {
var oauth2Client = getOAuthClient();
var session = req.session;
var code = req.query.code;
oauth2Client.getToken(code, function(err, tokens) {
console.log("tokens : ", tokens);
// Now tokens contains an access_token and an optional refresh_token. Save them.
if(!err) {
oauth2Client.setCredentials(tokens);
session["tokens"]=tokens;
res.send(`
<html>
<body>
<h3>Login successful!!</h3>
Go to details page
<body>
<html>
`);
}
else{
res.send(`
<html>
<body>
<h3>Login failed!!</h3>
</body>
</html>
`);
}
});
});
app.use("/details", function (req, res) {
var oauth2Client = getOAuthClient();
oauth2Client.setCredentials(req.session["tokens"]);
var p = new Promise(function (resolve, reject) {
plus.people.get({ userId: 'me', auth: oauth2Client }, function(err, response) {
console.log("response : " , response);
resolve(response || err);
});
}).then(function (data) {
res.send(`<html><body>
<img src=${data.image.url} />
<h3>Hello ${data.displayName}</h3>
</body>
</html>
`);
})
});
app.use("/", function (req, res) {
var url = getAuthUrl();
res.send(`
<html>
<body>
<h1>Authentication using google oAuth</h1>
<a href=${url}>Login</a>
</body>
</html>
`)
});
var port = 8081;
var server = http.createServer(app);
server.listen(port);
server.on('listening', function () {
console.log(`listening to ${port}`);
});
The refresh token is only sent once the first time user login to your application after approving the scopes you have specified.
Edit 08/2018 : Using approval_prompt:'force' no longer works, you need to use prompt:'consent' (check #Alexander's answer)
If you want to get the refresh token each time user login (even if user has already login before and approved the scopes), you have to specify prompt:'consent' in Oauth2Client configuration :
var url = oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: scopes,
prompt : 'consent'
});
Note that this will require user to accept the specified scope each time he/she will click on your link to authenticate :
You can also disable manually the application in your account permission settings, this will revoke the application and the user will have to accept the scopes again that will trigger the refresh_token to be sent the next time you authenticate :
FYI, if you need to use access_token offline, you have to store the refresh_token server side, and refresh the access_token with the stored refresh_token when you receive status 401 from Google API. So, if you store refresh_token as you should, there is actually no need to use prompt:'consent' and force user to approve the scopes each time he/she connects to your application.
According the documentation the refresh_token is only returned on the first authorization.
You can remove the permissions manually on: https://myaccount.google.com/permissions
Also you can force the user to see the consent screen again by passing &prompt=consent in the authorization URL, just add this parameter:
var url = oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: scopes,
prompt: 'consent'
});
It worked just fine for me :)
In some cases (Web clients mostly) the refresh token is only sent the first time the user is authenticated.
If you go to apps connected to your account remove the app in question. Then try and authenticated again. Check and there should now be a refresh token.