Is it ok to store user's secrets in PropertiesService.getUserProperties()? - security

I am developing a Google Workspace Addon (standalone script) which will make REST API calls to external service and for that purpose it needs to provide an API key.
I request the API key input from a user and then store it in PropertiesService in the following way:
function onSheets(e) {
const userProperties = PropertiesService.getUserProperties();
const saved_api_key = userProperties.getProperty('api_key');
const api_key: string = saved_api_key ? saved_api_key : "";
const builder = CardService.newCardBuilder();
const apiKeyInput = CardService.newTextInput().setTitle('API Key')
.setFieldName('api_key')
.setHint('Enter your API Key')
.setValue(api_key);
const saveApiKey = CardService.newAction().setFunctionName('saveApiKeyFn');
const button = CardService.newTextButton().setText('Save').setOnClickAction(saveApiKey);
const optionsSection = CardService.newCardSection()
.addWidget(apiKeyInput)
.addWidget(button)
builder.addSection(optionsSection);
return builder.build();
}
function saveApiKeyFn(e) {
const api_key = e.formInput.api_key;
const userProperties = PropertiesService.getUserProperties();
userProperties.setProperty('api_key', api_key);
return CardService.newActionResponseBuilder()
.setNotification(CardService.newNotification()
.setText("API Key saved"))
.build();
}
Since userProperties are scoped to a current user it seems fine. But I have serveral problems with this solution:
Is this really safe? I mean it is stored in plain text so maybe there are ways to retrive it by other mailcious user?
The idea that by mistake I would use getScriptProperties() and thus leak one user's API key to all other users gives me nightmares. It is highly sensitive API key. It would cost a user tons of money if abused.
I read that some user's suggest https://cloud.google.com/secret-manager but I am not sure it's fit for this particular scenario. It would require one more external API call. It is not free. And lastly from what I underestand I would be sort of an owner of all of these secrets since I will be the owner of the Google Cloud project in which this API runs.
All I want is for the users to be able to store their keys safely, so that no one else including me can never access them.
What would you suggest? Thanks!

Is this really safe? I mean it is stored in plain text so maybe there are ways to retrive it by other mailcious user?
Security is relative. There's no such thing as absolute secrecy. Here are some attack scenarios:
Google employees or support may have unrestricted access
If a particular user installed a trigger, that trigger runs as that user and other users, if they can trigger the script and have edit access to the script, will be able to access the keys. A common scenario would be a installed edit trigger in a sheet. User B can access user A, if he can make a edit as well as edit the script. As mentioned in the comments by doubleunary, this is less of a problem in a published add on, as the source code is not accessible or editable.
Encrypting keys is a possibility. But, where would you store the decrypting key? You could ask every user to have a custom password for decrypting the key. But how many times are you going to make a API call? Would they have to enter the key every time? At what point does convenience overtake the need for secrecy?
The idea that by mistake I would use getScriptProperties() and thus leak one user's API key to all other users gives me nightmares. It is highly sensitive API key. It would cost a user tons of money if abused.
That is a possibility, but one that's easily avoidable by careful code review by yourself and your peers.
Those are the scenarios I could think of.
Related:
Securely Storing API Secrets used in Google Apps Script - Published Library

Related

REST API - How to implement user specific authorisation?

So I'm currently learning/building a REST API backend server for my web application using NodeJS, ExpressJS, and MySQL as the database. My question is in regards to the best way to implement authorisation to ensure User A does not access or edit the data belonging to another User. Please note that I understand there are a lot of examples for implementation of role based authorisation (ie user groups vs admin groups, etc) but this is not what I'm asking. Instead, how do I authorise a user against the data they are accessing?
It is possible that I'm overthinking this and this is not even necessary; that I should just check whether the data belongs to the user in every SQL query, but I thought I'd ask if there's a middleware or policy architecture that takes care of this, or maybe even authorise through caching.
The only solution I can think of is that every SQL query returns the the user id with the result, then I just create a service that checks every result if the id matches or not. If yes, then proceed. If not rollback the query and return unauthorised error. Is this ok?
I very much appreciate your advice, help, and if you can point me in the right direction.
Many thanks in advance.
Save the userId (or ownerId) in every table, and create a middleware where each db access method requires the userId as a parameter, for example:
readOne(id, userId) {
// implements SELECT * FROM example WHERE id = id AND userId = userId
}
updateOne(id, data, userId) {
// implements UPDATE example SET data = data WHERE id = id AND userId = userId
}
...
For security reasons, never send as a response "Requested data exist by you aren't the owner".
The simplest things usually work best. You wouldn't have to have a special service for checking authorization rights for every entity and you can do it at data access level eg. SELECT * FROM foo WHERE user_id = :currentUser or UPDATE foo SET foo = bar WHERE user_id = :currentUser
It also depends whether you want to notify the user about unallowed access via HTTP401 or not to reveal that such a resource even exists for different user HTTP404.
For HTTP401 the scenario would be:
const entity = loadFromDB(id);
if(entity.userId !== currentUserId) {
res.send(401);
return;
}
... update entity logic ...

