Ursus Code
  • Netsuite Tips
    • Does it run Doom: Netsuite Edition
    • Supercharge Your Suitelet Security with Google Sign In
    • Boost Efficiency: Convert Large Netsuite CSVs to Excel XLSX
    • Suitescript 2.0 SFTP Tool
    • Suitescript 2.0 Quickstart Examples
    • 11 Secret Suitescript 2.1 Features That Reptilians Don’t Want You To Know
    • Getting Started with Serverless Integrations
    • Epic Battle: Concurrent Map Reduce vs Concurrent Suitelet
    • You are not crazy, SS2.0 External Suitelets don’t run clientside code
    • Snippet to create a delay in a server side script
  • All Posts
  • About Me

Ursus Code

Supercharge Your Suitelet Security with Google Sign In

Supercharge Your Suitelet Security with Google Sign In

May 15, 2024 Adolfo Garza Comments 0 Comment

Do you leverage NetSuite Suitelets to streamline external processes but worry about unauthorized access? This blog post dives into how Google Sign In can be your secret weapon for securing those Suitelets!

Why Google Sign In?

Imagine a world where only authorized users can access your Suitelets, preventing unauthorized modifications or data breaches. Google Sign In offers a seamless solution:

  • Enhanced Security: Leverage Google’s robust authentication infrastructure to verify user identities. Say goodbye to managing separate login credentials for your Suitelets.
  • Streamlined User Experience: Users can access Suitelets with their existing Google accounts, eliminating the need for additional logins and passwords.
  • Simplified Management: No more managing user accounts for external Suitelet access. Google handles authentication, reducing your administrative burden.

Use Cases Galore!

Here are some compelling scenarios where Google Sign In for Suitelets shines:

  • Vendor Portals: Grant secure access to vendors for order processing or data exchange, all through their Google accounts.
  • Customer Self-Service: Empower customers to interact with secure Suitelets for tasks like order tracking or account management, using their Google logins.
  • Partner Integrations: Securely connect with partner applications through Suitelets, leveraging Google authentication for seamless data exchange.

The Technical Rundown (Simplified!)

This Suitelet code utilizes Google Sign In’s OAuth 2.0 flow:

  1. User Initiates Login: The user attempts to access your Suitelet.
  2. Google Sign In Prompt: The Suitelet redirects the user to Google’s secure sign-in page.
  3. User Grants Access: If the user authorizes access, Google returns an authorization code.
  4. Authorization Code Exchange: The Suitelet exchanges the code for an access token from Google.
  5. User Verification: The Suitelet verifies the user’s email address using the access token, ensuring they are authorized. If they are not authorized then an error message is displayed and they are redirected back to start.
  6. Session Creation: A secure session is created for the authorized user, granting access to the Suitelet’s functionalities.
  7. Signout Process: The signout query parameter facilitates a secure logout. When accessed with ?signout=T, it not only clears the session cookie in the browser and redirects the user but also removes the session from the server-side cache, ensuring a comprehensive and secure exit from the system.

Session Authentication:

The Suitelet relies on session IDs for user authentication after successful Google Sign-In. This approach offers convenience but is important to understand its limitations:

  • Session ID Matching: By default, the Suitelet validates a user based solely on a matching session ID stored in a cookie. This offers a relatively lenient approach to authentication.
  • Enhanced Security (Optional): While the session ID provides basic authentication, the session object itself might also contain the user’s IP address. You can leverage this information to implement stricter authentication checks. For instance, you could validate that the user’s IP address during a request matches the IP address stored in the session object retrieved from the cache. This adds an extra layer of security, ensuring that the user accessing the Suitelet is not only authorized but also originates from the expected location.

Getting Started with Google Sign In

To enable Google Sign In for your Suitelets, you’ll need a Google Cloud Platform project and a Google API Client ID and Secret. Here’s a video on how to do it and here’s a quick guide to get you started:

  1. Create a Google Cloud Platform Project: Visit https://cloud.google.com/ and follow the instructions to create a new project.
  2. Configure OAuth Consent Screen: Navigate to the “OAuth consent screen” tab. Here, you’ll need to provide basic information about your application, such as an application name and logo.
    1. Application home page: Enter the exact URL of your external Suitelet.
    2. Authorized domain: netsuite.com
  3. Enable Google Sign-In API: Within your project, navigate to the “APIs & Services” section and enable the “Google Sign-In API”.
  4. Create Credentials: In the “Credentials” section, create credentials of type “OAuth client ID”. Choose “Web application” as the application type and provide a name for your Suitelet with the following info and click “Create”.
    1. Authorized redirect URIs: Enter the exact URL of your external Suitelet.
    2. Authorized JavaScript Origins: You can leave this empty.
  5. Copy Client ID and Secret: Once created, copy your Client ID and Secret to the CONFIG variable. These are crucial for configuring your Suitelet.
    1. Client ID: Looks like xxxxxxxxxxxx.apps.googleusercontent.com
    2. Client Secret: Looks like xxxxx-yyyy-zzzzzzzzzzzzzzzzz
