google identity services equivalent for googleUser.getBasicProfile() - authentication

I have a react app where I'm trying to migrate from using gapi.auth2 module in the Google API Client Library for JavaScript to the Google Identity Services Library.
With gapi.auth2 module you could get the signed in users basic profile info with googleUser.getBasicProfile(). The following code is how you log a user in with the Google Identity Services Library.
Login.js
function Login(){
var tokenClient;
var access_token;
function getToken(){
tokenClient.requestAccessToken();
}
function initGis(){
tokenClient = window.google.accounts.oauth2.initTokenClient({
client_id: '********.apps.googleusercontent.com',
scope: 'https://www.googleapis.com/auth/books',
callback: (tokenResponse) => {
access_token = tokenResponse.access_token;
},//end of callback:
});
}
useEffect(()=>{
initGis();
getToken();
});
return (
<>
<p>Logging in...</p>
</>
)
}
export default Login;
How do you get the users basic profile info when using the Google Identity Services Library?

Let me keep this answer short.🙂 Once you get the access_token just invoke the following function:
const getUserProfileData = async (accessToken: string) => {
const headers = new Headers()
headers.append('Authorization', `Bearer ${accessToken}`)
const response = await fetch('https://www.googleapis.com/oauth2/v3/userinfo', {
headers
})
const data = await response.json();
return data;
}
PS: [Unfortunately] I am also working on migrating to Google Identity Services Library. 😰

After a discussion on Discord where a very helpful user explained that it can only be done server side. So the simple answer is that it can't be done client side using the Google Identity Services Library

I was faced with the same issue migrating my web app to Google Identity Services. I resolved it by using the Google Drive API About:get method, and requested user fields. This returns the user's displayName and emailAddress (plus some other data that's really not very useful). I use the drive.readonly scope, but I believe a less sensitive scope like drive.appdata or drive.file would work.

You can try this:
function getTokenInfos(token) {
var splitResponse = token.split(".");
var infos = JSON.parse(atob(splitResponse[1]));
return infos;
}

Related

NextJS/Next-Auth Backend Authentication with OAuth

I am currently building a web app based on a turborepo (monorepo) in which I want to use Discord OAuth login with next-auth. Therefore I have two modules web and api, where api is my express backend with discord.js. The web app is basically a dashboard for a Discord bot.
I figured that next-auth only provides client side authentication. So my question is how can I validate the OAuth session from the client side in the best manner?
My middleware for express currently looks like this:
function throwUnauthorized(res: Response) {
res.status(401).json({ code: 401, message: 'Unauthorized' });
}
export async function isAuthorized(req: Request, res: Response, next: NextFunction) {
try {
const authorization = req.headers.authorization;
if (!authorization) {
return throwUnauthorized(res);
}
// validate token with Discord API
const { data } = await axios.get('https://discord.com/api/oauth2/#me', {
headers: { Authorization: authorization },
});
// protect against token reuse
if (!data || data.application.id !== process.env.TC_DISCORD_CLIENT_ID) {
return throwUnauthorized(res);
}
// map to database user
let user = await User.findOne({ id: data.user.id });
user ??= await User.create({ id: data.user.id });
data.user.permissions = user.permissions;
req.user = data.user;
next();
} catch (error) {
return throwUnauthorized(res);
}
}
In this approach the Discord OAuth Token would be send via the Authorization header and checked before each request that requires Authorization. Which leads to my problem: The token needs to be validated again causing multiple request to Discord API.
Is there a better way to handle this? Because I need to map Discord user profiles to database profiles. I read that you could try decode the jwt session token from next-auth, but this did not work when I tested it.
Maybe there is a whole different project structure suggested for my project. But I thought I should separate the api and web-app since I would have needed a custom express server because it includes the Discord bot and Prometheus logging functions. I am open for suggestions and your thoughts!

API Authorization Tool

I'm working on an application with RESTful API endpoints that needs proper authorization security using an RBAC system. So far, I've looked into Keycloak. It looks promising at first but doesn't support granular authorization control of an endpoint, which is a hard requirement. For example, if I have the endpoint /object/<object:id>, a list of object IDs [1,2,3,4] and a test user, there's no way to restrict the test user to only have access to object IDs [1,2] but not [3,4] for the same endpoint. It seems the user will have access to all the IDs or none. Perhaps this can be accomplished by customizing or extending the base Keycloak server but there isn't enough documentation on the Keycloak website on how to do so.
I've done a search for other RBAC permissions systems but haven't been able to find much. Are there any authorization systems out there that can accomplish this?
but doesn't support granular authorization control of an endpoint
Check out Auth0's Fine Grained Authorization solution: https://docs.fga.dev/. (Disclaimer: I am employed by Auth0).
In your specific case you would need to create an authorization model like
type object
relations
define reader as self
And then add the following tuples in the FGA store using the Write API:
(user:test, relation:reader, object:1)
(user:test, relation:reader, object:2)
Then, in your API, you would do something like this:
const { Auth0FgaApi } = require('#auth0/fga')
const express = require('express')
const app = express()
const fgaClient = new Auth0FgaApi({
storeId: process.env.FGA_STORE_ID, // Fill this in!
clientId: process.env.FGA_CLIENT_ID, // Fill this in!
clientSecret: process.env.FGA_CLIENT_SECRET // Fill this in!
});
app.get('/objects/:id', async (req, res) => {
try {
const { allowed } = await fgaClient.check({
tuple_key: {
user: req.query.user,
relation: 'reader',
object: "object:" + req.params.id
}
});
if (!allowed) {
res.status(403).send("Unauthorized!")
} else {
res.status(200).send("Authorized!")
}
} catch (error) {
res.status(500).send(error)
}
});
const port = 3000
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})