Is there an anonymous ID in Actions on Google with Dialogflow?

Is there an anonymous ID in Actions on Google with Dialogflow that I can access using DialogFlow in Node.js?
I don't need to know the Google account of who is using the Action, but I do want to have a unique identifier so that the Action can know when they come back.
Google no longer provides one for you. You will have to generate one when a new user interacts with your webhook and store the generated id in their user storage object.
To identify a new user your just check if they already have an id in their user storage object. For generating the id you can use an library like uuid. https://www.npmjs.com/package/uuid
Uuidv4 is probably the one that you need if you just need a unique id for simple identifications
The original idea from Google was to leverage a field called userStorage, but this feature seems to be borked ATM.
userStorage Documentation:
https://developers.google.com/actions/assistant/save-data
Reddit thread regarding issues:
https://www.reddit.com/r/GoogleAssistantDev/comments/d88z7e/userstorage_saga_continued/
Unless something has changed (I haven't checked on userStorage since I've been busy writing a fix around it) you may be out of luck without Account Linking. Feel free to try userStorage and keep me honest as they may have remedied the situation internally.
Alternatively, if all you need is an identifier or session for a single conversation you can leverage the conversationId which will be unique until the conversation ends.
I've found a possible option...
(When working in DialogFlow in Node.js, most code is in a handler and the parameter is usually called conv. The following assumes that it is inside such a handler.)
On every single call, check for an 'existing' id in the session data and the user storage:
var id = conv.data.MyId || conv.user.storage.MyId || '';
if(!id) {
id = /* make a new Id for this user... a GUID or some other unique id */
conv.user.storage.MyId = id;
}
Once I get the Id from storage or make a new one, it is critical to reassign it to conv.data, since conv.user.storage seems to be reliably provided only on the first call!
// IMPORTANT
conv.data.MyId = id;
/* use the Id as needed */
My code looks up the Id in a firebase database to get details from their last visit.
This seems to be working, but may not be reliable.

IBM Domino 10 - Java XPage create session or compare credentials of another user

