├── GSApp.gs ├── LICENSE ├── README.md └── example.gs /GSApp.gs: -------------------------------------------------------------------------------- 1 | /* 2 | * GAS library for generating user OAuth Tokens via Google service account. 3 | * @param {String} rsaKey The private_key from your service account JSON key 4 | * @param {Array} Scopes An Array of scopes you want to authenticate 5 | * @param {String} saEmail The service account Email 6 | * @return {object} self for chaining 7 | */ 8 | function init(rsaKey, Scopes, saEmail){ 9 | return new saService_(rsaKey,Scopes,saEmail); 10 | } 11 | 12 | 13 | function saService_(rsaKey, Scopes, saEmail){ 14 | 15 | var self = this; 16 | var rsaKey_ = rsaKey; 17 | var Scopes_ = Scopes; 18 | var saEmail_ = saEmail; 19 | var jwts_ ; 20 | var tokens_ = {}; 21 | var expireTime_; 22 | var subAccounts_; 23 | 24 | 25 | if (!rsaKey_) { 26 | throw 'You must provide the private key'; 27 | } 28 | if(!(Scopes_.constructor === Array)){ 29 | throw "The Scopes must be in a valid array" 30 | } 31 | 32 | if (!Scopes_) { 33 | throw 'You must provide atleast one scope'; 34 | } 35 | 36 | if (!saEmail_) { 37 | throw 'You must provide the service account email'; 38 | } 39 | 40 | self.addUser = function(userEmail){ 41 | if(!subAccounts_)subAccounts_ = []; 42 | subAccounts_.push(userEmail); 43 | return self; 44 | } 45 | 46 | self.addUsers = function(userEmailArray){ 47 | if(!subAccounts_)subAccounts_ = []; 48 | subAccounts_ = Array.concat(subAccounts_,userEmailArray); 49 | return self; 50 | } 51 | 52 | self.removeUsers = function(){ 53 | subAccounts_ = null; 54 | return self; 55 | } 56 | 57 | self.removeUser = function(userEmail){ 58 | var index = subAccounts_.indexOf(userEmail); 59 | if (index > -1) { 60 | subAccounts_.splice(index, 1); 61 | } 62 | return self; 63 | } 64 | 65 | self.generateJWT_ = function(){ 66 | var sResult="", 67 | claim="", 68 | JWTs = {}, 69 | header = Utilities.base64Encode('{"alg":"RS256","typ":"JWT"}'); 70 | 71 | if(!subAccounts_){ 72 | throw new Error("You must add at least one user account"); 73 | } 74 | 75 | for(var i=0; i < subAccounts_.length; i++){ 76 | claim = header+"."+Utilities.base64Encode(JSON.stringify(makeClaim(subAccounts_[i]))); 77 | JWTs[subAccounts_[i]]={"signedClaim": claim +"."+ Utilities.base64Encode(Utilities.computeRsaSha256Signature(claim, rsaKey_)), 78 | "expire":expireTime_}; 79 | } 80 | jwts_ = JWTs; 81 | return self; 82 | } 83 | 84 | self.getToken = function(userEmail){ 85 | if(!(userEmail in tokens_)){ 86 | throw new Error("User not found"); 87 | }else{ 88 | return tokens_[userEmail]; 89 | } 90 | } 91 | 92 | self.getTokens = function(){ 93 | return tokens_; 94 | } 95 | 96 | 97 | 98 | 99 | self.requestToken = function(){ 100 | self.generateJWT_(); 101 | if(!jwts_){ 102 | throw 'You must run generateJWT' 103 | } 104 | 105 | var params = { 106 | grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', 107 | assertion: '' 108 | } 109 | 110 | 111 | var url = "https://www.googleapis.com/oauth2/v3/token" 112 | var parameters = { 'method' : 'post', 113 | 'payload' : params, 114 | muteHttpExceptions:true}; 115 | 116 | var response=""; 117 | 118 | for(var user in jwts_){ 119 | params.assertion = jwts_[user].signedClaim; 120 | response = JSON.parse(UrlFetchApp.fetch(url,parameters).getContentText()); 121 | 122 | if(response.error){ 123 | if(response.error === "invalid_grant"){ 124 | tokens_[user]={}; 125 | tokens_[user].token = "invalid_grant: Does this user exist?"; 126 | tokens_[user].expire = jwts_[user].expire; 127 | }else{ 128 | throw new Error('There was an error requesting a Token from the OAuth server: '+ response.error); 129 | } 130 | } 131 | 132 | if(response.access_token){ 133 | tokens_[user]={}; 134 | tokens_[user].token = response.access_token; 135 | tokens_[user].expire = jwts_[user].expire; 136 | } 137 | } 138 | return self; 139 | } 140 | 141 | 142 | self.tokenService = function(email){ 143 | return function(){ 144 | var token = self.getToken(email); 145 | if(token.expire<(Date.now()/1000).toString().substr(0,10)){ 146 | self.requestToken() 147 | } 148 | return self.getToken(email).token; 149 | } 150 | } 151 | 152 | function makeClaim(subAccount){ 153 | var now = (Date.now()/1000).toString().substr(0,10); 154 | var exp = (parseInt(now) + 3600).toString().substr(0,10); 155 | expireTime_ = exp; 156 | var claim = 157 | { 158 | "iss": saEmail_, 159 | "sub": subAccount, 160 | "scope": Scopes_.join(" "), 161 | "aud":"https://www.googleapis.com/oauth2/v3/token", 162 | "iat": now, 163 | "exp": exp 164 | }; 165 | if(subAccount === saEmail_){ 166 | delete claim.sub; 167 | } 168 | return claim; 169 | } 170 | return self; 171 | } 172 | 173 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Spencer-Easton 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apps-Script-GSApp-Library 2 | Google Service Account Library for Apps Script 3 | 4 | This is an improved version of my Google service account library for Google Apps Script. This makes use of the new Utilities.computeRsaSha256Signature(value, key). I've added the ability to batch request tokens for users in your domain. If you need to request a token for your script itself add the client_email as a user. You can add the library with the project Id: `MJ5317VIFJyKpi9HCkXOfS0MLm9v2IJHf` or add from GSApp.gs found in this repo. 5 | 6 | You can contact me on google+ at: 7 | [Spencer Easton](https://plus.google.com/+SpencerEastonCCS) 8 | 9 | #### Using this Library 10 | ##### Create a Service Account 11 | 1) Create a new API Project in your developers console, or use one associated with your script. 12 | 2) Select APIs and Auth -> Credentials 13 | 3) Click 'Create New Client Id' 14 | 4) Select Service Account and Generate new JSON key 15 | 5) Click 'Create Client Id'. The JSON key will autodownload. 16 | 6) Click APIs in left menu 17 | 7) Enable any APIs you need this service account to access 18 | 19 | ##### Prepare your Service Account Key 20 | 1) Copy the contents of the JSON key and paste it into your scripts properties. Name the property as desired. In the example case I use `jsonKey`. It is a very bad practice to leave your key in your source code. 21 | 2) When you need access, use the following command: 22 | 23 | var jsonKey = JSON.parse(PropertiesService.getScriptProperties().getProperty("jsonKey")); 24 | var privateKey = jsonKey.private_key; 25 | var serviceAccountEmail = jsonKey.client_email; 26 | 27 | 28 | 29 | ##### Authorize the service account for your google domain 30 | If you are going to use the service account to request Oauth2 tokens for users in you domain do the following: 31 | 1) Launch admin.google.com as a domain admin 32 | 2) Open Security settings 33 | 3) Choose advanced settings 34 | 4) Choose Manage API client access 35 | 5) Add you service account client Id in the 'Client Name' box 36 | 6) Add the OAuth2 scopes to the APIs you enabled for this service account 'One or More API scopes' box 37 | 38 | 39 | ##### Adding the Library 40 | You can either either use the code from this repo directly in your project or include the library `MJ5317VIFJyKpi9HCkXOfS0MLm9v2IJHf` 41 | 42 | 43 | /* 44 | * Constructor for the GSApp Library. Use this to initialize a GSApp object. 45 | * @param {String} rsaKey The private_key from your service account JSON key 46 | * @param {Array} Scopes An Array of scopes you want to authenticate 47 | * @param {String} saEmail The client_email from your service account JSON key 48 | * @return {object} self for chaining 49 | */ 50 | function init(string RSAKey, array Scopes, string ServiceAccountEmail) 51 | 52 | 53 | /* 54 | * Adds a user to GSApp. 55 | * @param {String} userEmail The Email account of the user for whom you are requesting the token 56 | * @return {object} self for chaining 57 | *\ 58 | function addUser(string userEmail) 59 | 60 | /* 61 | * Adds an array of users to GSApp. 62 | * @param {[string]} userEmails Array of emails to be added. 63 | * @return {object} self for chaining 64 | *\ 65 | function addUsers([userEmails]) 66 | 67 | /* 68 | * Removes a user from GSApp. 69 | * @param {String} userEmail The Email account of the user you want to remove. 70 | * @return {object} self for chaining 71 | *\ 72 | function removeUser(string userEmail) 73 | 74 | /* 75 | * Removes all users from GSApp 76 | * @return {object} self for chaining 77 | *\ 78 | function removeUsers() 79 | 80 | /* 81 | * Requests an Oauth token for each user added. Saves it in the GSApp object. 82 | * @return {object} self for chaining 83 | *\ 84 | function requestToken() 85 | 86 | /* 87 | * Gets the Oauth token for the specified user. You must call requestToken() before this function. 88 | * @param {String} userEmail The email account of the user you want 89 | * @return {object} {token,expire} 90 | *\ 91 | function getToken(string userEmail) 92 | 93 | /* 94 | * Gets all the tokens generated by requestToken(). You must call requestToken() before this function. 95 | * @return {object} {user:{token,expire} , user:{token:expire} , ... } 96 | *\ 97 | function getTokens() 98 | 99 | /* 100 | * returns a function that can be invoked to get a fresh token 101 | * @param {String} userEmail The email account of the user you want 102 | * @return {function} 103 | *\ 104 | function tokenService(email) 105 | 106 | 107 | -------------------------------------------------------------------------------- /example.gs: -------------------------------------------------------------------------------- 1 | function example(){ 2 | 3 | 4 | var jsonKey = JSON.parse(PropertiesService.getScriptProperties().getProperty("jsonKey")); 5 | var key = jsonKey.private_key; 6 | var clientEmail = jsonKey.client_email; 7 | 8 | //example how to request OAuth2 tokens of your domain users 9 | var userTokens = GSApp.init(key, ['https://www.googleapis.com/auth/drive'], clientEmail); 10 | userTokens.addUser("1test@example.com") 11 | .addUser("1test@example.com") //add multiple users to batch process 12 | .removeUsers() //remove all users 13 | .addUser("1test@example.com") 14 | .addUsers(["2test@example.com","3test@example.com"]) //pass an array of user emails 15 | .removeUser("1test@example.com") //remove individual users 16 | .removeUser("Fonzie") // shouldn't break things 17 | .addUser("doesNotExist@example.com") //adds an invalid_grant error in the token property 18 | .requestToken(); //requests the tokens and saves them in GSApp 19 | Logger.log(userTokens.getTokens()); //returns tokens for all added users 20 | Logger.log(userTokens.getToken("1test@example.com")); //returns the token for the specified user 21 | 22 | //example how to request an Oauth2 token for your script 23 | var serverToken = new GSApp.init(key, ['https://www.googleapis.com/auth/drive'], clientEmail); 24 | sb.addUser(clientEmail) 25 | .requestToken(); 26 | Logger.log(serverToken.getTokens()); 27 | 28 | } 29 | --------------------------------------------------------------------------------