Is there a way to call the google books api from firebase auth to get a logged in users bookshelves in "My Library"?

I have the firebase auth set up and can log in, get the consent screen to access google books account, get the token, and log out.
Google will be deprecating the gapi.auth2 module and using Google Identity Services so I'm trying to find a solution that doesn't involve using the gapi.auth2 module.
The following website has an example of how to use the Google Identity Services:
https://developers.google.com/identity/oauth2/web/guides/migration-to-gis#gis-only
In this example they use the Google Identity Services library to get the access_token to then pass it along in the The XMLHttpRequest object to request data from a logged in users account. Seeing as I can already get the access_token I'm trying to make the request without using the Google Identity Service or Google API Client Library for JavaScript and just use a XMLHttpRequest.
The below code returns a response of the index.html page and not the response from the google books API.
function getData(access_token){
if(access_token !== undefined){
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
// Typical action to be performed when the document is ready:
console.log(xhttp.responseText);
}
};
xhttp.open("GET", "books/v1/mylibrary/bookshelves/4/volumes?fields=totalItems, items(id)");
xhttp.setRequestHeader('Authorization', 'Bearer ' + access_token);
xhttp.send();
}
}
function Login(){
useEffect(()=>{
signInWithGoogle();
});
provider.addScope("https://www.googleapis.com/auth/books");
const signInWithGoogle = () =>{
signInWithPopup(auth, provider).then((result)=>{
// This gives you a Google Access Token. You can use it to access the Google API.
const credential = GoogleAuthProvider.credentialFromResult(result);
const token = credential.accessToken;
if(result.user){
getData(token);
}
}).catch((error)=>{
if(error.code === 'auth/popup-closed-by-user'){
}
});
}
return (
<p>Logging in...</p>
)
}
export default Login;
I know that "books/v1/mylibrary/bookshelves/4/volumes?fields=totalItems, items(id)" works because I've used it in a working version that doesn't use firebase as it's just a plain html version that uses the Google Services Identity library with the Google API Client Library for JavaScript. The network request look the same and have the same access_token with my adapted version and the plain html version that works.
Is there a way to call the google books API from firebase auth to get a logged in users bookshelves in "My Library"?

Anyone have a solution for generating server-side tokens for the ESRI JSAPI SDK?