I'm writing Java code in an XPage Rest Service basing on https://setza-projects.atlassian.net/wiki/spaces/RSD/pages/44007659/IBM+Domino which is an REST service written in Java used to handle Resource Reservations database. However the way it currently works, it creates the reservations for the current session user only:
private JsonObject createIntanceAppointment(ResourceDefinition rd, Database reDatabase, Date dtStart, Date dtEnd, String subject) throws NotesException {
Session session = reDatabase.getParent();
Name nnOrganizier = session.createName(session.getEffectiveUserName());
Name nnREsource = session.createName(rd.getFullName());
DateTime dt_startDateUTC = session.createDateTime(dtStart);
DateTime dt_endDateUTC = session.createDateTime(dtEnd);
Document doc = reDatabase.createDocument();
doc.replaceItemValue("form", "Reservation");
doc.replaceItemValue("Purpose", subject);
doc.replaceItemValue("ReservedFor", nnOrganizier.getCanonical());
doc.replaceItemValue("ResourceName", nnREsource.getAbbreviated());
doc.replaceItemValue("ResNameFormat", nnREsource.getAbbreviated());
I'm doing a very similar integration with Domino, although I'd prefer to have the reservations created for individual users (they provide their username & password on the room-booking application on a touch screen).
I could just authenticate as the user in my REST client, but if I understand the installation requirements for that RoomZ api correctly, the 'api managing user' needs to be exclusively signed to the database, so I would need to do that for every user in Domino that could make reservations.
I tried using NotesFactory.createSession("", "user", "password"); but that doesn't work, it gives Cannot create a session from an agent error
If I cannot create another session, is there any way I could verify that the username and password passed to the API in the payload is correct (to verify if the user can login)? Then I could just set the organizer/reserved for to this user.
Also, is there any way to make these reservation also appear in the organizer's Notes calendar? Currently they are succesfully created in the Reservations database and all, but the organizer is unaware of them despite he's assigned to the reservation.
You do not need to create a session for every user. The important thing is the nnOrganizer = session.createName(" ") which should contain the user. Probably you'll also need to set additional fields like chair or from for the reservation.
If you want to have some entries in the organizers calendar, send them a proper invitation or create a calendar entry in their mailfile.

How do I save and retrieve information across invocations of my agent in Dialogflow?

I would like my Actions on Google agent to store and retrieve certain pieces of information across invocations - like a cookie. How do I do this?
You have a lot of options on how you want to do this, depending on exactly what you're trying to do. It isn't exactly like a web cookie, although there are similarities.
If you want the equivalent of a session cookie, information that is retained during a single conversation, then your options are
Using the Session ID provided as part of the information sent to you on each invocation and tracking this in your fulfillment.
Storing information you want retained using a Dialogflow context
If you are using the actions-on-google JavaScript library, storing this in the app.data object created for you.
If you want the equivalent of a long-lasting cookie to retain information between conversations then your options are
Using the anonymous User ID provided as part of the information sent to you on each invocation and tracking this in your fulfillment.
If you are using the actions-on-google javascript library, storing this in the app.userStorage object created for you.
Storing it as part of the string in the JSON response under data.google.userStorage.
Some more information about each of these
Session ID
A different Session ID is created for each conversation you have. You can get this Session ID by examining the JSON sent to your webhook in the sessionId parameter.
You can then look this up in a data store of some sort that you manage.
Dialogflow context
Contexts are powerful tools that are available with Dialogflow. You return a context as part of your fulfillment webhook and indicate the name of the context, its lifetime (how many more rounds of the conversation it will be passed back to your webhook), and any parameters associated with the context (string key/value pairs).
Contexts are especially useful in helping determine what intents may be called. You can indicate what contexts must be active for an Intent to be recognized by Dialogflow.
If you're using the actions-on-google node.js library, you can set a context using something like this:
var contextParameters = {
foo: "Something foothy",
bar: "Your local bar."
};
app.setContext( "remember_this", 5, contextParameters );
You need to do this before you call app.ask() or app.tell().
Or you can do the equivalent in the JSON as part of the contextOut block of the response
"contextOut": [
{
"name": "remember_this",
"lifespan": 5,
"parameters": {
"foo": "Something foothy",
"bar": "Your local bar."
}
}
]
The next time your webhook is called, you can fetch this context either by looking at the result.contexts array or by using the app.getContext() or app.getContextArgument() methods in the library.
Using app.data
If you're using the library, Google has done some of the work for you. The app.data object is created for you. Any values you set in the object are available for the lifetime of the session - you just read them in later calls to your webhook.
(Under the covers, Google uses a context for this, so there is no magic. The two work together and you're free to do both.)
Anonymous UserID
When a user first uses your action, a user ID is generated. This ID doesn't give you access to any specific information about them, and isn't used for any other action, but every time you see it, you can be assured that it was the same user that used it on a previous occurrence. Just like a cookie, however, the user can reset it and a new ID will be generated for them for your action.
You get this from the JSON at originalRequest.user.userId or by using app.getUser().userId. Once you have it, you'd use a data store of some sort to store and retrieve information about this user.
Using app.userStorage
Similar to app.data, there is also an app.userStorage object that is created for you for each user. Any changes you make to this object are saved in between conversations you have with this user.
Unlike app.data, however, this doesn't get stored in a context. It has its own storage method. Which leads to...
Storing it in JSON
If you're not using the actions-on-google library, you still have access to userStorage through the response and request JSON directly. You need to store this as a string, but if you need to store a more complex object, a common method is to stringify it as JSON.
You'll store this value under data.google.userStorage in the response and can retrieve it under originalRequest.data.user.userStorage in the request your webhook receives.
You can save the information in Context with a key value parameter.
SAVING VALUES IN CONTEXT :
agent.set.Context({
name:'context-name',
lifespan: 5,
parameters:{
'parameter-name':'parameter-value'
}
});
GETTING VALUES FROM CONTEXT
agent.getContext('context-name');
For more Details : https://dialogflow.com/docs/contexts/contexts-fulfillment
You could also use a Google Cloud database like BigQuery or Firestore
Sounds like you may want to checkout out Account Linking: https://developers.google.com/actions/identity/account-linking. With account linking you can collect end-user information which you exchange with Google by providing a unique key. This unique key becomes part of every request you receive from Google, so when you get that unique key you lookup the information you collected from the end-user. In your case, you would store credentials or whatever key is required to access the end-user information. After the initial linking, any new data you obtain could be stored along with the original information collected, based on the unique key obtained during account linking.
For this purpose, i just did a node module just for that, in external json file from api call, i need to store and add additional informations to retrieve later. I thing that you can do a lot with this module, Store object, array, json, value, Navigation history?, back to previous page.
It work like localStorage or Cookies.
There's no limit, you can create multiple storage by name (key) an value. It's new and i'm testing it for bugs right now on my own project.
Test on Runkit
On npm
vStorage = require('virtual-storage');
vStorage.set('name', '{title:'Title 1', description:'Descriptions 1'}')
let getStorage_name = vStorage.get('name');
console.log(getStorage_name.title);
vStorage.get('name')

Can A Mobile Application use TrueVault to store JSON data without a "middleman" server?

I have been reading the documentation at https://docs.truevault.com/ but I am a little confused. I read this on the true vault site:
If you plan on using any of the server-side libraries, please ensure
any hosting environment you use is HIPAA compliant.
I took this to mean that TrueValut could support a standalone (client side only) mobile application architecture. Where the TrueVault API was the only server side interaction.
However my understanding of the documentation is that:
An API_KEY is required to register a new user.
Any API_KEY provides full access to all data vaults and JSON documents stored in TrueVault.
If both of these assumptions are correct that would mean it would be impossible to register new users directly from the client side app, forcing me to use a costly and resource intensive HIPPA compliment web server. The only way to get around this would be top hard code the API_KEY into the app, an obvious no go if that API_KEY can access all of my TrueVault data.
For my use case I have the following requirements for TrueVault for me to be able to consider using it (I would imagine these requirements are the same for anyone looking to develop a client side only healthcare application):
A user can sign up via the API directly from my client side app without requiring any sensitive keys or root auth data.
A user can authenticate using only the data they provided to sign up (username/email/password). My app is multi platform I cant ask them to remember their API keys to log in.
A user can Read/Write/Update/Delete data linked to their profile. They can not access any data from another user using their credentials.
Is TrueVault able to deliver these three basic requirements?
If the answer to this is "No" I would recommend you update this text on your website as there are not going to me any viable HIPPA compliment applications that can be supported by TrueVault without an independent server side interface.
I'm currently using AWS Lambda as a solution. Lambda is HIPPA compliant, more info here. Lambda is also a low cost solution.
Here is an example of the code I'm running on Lambda using Node.js.
var request = require('request-promise');
var _ = require('lodash');
function encodeBase64(str) {
return (new Buffer(str)).toString('base64');
}
var baseUrl = 'https://api.truevault.com/v1/';
var headers = {
'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'
};
var req = request.defaults({
baseUrl: baseUrl,
headers: _.extend({
Authorization: 'Basic ' + encodeBase64('your api key:')
}, headers),
transform: function(body) {
return JSON.parse(body);
}
});
exports.handler = function(event, context) {
req.post('users', {
form: {
username: event.email,
password: event.password,
attributes: encodeBase64(JSON.stringify({
name: event.name
}))
}
}).then(function() {
context.succeed({user: user});
}).catch(context.fail);
}
In general, you are correct - if you include zero server-side processing between user and TrueVault, then the API keys will be public. At least, I don't know of any way to avoid this.
That being said, it is incorrect to jump to "any API_KEY provides full access to all data vaults and JSON documents stored in TrueVault." - that's not the case if setup properly.
TrueVault API keys are able to be narrowed in scope quite a lot. Limiting a key to only Write permission on {Vault#1}, a second key to only Read permission on {Vault#2}, a third key to allow uploading Blogs in {Vault#1&#3}, quite a few variations, a forth for deleting information from {Vault#2}, and on as needed. You can also limit permissions specifically to content "owned" by the API key (e.g. user-specific keys) Full documentation here.
There are also limited scope keys (set expiry time, usage count, limit to any of the prior permission scopes). Docs here.
TrueVault also offers user logins separate from API keys which may be better suited if your user are using login credentials. Docs here.
I'm still rather figuring out TrueVault myself (at time of writing at least) so be sure to research and review more for your needs. I'm still torn if the limited scoping is "good enough" for my needs - I'm leaning towards using AWS Lambda (or similar) to be a HIPAA compliant middle man, if only to better hide my access token generation and hide that my data is going to TrueVault and add some "serverless-server-side" data validation of sorts.

Resources