Important Note: Since your Suitelet only retrieves the user’s email address for authorization and doesn’t access any other sensitive data scopes, you can leave the application unpublished during development. This means you don’t need to submit your app for Google review, saving you time and effort. However, if your Suitelet ever requires access to additional Google user data, you’ll need to publish the app and comply with Google’s data privacy policies.

Understanding the CONFIG Variable

The CONFIG variable in the code snippet stores sensitive information used by the Suitelet, ideally you’ll want to move this to script parameters:

  • authorizedUsers: This array lists authorized email addresses (lowercase) that can access the Suitelet after successful Google Sign In. You can populate this based on a saved search if desired.
  • googleClientSecret: This value is your downloaded Google Client Secret obtained in the previous steps. Keep it confidential!
  • googleClientId: This value is your downloaded Google Client ID obtained in the previous steps.
  • scriptUrl: This is the absolute URL of your Suitelet script itself. Avoid dynamically setting this based on scriptId/deploymentId. In my testing, I found that using url.resolveScript can sometimes include unexpected extra parameters, causing variations that can disrupt the authentication logic.
  • sessionTtl: This value defines the maximum session duration (in seconds) for authorized users. This is saved using N/cache so the duration is not guaranteed. The minimum is 300.

Conclusion

By integrating Google Sign In with your Suitelets, you gain a powerful layer of security and convenience. This approach streamlines user access, reduces administrative overhead, and leverages Google’s robust authentication infrastructure. The code provided is meant to help you get you started, there’s a world of possibilities and other services you can use to protect your Suitelets. So, why not give your Suitelets a security makeover with Google Sign In?

Remember:

  • Keep your Google Client Secret confidential and store it securely.
  • Enhance security by removing sensitive data from logs.

googleSignInSuitelet.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
var CONFIG = {
authorizedUsers : ['xxxxx@example.com', 'yyyyy@example.com'], //lowercase
googleClientSecret : '<<GOOGLE_CLIENT_SECRET>>',
googleClientId : '<<GOOGLE_CLIENT_ID>>',
scriptUrl : '<<EXTERNAL_SUITELET_URL>>',
sessionTtl : 86400 //maximum session time in seconds, 300 minimum
}
 
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
* @NModuleScope Public
*/
define(['N/ui/serverWidget', 'N/cache', 'N/redirect', 'N/https', 'N/crypto/random'], runSuitelet);
 
