I have a button in my HTML body, which is supposed to load a user's google contacts when a user clicks the button. I've registered for the required credentials and authorization via Google Cloud Platform, but for some reason, the javascript code I have is not working and clicking the button in demo mode in Visual studio does not open a new window asking me to login into my gmail account, permission for the website to access my contacts, etc. Any help in getting the API to work on my website will be much appreciated.
<button id="google-button" onclick="return auth()" style="color:white;background-color:royalblue;margin:20px;padding:5px 10px;border-style:solid;font-weight:bold">Share to all of your Google contacts</button>
<script>
function auth() {
const fs = require('fs');
const readline = require('readline');
const { google } = require('googleapis');
// If modifying these scopes, delete token.json.
const SCOPES = ['https://www.googleapis.com/auth/contacts.readonly'];
// The file token.json stores the user's access and refresh tokens, and is
// created automatically when the authorization flow completes for the first
// time.
const TOKEN_PATH = 'token.json';
// Load client secrets from a local file.
fs.readFile('credentials.json', (err, content) => {
if (err) return console.log('Error loading client secret file:', err);
// Authorize a client with credentials, then call the Google Tasks API.
authorize(JSON.parse(content), listConnectionNames);
});
function authorize(credentials, callback) {
const { client_secret, client_id, redirect_uris } = credentials.installed;
const oAuth2Client = new google.auth.OAuth2(
client_id, client_secret, redirect_uris[0]);
// Check if we have previously stored a token.
fs.readFile(TOKEN_PATH, (err, token) => {
if (err) return getNewToken(oAuth2Client, callback);
oAuth2Client.setCredentials(JSON.parse(token));
callback(oAuth2Client);
});
}
function getNewToken(oAuth2Client, callback) {
const authUrl = oAuth2Client.generateAuthUrl({
access_type: 'offline',
scope: SCOPES,
});
console.log('Authorize this app by visiting this url:', authUrl);
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
rl.question('Enter the code from that page here: ', (code) => {
rl.close();
oAuth2Client.getToken(code, (err, token) => {
if (err) return console.error('Error retrieving access token', err);
oAuth2Client.setCredentials(token);
// Store the token to disk for later program executions
fs.writeFile(TOKEN_PATH, JSON.stringify(token), (err) => {
if (err) return console.error(err);
console.log('Token stored to', TOKEN_PATH);
});
callback(oAuth2Client);
});
});
}
function listConnectionNames(auth) {
const service = google.people({ version: 'v1', auth });
service.people.connections.list({
resourceName: 'people/me',
pageSize: 10,
personFields: 'names,emailAddresses',
}, (err, res) => {
if (err) return console.error('The API returned an error: ' + err);
const connections = res.data.connections;
if (connections) {
console.log('Connections:');
connections.forEach((person) => {
if (person.names && person.names.length > 0) {
console.log(person.names[0].displayName);
} else {
console.log('No display name found for connection.');
}
});
} else {
console.log('No connections found.');
}
});
}
}
</script>
The code in your <script> tag is server-side Node.js code, not client-side JavaScript. It will not function in the browser because:
require('fs') imports the filesystem module, but no such thing exists outside of Node.js.
readline and googleapis are also Node-specific modules, so they have no meaning in client-side JS and will probably throw errors if require hasn't already.
fs.readFile(...) attempts to use the fs module (see above) to read a file at a certain path, but client-side JavaScript doesn't have access to the filesystem.
Fundamentally, OAuth negotiation should be handled on the server, not on the client. Typically, a privileged request will use data from your database, which it cannot do if the token is stored on the client.
It seems like the main problem here is confusion about what OAuth is and how it works. Here's a simplified step-by-step walkthrough of the process:
The user logs into an external service from their client.
The external service generates a code (a token) and sends it to the client.
The client receives the token (in a callback, hash param, etc.) and sends it to your server. Most often the external service will simply redirect the client to a URL on your server with the token in the query string, allowing you to grab it out of the request.
Your server stores the token for a specific user account in your database.
For privileged actions, your server sends the request to the external service and includes the token.
The server receives your request with the token and performs an action on the user's behalf.
When the external service receives a request with a token, it looks up that token and sees that it belongs to a specific user. Because that user must have logged in and authorized your app in order to create the token, the service knows that it should proceed with the action.
OAuth tokens may be permanent, but much more often they will expire after a set period of time and have to be regenerated. This means that you should never be using the token as a primary key to identify a user. As for how to regenerate an expired token, the exact details vary by provider. The service you're using (Google, in this case) will have more information on how their auth flow works, and how refreshing should be handled.
Related
I have a discord bot, written in node.js using discord.js, that accesses a slice of my Google Drive for storing user preferences and data. Everything is currently working. It only accesses my Google account.
So far, I don't understand what to do in response to the email I received from Google last night, subject "[Action Required] Migrate your OAuth out-of-band flow to an alternative method before Oct. 3, 2022".
My bot currently uses a slightly modified version of the node.js quickstart code, found here:
https://developers.google.com/drive/api/quickstart/nodejs
(the modifications check for environment variables for authorization data, before checking for that data in files).
The bot is hosted at Heroku as a worker dyno (not a web dyno) and frankly it doesn't seem sensible that I should need to add web server functionality to the bot just to log in to Google... I'm probably missing something reasonably easy in their gobbledygook blog post and help files.
Relevant blog post: https://developers.googleblog.com/2022/02/making-oauth-flows-safer.html
Relevant help file: https://developers.google.com/identity/protocols/oauth2/native-app#redirect-uri_loopback
The relevant part of the bot's code:
// # ============ GOOGLE * google * Google ===========
const fs = require('fs');
const readline = require('readline');
const {google} = require('googleapis');
// If modifying these scopes, delete googletoken.json.
const G_SCOPES = ['https://www.googleapis.com/auth/drive.appdata',
'https://www.googleapis.com/auth/drive.file'];
// The file googletoken.json stores the user's access and refresh tokens, and is
// created automatically when the authorization flow completes for the first
// time.
const G_TOKEN_PATH = 'googletoken.json';
// Load client secrets from env or local file.
if (process.env.hasOwnProperty('GOOGLE_CREDENTIALS')) {
authorize(JSON.parse(process.env.GOOGLE_CREDENTIALS), initAll);
} else {
fs.readFile('googlecredentials.json', (err, content) => {
if (err) return console.log('Error loading client secret file:', err);
// Authorize a client with credentials, then call the Google Drive API.
authorize(JSON.parse(content), initAll);
});
}
// # =========== google's library functions =============
/**
* Create an OAuth2 client with the given credentials, and then execute the
* given callback function.
* #param {Object} credentials The authorization client credentials.
* #param {function} callback The callback to call with the authorized client.
*/
function authorize(credentials, callback) {
const {client_secret, client_id, redirect_uris} = credentials.installed;
const oAuth2Client = new google.auth.OAuth2(
client_id, client_secret, redirect_uris[0]);
// Check if we have previously stored a token.
if (process.env.hasOwnProperty('GOOGLE_TOKEN')) {
oAuth2Client.setCredentials(JSON.parse(process.env.GOOGLE_TOKEN));
callback(oAuth2Client);
} else {
fs.readFile(G_TOKEN_PATH, (err, token) => {
if (err) return getAccessToken(oAuth2Client, callback);
oAuth2Client.setCredentials(JSON.parse(token));
callback(oAuth2Client);
});
}
}
/**
* Get and store new token after prompting for user authorization, and then
* execute the given callback with the authorized OAuth2 client.
* #param {google.auth.OAuth2} oAuth2Client The OAuth2 client to get token for.
* #param {getEventsCallback} callback The callback for the authorized client.
*/
function getAccessToken(oAuth2Client, callback) {
const authUrl = oAuth2Client.generateAuthUrl({
access_type: 'offline',
scope: G_SCOPES,
});
console.log('Authorize this app by visiting this url:', authUrl);
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
rl.question('Enter the code from that page here: ', (code) => {
rl.close();
oAuth2Client.getToken(code, (err, token) => {
if (err) return console.error('Error retrieving access token', err);
oAuth2Client.setCredentials(token);
// Store the token to disk for later program executions
fs.writeFile(G_TOKEN_PATH, JSON.stringify(token), (err) => {
if (err) return console.error(err);
console.log('Token stored to', G_TOKEN_PATH);
});
callback(oAuth2Client);
});
});
}
Your credentials file should look something like this.
{
"installed": {
"client_id": "[REDACTED]",
"project_id": "daimto-tutorials-101",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_secret": "[REDACTED]",
"redirect_uris": [
"urn:ietf:wg:oauth:2.0:oob",
"http://localhost"
]
}
}
Remove the line that says "urn:ietf:wg:oauth:2.0:oob",
Note when you run your code now your going to get a web page popping up with a 400 error in it. Ignore the error. The authorization code you need is in the URL bar. This is the code that you add when your application asks for it
'Enter the code from that page here: '
This is the best you can do currently I am in talks with the Oauth2 team to trying to get some feed back on how we can get a better result then a 400 error window when running installed apps like this. Currently this is what we have.
Note google is in the process of updating all their samples but its a slow process.
note
Your code is currently authorized and authorization is stored in G_TOKEN_PATH. You need to force it to authorize the user again. To do this move that file out. I wouldn't delete it because if this doesn't work then your app will be broken just copy it someplace. And run your app again
It seems there are many issues surrounding this with very few results e.g. here with no definitive answer.
Using the javascript Google Sign-in method, which returns the OAuth credential which includes accessToken, User, etc - the token which should be present in all authenticated requests to the hosted app (where security is crucial).
Is the following a good solution for vanilla Javascript web apps client-side (in one of the .js files) and what issues may arise from it?
// 1. Sign-in
firebase.auth()
.signInWithPopup(provider)
.then((result) => {
// 2. upon successful sign-in
Axios({
url: "/user/register-token",
method: "POST",
headers: {authorization: `Bearer ${idToken}`},
data: { //empty }
}).then((result) => {
// 3. successfully registered & authenticated, proceed to authenticated dashboard route
window.location.assign('/user/dashboard');
}).catch((error) => { // handle error});
}).catch((error) => {
// 4. Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
console.log(error);
});
Steps:
Sign-in with Google signin (using popup)
On successful popup signin, send token to GET /user/register-token which takes the token, verifies it (with some additional email domain checks) and saves it to the session cookie
Upon successful token registered, go to authenticated page
Handle error
My logic:
Since the session is stored server side, but there are session Id's attached to the current browsing session, I can thus use this as a "password of sorts" to verify the firebase session.
Concerns:
The only concern is using any verified firebase token and registering it. This however is address (where I check the associated email and confirm it is part of a firebase user collection & it has a recognized and accepted domain). Thus, they are not confirmed and sent back to the login page with a HTTP/403.
Is my logic sound, are there any potential issues?
My solution was on the right track, and matched up quite well with Firebase's cookie session.
For the record, I had alot of trouble storing the bearer token in my express session, thus searched for other solutions.
According to my steps, the solutions follows the same footprint, but with a slightly different implementation:
client-side changes
The ID Token (bane of my existence for atleast 24 hours) can be retrieved using this solution.
TL;DR (client side)
firebase.auth()
.signInWithPopup(provider)
.then((result) => {
firebase.auth().currentUser
.getIdToken()
.then((idToken) => // use this idToken, NOT result.credential.idToken
//...
Finally, send that to your server to create a session, endpoint is of your choosing e.g. POST: /user/createSession
Server-side changes
You need an endpoint (mine is /user/sessionLogin - part of userRouter - see more more info). Using the code below, you can also include CSRF (I couldn't get mind to work on the client side...), createSessionCookie using the idToken above - this works perfectly w/o any issues.
router.post('/sessionLogin', (req, res) => {
// Get the ID token passed and the CSRF token.
const idToken = req.body.idToken.toString();
// const csrfToken = req.body.csrfToken.toString();
// // Guard against CSRF attacks.
// if (csrfToken !== req.cookies.csrfToken) {
// res.status(401).send('UNAUTHORIZED REQUEST!');
// return;
// }
// Set session expiration to 5 days.
const expiresIn = 60 * 60 * 24 * 5 * 1000;
// Create the session cookie. This will also verify the ID token in the process.
// The session cookie will have the same claims as the ID token.
// To only allow session cookie setting on recent sign-in, auth_time in ID token
// can be checked to ensure user was recently signed in before creating a session cookie.
const auth = admin.auth();
auth.verifyIdToken(idToken).then(value => {
console.log("Token verified")
return auth.createSessionCookie(idToken, {expiresIn})
.then((sessionCookie) => {
// Set cookie policy for session cookie.
const options = {maxAge: expiresIn, httpOnly: true, secure: true};
res.cookie('session', sessionCookie, options);
res.end(JSON.stringify({status: 'success'}));
}).catch((error) => {
console.error(error);
res.status(401).send('UNAUTHORIZED REQUEST!');
});
}).catch(reason => {
console.error("Unable to verify token");
console.error(reason);
res.status(401).send('INVALID TOKEN!');
});
});
Finally, to validate your requests and check cookie status, you need to verify your requests to the server using (or request a new login session by redirecting to /user/login):
exports.authenticate = (req, res, next) => {
const sessionCookie = req.cookies.session || '';
// Verify the session cookie. In this case an additional check is added to detect
// if the user's Firebase session was revoked, user deleted/disabled, etc.
return admin
.auth()
.verifySessionCookie(sessionCookie, true /** checkRevoked */)
.then((decodedClaims) => {
req.user = decodedClaims;
next();
})
.catch((error) => {
console.error(error);
// Session cookie is unavailable or invalid. Force user to login.
req.flash("message", [{
status: false,
message: "Invalid session, please login again!"
}])
res.redirect('/user/login');
});
};
see this for more information.
In my node project I have the following basic code to connect to Azure via a token. The login/logout works great together with our Azure:
const express = require("express");
const msal = require('#azure/msal-node');
const SERVER_PORT = process.env.PORT || 3000;
const config = {
auth: {
clientId: "XXX",
authority: "https://login.microsoftonline.com/common",
clientSecret: "XXX"
},
system: {
loggerOptions: {
loggerCallback(loglevel, message, containsPii) {
console.log(message);
},
piiLoggingEnabled: false,
logLevel: msal.LogLevel.Verbose,
}
}
};
const pca = new msal.ConfidentialClientApplication(config);
const app = express();
app.get('/', (req, res) => {
res.send("Login Logout");
});
app.get('/dashboard', (req, res) => {
// check here for valid token...
});
app.get('/login', (req, res) => {
const authCodeUrlParameters = {
scopes: ["user.read"],
redirectUri: "http://localhost:3000/redirect",
};
pca.getAuthCodeUrl(authCodeUrlParameters).then((response) => {
res.redirect(response);
}).catch((error) => console.log(JSON.stringify(error)));
});
app.get('/logout', (req, res) => {
res.redirect('https://login.microsoftonline.com/common/oauth2/v2.0/logout?post_logout_redirect_uri=http://localhost:3000/');
});
app.get('/redirect', (req, res) => {
const tokenRequest = {
code: req.query.code,
scopes: ["user.read"],
redirectUri: "http://localhost:3000/redirect",
};
pca.acquireTokenByCode(tokenRequest).then((response) => {
console.log("\nResponse: \n:", response);
res.sendStatus(200);
}).catch((error) => {
console.log(error);
res.status(500).send(error);
});
});
app.listen(SERVER_PORT, () => console.log(`Msal Node Auth Code Sample app listening on port ${SERVER_PORT}!`))
But how to properly check after that logging in if the token is still valid?
So the question is, how can I be save that the user on /dashboard has still a valid token or is logged in?
app.get('/dashboard', (req, res) => {
// check here for valid token...
});
At the end I need a node.js application that:
is safe (token-based)
has user auth (msal)
can give granular permissions on routes
Can I do all that in node.js or better doing that in client-side? But am I then reducing the security?
Once you get your authentication result the first time, this should have received tokens if authentication was successful. You should be able to parse the Id token to get information about the user. You can then use that information to create a session via the web framework that you are using. The session can be used thorough out the web app to give you information like if the user is authenticated or not, how long they are authenticated for, and what they have permission to access. Usually web frameworks will create a cookie with a session id so that requests coming in will be able to have session information, and the user won't have to authenticate every time.
If the session expires, you can try acquiring a token silently (without prompting the user) by using the token cache that is part of MSAL. When you call acquire token silent, MSAL will automatically check if the access token is valid, if not it will try to refresh the access token via the refresh token. If neither are valid, they will return an error. At this point you can fall back to prompting the user again to authenticate (via the code that you have already shared).
I found a kind of dirty way to solve my issue. Could you maybe tell me if that is a proper way? Also my solution is not safe as the user could change the client-side JS code and ignore the user auth.
Create HTML file for /dashboard:
app.get('/dashboard', function(req, res) {
res.sendFile(__dirname + "/" + "index.html");
});
and here using this JS code:
var headers = new Headers();
var bearer = "Bearer " + "ey...........Ac"; // <---- accessToken
headers.append("Authorization", bearer);
var options = {
method: "GET",
headers: headers
};
var graphEndpoint = "https://graph.microsoft.com/v1.0/me";
fetch(graphEndpoint, options)
.then(resp => {
// when error redirect to ... otherwise do log:
console.log(123);
});
on the <---- accessToken putting a valid token and it works. Only when token is valid the console.log is done.
So this works, but as I said, if the user is manipulating the code he can still see the page. Also the page is loading until the script is fired. So I cannot see a real value on this. This should happen on server-side somehow. Any idea?
Using the silent-flow was a good idea. It works great on my example.
https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/samples/msal-node-samples/standalone-samples/silent-flow
This works with the PublicClientApplication and acquireTokenSilent works also as expected.
I am using javascript sdk for AWS cognito and able to login with aws cognito and receiving tokens in response.
I can see that the user session is valid until I refresh the page. Please suggest how the user session can persist after refreshing the page.
Below is my code.
function getSession() {
let poolData = {
UserPoolId: _config.cognito.userPoolId, // Your user pool id here
ClientId: _config.cognito.clientId, // Your client id here
};
//alert(sessionStorage.getItem("SessionName"));
let userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);
cognitoUser = userPool.getCurrentUser();
cognitoUser.getSession(function (err, session) {
if (err) {
alert(err);
return;
}
console.log('session validity: ' + session.isValid());
//Set the profile info
cognitoUser.getUserAttributes(function (err, result) {
if (err) {
console.log(err);
return;
}
console.log("------>>" + result);
//document.getElementById("email_value").innerHTML = result[2].getValue();
});
});
}
good news - the SDK does this for you. Check out their code for the getsession method
You can see they store the tokens to local storage for you.
To view the tokens from Google Chrome, go to developer tools -> Application. You should see a 'Storage' section on the left hand side. Open Local Storage, the tokens are saved under the URL of the application.
You should not need to access these token directly, the SDK will fetch and save the tokens as required when you call different methods.
Is there a way to check if a user is firebase-authorized before triggering a cloud function? (Or within the function)
Yes. You will need to send the Firebase ID token along with the request (for example in the Authorization header of an AJAX request), then verify it using the Firebase Admin SDK. There is an in-depth example in the Cloud Functions for Firebase samples repository. It looks something like this (made shorter for SO post):
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const cors = require('cors')();
const validateFirebaseIdToken = (req, res, next) => {
cors(req, res, () => {
const idToken = req.headers.authorization.split('Bearer ')[1];
admin.auth().verifyIdToken(idToken).then(decodedIdToken => {
console.log('ID Token correctly decoded', decodedIdToken);
req.user = decodedIdToken;
next();
}).catch(error => {
console.error('Error while verifying Firebase ID token:', error);
res.status(403).send('Unauthorized');
});
});
};
exports.myFn = functions.https.onRequest((req, res) => {
validateFirebaseIdToken(req, res, () => {
// now you know they're authorized and `req.user` has info about them
});
});
Since the question asks for auth-based access (1) within, or (2) before a function, here's an method for the "before" case: >
Since every Firebase Project is also a Google Cloud Project -- and GCP allows for "private" functions, you can set project-wide or per-function permissions outside the function(s), so that only authenticated users can cause the function to fire.
Unauthorized users will be rejected before function invocation, even if they try to hit the endpoint.
Here's documentation on setting permissions and authenticating users. As of writing, I believe using this method requires users to have a Google account to authenticate.