There are a number of solutions to this:
use the build-in dialog provided by esri/IdentityManager (https://developers.arcgis.com/javascript/3/jsapi/identitymanagerbase-amd.html)
use a server-side proxy (https://github.com/Esri/resource-proxy)
use the identity manager initialize() method (https://developers.arcgis.com/javascript/3/jsapi/identitymanagerbase-amd.html#initialize)
But there what is missing is the ability to hook into the request for a token. I am working with ArcGISDynamicMapServiceLayer and there is no way to know if the server return a 498/499, and no way to update the url to update the token.
I started hacking around in the API to try to hook into various events with no real promise of success. What seems to be missing:
a way to detect when a token is needed
a way to update the token
Closes I came up with is listening for "dialog-create" but there is no way to disable the dialog apart from throwing an exception, which disables the layer.
I tried replacing the "_createLoginDialog" method and returning {open: true} as a trick to pause the layers until I had a token ready but since there is no way to update the layer endpoint I did not pursue this hack. It seems the only way this might work is to use the initialize() method on the identity manager.
Does anyone have knowledge of options beyond what I have outlined?
EDIT: The goal is to provide a single-sign-on experience to users of our product.
"User" is already signed in to our application
"User" wishes to access a secure ESRI ArcGIS Server MapServer or FeatureServer services from the ESRI JSAPI
"User" is prompted for user name and password
The desired flow is to acquire a token on the users behalf using a RESTful services in our product and return the appropriate token that will allow the "User" to access the secure services without being prompted.
I do not wish to use a proxy because I do not want all that traffic routed through the proxy.
I do not wish to use initialize() because it is complicated and not clear how that works apart for re-hydrating the credentials.
I do wish for an API that simply allows me to set the token on any layer services that report a 499 (missing token) or 498 (invalid token), but I cannot find any such API. The solution I am focusing on hinges on being able to update the url of an ArcGISImageServiceLayer instance with a new token.
This answer lacks in satisfaction but delivers on my requirements. I will start with the code (client-side typescript):
class TokenProxy {
private tokenAssuranceHash = {} as Dictionary<Promise<{ token: string, expiration: string }>>;
private service = new TokenService();
private timeoutHandle = 0;
watchLayer(esriLayer: ArcGISDynamicMapServiceLayer) {
setInterval(async () => {
const key = esriLayer._url.path;
const token = await this.tokenAssurance(key);
esriLayer._url.query.token = token;
}, 5000);
}
updateRefreshInterval(ticks: number) {
clearTimeout(this.timeoutHandle);
this.timeoutHandle = setTimeout(() => {
Object.keys(this.tokenAssuranceHash).forEach(url => {
this.tokenAssuranceHash[url] = this.service.getMapToken({serviceUrl: url});
});
this.updateRefreshInterval(ticks);
}, ticks);
}
async tokenAssurance(url: string) {
if (!this.tokenAssuranceHash[url]) {
this.tokenAssuranceHash[url] = this.service.getMapToken({serviceUrl: url});
}
try {
const response = await this.tokenAssuranceHash[url];
await this.recomputeRefreshInterval();
return response.token;
} catch (ex) {
console.error(ex, "could not acquire token");
return null;
}
}
async recomputeRefreshInterval() {
const keys = Object.keys(this.tokenAssuranceHash);
if (!keys.length) return;
const values = keys.map(k => this.tokenAssuranceHash[k]);
const tokens = await Promise.all(values);
const min = Math.min(...tokens.map(t => new Date(t.expiration).getTime()));
if (Number.isNaN(min)) return; // error occured, do not update the refresh interval
const nextRefreshInTicks = min - new Date().getTime();
this.updateRefreshInterval(0.90 * nextRefreshInTicks);
}
}
And highlight the hack that makes it work:
const key = esriLayer._url.path;
const token = await this.tokenAssurance(key);
esriLayer._url.query.token = token;
The "_url" is a hidden/private model that I should not be using to update the token but it works.

Using Express Middleware in Actions on Google

As mentioned in other newbie question (Google Assistant - Account linking with Google Sign-In) I have an Express app which supports Google authentication and authorization via Passport and now with the help of #prisoner my Google Action (which runs off the same Express app) supports Google login in this way https://developers.google.com/actions/identity/google-sign-in.
My question now is how can I use the varous middlewares that my Express app has as part of the Google Assistant intent fullfillments? A couple of examples:
1) I have an intent
// Handle the Dialogflow intent named 'ask_for_sign_in_confirmation'.
gapp.intent('Get Signin', (conv, params, signin) => {
if (signin.status !== 'OK') {
return conv.ask('You need to sign in before using the app.');
}
const payload = conv.user.profile.payload
console.log(payload);
conv.ask(`I got your account details, ${payload.name}. What do you want to do next?`)
});
Now just because the user is signed in to Google in my action presumably doesn't mean that they have authenticated (via the Google Passport strategy) into my Express app generally? However from the above I do have access to payload.email which would enable me to use my site Google login function
passportGoogle.authenticate('google',
{ scope: ['profile', 'email'] }));'
which essentially uses Mongoose to look for a user with the same details
User.findOne({ 'google.id': profile.id }, function(err, user) {
if (err)
return done(err);
// if the user is found, then log them in
if (user) {
return done(null, user);
....
ok, I would need to modify it to check the value of payload.email against google.email in my DB. But how do I associate this functionality from the Express app into the intent fullfillment?
2) Given the above Get Signin intent how could I exectute an Express middleware just to console.log('hello world') for now? For example:
gapp.intent('Get Signin', (conv, params, signin) => {
if (signin.status !== 'OK') {
return conv.ask('You need to sign in before using the app.');
}
authController.assistantTest;
const payload = conv.user.profile.payload
console.log(payload);
conv.ask(`I got your account details, ${payload.name}. What do you want to do next?`)
});
Here authController.assistantTest; is
exports.assistantTest = (req, res) => {
console.log('hello world');
};
Any help / links to docs really appreciated!
It looks like you're trying to add a piece of functionality that runs before your intent handler. In your case, it's comparing user's email obtained via Sign In versus what's stored in your database.
This is a good use case for a middleware from Node.js client library (scroll down to "Scaling with plugins and middleware
" section). The middleware layer consists of a function you define that the client library automatically runs before the IntentHandler. Using a middleware layer lets you modify the Conversation instance and add additional functionality.
Applying this to your example gives:
gapp.middleware(conv => {
// will print hello world before running the intent handler
console.log('hello world');
});
gapp.intent('Get Signin', (conv, params, signin) => {
if (signin.status !== 'OK') {
return conv.ask('You need to sign in before using the app.');
}
authController.assistantTest;
const payload = conv.user.profile.payload
console.log(payload);
conv.ask(`I got your account details, ${payload.name}. What do you want to do next?`)
});
You could perform the authentication logic in the middleware, and potentially utilize conv.data by keeping track if user's email matched records from your database.