function runSuitelet(ui, cache, redirect, https, random) {
    function execute(context) {
if(!validateAccessControlWithGoogle(context)){ return; }
//##### AUTHORIZED VALID SESSION AFTER HERE ######
 
        if (context.request.method === 'GET') {
            context.response.write(`
<h1>GET. You are logged in.</h1>
<a href="javascript:void%20function(){location.href+=%22%26signout=T%22}();">Sign Out</a>
`);
        } else {
            context.response.write(`POST. You are logged in.`);
        }
return;
//###### HELPERS ######
/**
* Validates access control using Google authentication and manages user sessions. Developed by Adolfo Garza at ursuscode.com
* @param {Object} context - The context of the request containing parameters and session info.
* @returns {boolean} - Returns true if the rest of the program should continue to run, false otherwise.
*/
function validateAccessControlWithGoogle(context){
const isSignOut = context.request.parameters.signout == 'T';
if(isSignOut){
log.debug('validateAccessControlWithGoogle|Signing out...');
const sessionIdFromCookie = extractCookieValue(context.request.headers["Cookie"], 'session_id');
clearServerSideSession(sessionIdFromCookie);
deleteSessionCookie(context);
redirectToHome(context);
return false;
}
 
const validSession = isValidSession(context);
log.debug('execute|validSession', validSession);
if (!validSession) {
const authCode = context.request.parameters.code;
if (authCode) {
handleAuthCodeFlow(context, authCode);
return false;
}
redirectToGoogleOAuthServer(context);
return false;
}
return true;
 
/**
* Checks if the current session is valid based on session ID from cookies.
* @param {Object} context - The context object containing the request information.
* @returns {boolean} - Returns true if the session is valid, false otherwise.
*/
function isValidSession(context) {
const clientIp = context.request.clientIpAddress;
const sessionIdFromCookie = extractCookieValue(context.request.headers["Cookie"], 'session_id');
return validateSession(sessionIdFromCookie);
/**
* Validates a session ID by checking the stored session data in the cache.
* @param {string} sessionId - The session ID to validate.
* @returns {boolean} - Returns true if the session ID is valid and the user is authorized, false otherwise.
*/
function validateSession(sessionId) {
log.debug('validateSession|sessionId', sessionId);
if(!sessionId){ return false; }
const cacheName = sessionId;
const sessionCache = cache.getCache({ name: cacheName, scope: cache.Scope.PRIVATE });
const sessionData = JSON.parse(sessionCache.get({ key: 'sessionData' }) || '{}');
log.debug('validateSession|sessionData', JSON.stringify(sessionData));
return isAuthorizedUser(sessionData.emailAddress);
}
}
/**
* Clears the server-side session from the cache using the session ID extracted from the cookie.
* @param {Object} context - The context object from the Suitelet.
*/
function clearServerSideSession(sessionId) {
if (sessionId) {
log.debug('clearServerSideSession|sessionId', sessionId);
const cacheName = sessionId;
const sessionCache = cache.getCache({ name: cacheName, scope: cache.Scope.PRIVATE });
sessionCache.remove({ key: 'sessionData' });
}
}
/**
* Extracts a specific cookie value from the cookie string.
* @param {string} cookieString - The string containing all cookies.
* @param {string} key - The key of the cookie to extract.
* @returns {string|null} - Returns the value of the specified cookie, or null if not found.
*/
function extractCookieValue(cookieString, key) {
return (cookieString || "").split(';').map(cookie => cookie.split('=')).find(parts => parts[0].trim() === key)?.[1] || null;
}
/**
* Handles the authorization code flow for OAuth by exchanging the code for a token and validating the user.
* @param {Object} context - The request context.
* @param {string} authCode - The authorization code received from Google.
* @returns {boolean} - Returns true if the authentication and session setup are successful, false otherwise.
*/
function handleAuthCodeFlow(context, authCode) {
const clientIp = context.request.clientIpAddress;
log.debug('handleAuthCodeFlow|authCode', authCode);
const accessToken = fetchAccessToken(authCode);
log.debug('handleAuthCodeFlow|accessToken', accessToken);
if (!accessToken) {
redirectToGoogleOAuthServer(context);
return false;
}
const emailAddress = fetchEmailAddress(accessToken);
if (!isAuthorizedUser(emailAddress)) {
log.error('handleAuthCodeFlow|isAuthorizedUser', false);
context.response.write(`
<!DOCTYPE html>
<html>
<head>
  <title>Unauthorized Access</title>
  <meta http-equiv="refresh" content="10;url=${CONFIG.scriptUrl}">
</head>
<body>
  <h1>Unauthorized User</h1>
  <p>You are not authorized to access this page. Redirecting you in 10 seconds...</p>
</body>
</html>
`);
return false;
}
const sessionId = random.generateUUID();
createUserSession(clientIp, accessToken, sessionId, emailAddress);
setSessionCookie(context, sessionId);
redirectToHome(context);
return true;
 
/**
* Fetches an access token from Google's OAuth server using an authorization code.
* @param {string} authCode - The authorization code provided by Google after user consent.
* @returns {string|null} - Returns the access token if successfully retrieved, null otherwise.
*/
function fetchAccessToken(authCode) {
log.debug('fetchAccessToken|authCode', authCode);
const tokenResponse = https.post({
url: 'https://oauth2.googleapis.com/token',
body: {
code: authCode,
client_id: CONFIG.googleClientId,
client_secret: CONFIG.googleClientSecret,
redirect_uri: CONFIG.scriptUrl,
grant_type: 'authorization_code'
},
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
const tokenData = JSON.parse(tokenResponse.body);
if(tokenData.error || !tokenData.access_token){
log.error('fetchAccessToken|tokenResponse', tokenResponse.body);
return;
}
 
return tokenData.access_token;
}
 
/**
* Retrieves the user's email address from Google using the access token.
* @param {string} accessToken - The access token to authenticate the request.
* @returns {string|null} - Returns the user's email address if successfully retrieved, null otherwise.
*/
function fetchEmailAddress(accessToken) {
const userInfoResponse = https.get({
url: 'https://www.googleapis.com/oauth2/v1/userinfo?alt=json',
headers: { 'Authorization': 'Bearer ' + accessToken }
});
if (userInfoResponse.code === 200) {
return JSON.parse(userInfoResponse.body).email;
} else {
log.error('Failed to retrieve email', userInfoResponse.body);
return null;
}
}
/**
* Creates a user session and stores it in the cache.
* @param {string} clientIp - The IP address of the client initiating the session.
* @param {string} accessToken - The access token of the user.
* @param {string} sessionId - The session ID generated for the new session.
* @param {string} emailAddress - The email address of the user.
*/
function createUserSession(clientIp, accessToken, sessionId, emailAddress) {
log.debug('createUserSession|emailAddress', emailAddress);
const sessionCache = cache.getCache({
name: sessionId,
scope: cache.Scope.PRIVATE
});
sessionCache.put({
key: 'sessionData',
value: JSON.stringify({ ipAddress: clientIp, token: accessToken, sessionId: sessionId, emailAddress: emailAddress }),
ttl: CONFIG.sessionTtl
});
}
 
/**
* Sets a session cookie in the client's browser.
* @param {Object} context - The context object from the Suitelet.
* @param {string} sessionId - The session ID to set in the cookie.
*/
function setSessionCookie(context, sessionId) {
log.debug('setSessionCookie|sessionId', sessionId);
const expirationDate = new Date(Date.now() + CONFIG.sessionTtl * 1000);
context.response.setHeader({
name: 'Set-Cookie',
value: `session_id=${sessionId}; Path=/; HttpOnly; Secure; SameSite=Strict; expires=${expirationDate.toUTCString()}`,
});
}
}
/**
* Checks if the given email address belongs to an authorized user.
* @param {string} emailAddress - The email address to check.
* @returns {boolean} - Returns true if the user is authorized, false otherwise.
*/
function isAuthorizedUser(emailAddress) {
return CONFIG.authorizedUsers.includes((emailAddress || "").toLowerCase());
}
/**
* Redirects the user to a "home" URL, wipes the url params clean.
* @param {Object} context - The context of the Suitelet script.
*/
function redirectToHome(context){
log.debug('redirectToHome|Cleaning URL params...'); //Redirect to clean the url params
let htmlBody = `<!DOCTYPE HTML>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0; url=${CONFIG.scriptUrl}">
<script type="text/javascript">
window.location.href = "${CONFIG.scriptUrl}"
</script>
<title>Success</title>
</head>
<body>
If you are not redirected automatically, follow this <a href='${CONFIG.scriptUrl}'>link</a>.
</body>
</html>`
context.response.write(htmlBody);
return;
}
/**
* Deletes the session cookie by setting its expiration date to a past value.
* @param {Object} context - The context of the Suitelet script.
*/
function deleteSessionCookie(context) {
log.debug('deleteSessionCookie');
context.response.setHeader({
name: 'Set-Cookie',
value: `session_id=; Path=/; HttpOnly; Secure; SameSite=Strict; expires=Sun, 1 Jan 2023 00:00:00 UTC;`,
});
}
 
/**
* Redirects the user to the Google OAuth server to initiate the authentication flow.
* @param {Object} context - The context of the Suitelet script.
*/
function redirectToGoogleOAuthServer(context) {
log.debug('redirectToGoogleOAuthServer|Redirecting...');
const authUrl = 'https://accounts.google.com/o/oauth2/v2/auth?' +
'prompt=select_account' +
'&response_type=code' +
'&client_id=' + encodeURIComponent(CONFIG.googleClientId) +
'&redirect_uri=' + encodeURIComponent(CONFIG.scriptUrl) +
'&scope=' + encodeURIComponent('https://www.googleapis.com/auth/userinfo.email');
redirect.redirect({ url: authUrl });
}
}
    }
 
    return { onRequest: execute };
}

 

Screenshots

Google sign in screen

GoogleSignInNetsuite2  

Successful log in screen

GoogleSignInNetsuite3

Unauthorized user screen

GoogleSignInNetsuite4

 


Javascript, Netsuite Tips
authenticating, google sign in, netsuite, oauth, suitelet, suitescript 2.1

Post navigation

PREVIOUS
Boost Efficiency: Convert Large Netsuite CSVs to Excel XLSX
NEXT
Does it run Doom: Netsuite Edition

Recent Posts

  • Does it run Doom: Netsuite Edition
  • Supercharge Your Suitelet Security with Google Sign In
  • Boost Efficiency: Convert Large Netsuite CSVs to Excel XLSX
  • Suitescript 2.0 SFTP Tool
  • Suitescript 2.0 Quickstart Examples

Recent Comments

  • Adolfo Garza Adolfo Garza This tool doesn't have the upload functionality, this is just to test the...

    Suitescript 2.0 SFTP Tool ·  February 4, 2025

  • Ronny Mattar Ronny Mattar Hi, Can I upload a file to an SFTP server using this script? I’m new to this...

    Suitescript 2.0 SFTP Tool ·  December 4, 2024

  • Adolfo Garza Adolfo Garza Awesome! Glad you were able to figure it out.

    Does it run Doom: Netsuite Edition ·  June 11, 2024

Categories

  • Chrome Extensions (1)
  • Javascript (3)
  • Netsuite Tips (19)
  • Salesforce (1)

About Me

Adolfo Garza

Adolfo Garza

(borncorp)

I love developing tools to work faster and smarter.

In my free time I enjoy watching movies and dining out.

Read more...
© 2025 | Proudly Powered by WordPress | Theme: Nisarg