Last active
November 13, 2017 21:45
-
-
Save jasonpolites/dfce01b168420e676823350da7a031b3 to your computer and use it in GitHub Desktop.
Demo of manually signing a GCS url
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Copyright 2017, Google, Inc. | |
* Licensed under the Apache License, Version 2.0 (the "License") | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
'use strict'; | |
const rp = require('request-promise'); | |
/** | |
* @param {string} objectRef The Canonicalized Resource Reference for the object in GCS (e.g. /bucket/objectname). | |
* @param {number} ttlSeconds The number of seconds from `now` this link should remain valid (e.g 60). | |
* @param {string} contentType (Optional) The content type of the object (provide only if the client sends a content-type header in the request). | |
*/ | |
exports.signUrlForUpload = (objectRef, ttlSeconds, contentType) => { | |
return signUrl('PUT', objectRef, ttlSeconds, contentType); | |
} | |
/** | |
* @param {string} objectRef The Canonicalized Resource Reference for the object in GCS (e.g. /bucket/objectname). | |
* @param {number} ttlSeconds The number of seconds from `now` this link should remain valid (e.g 60). | |
* @param {string} contentType (Optional) The content type of the object (provide only if the client sends a content-type header in the request). | |
*/ | |
exports.signUrlForDownload = (objectRef, ttlSeconds, contentType) => { | |
return signUrl('GET', objectRef, ttlSeconds, contentType); | |
} | |
function signUrl(method, objectRef, ttlSeconds, contentType) { | |
let now = parseInt(Date.now() / 1000); // ms to seconds | |
let expires = now + parseInt(ttlSeconds); | |
let stringToSign = makeSignedUrlString(method, objectRef, expires, contentType); | |
console.log(`String to sign: ${stringToSign}`); | |
return signUrlString(stringToSign).then((value) => { | |
console.log(`Got signed blob payload: ${JSON.stringify(value)}`); | |
let signature = encodeURIComponent(value.signedBlob.signature); | |
let baseUrl = `https://storage.googleapis.com${objectRef}?GoogleAccessId=${value.email}&Expires=${expires}&Signature=${signature}`; | |
console.log(`Base url: ${baseUrl}`); | |
return Promise.resolve(baseUrl); | |
}); | |
} | |
function makeSignedUrlString(method, object, expires, contentType) { | |
contentType = contentType || ''; | |
// TODO: These probably should be params to the fn | |
let md5 = ''; | |
// StringToSign = HTTP_Verb + "\n" + | |
// Content_MD5 + "\n" + | |
// Content_Type + "\n" + | |
// Expiration + "\n" + | |
// Canonicalized_Extension_Headers + "\n" + | |
// Canonicalized_Resource | |
let stringToSign = `${method}\n${md5}\n${contentType}\n${expires}\n${object}`; | |
console.log(`String to sign (pre base64): [${stringToSign}]`); | |
return new Buffer(stringToSign).toString('base64'); | |
} | |
function signUrlString(urlString) { | |
let serviceNamePromise = callMetadataService('email'); | |
let serviceTokenPromise = callMetadataService('token'); | |
let email; | |
return Promise.all([ | |
serviceNamePromise, | |
serviceTokenPromise]) | |
.then(([serviceAccountName, serviceAccountToken]) => { | |
let projectId = process.env.GCLOUD_PROJECT; | |
email = serviceAccountName; | |
let accessToken = JSON.parse(serviceAccountToken).access_token; | |
return signBlob(urlString, projectId, email, accessToken); | |
}).then((signedBlob) => { | |
return Promise.resolve({ | |
'email' : email, | |
'signedBlob': signedBlob | |
}); | |
}); | |
} | |
function callMetadataService(endpoint) { | |
return rp({ | |
uri: `http://metadata/computeMetadata/v1/instance/service-accounts/default/${endpoint}`, | |
headers: { | |
'Metadata-Flavor': 'Google' | |
} | |
}); | |
} | |
function signBlob(blob, projectId, serviceAccount, accessToken) { | |
let uri = `https://iam.googleapis.com/v1/projects/${projectId}/serviceAccounts/${serviceAccount}:signBlob`; | |
let bearerToken = `Bearer ${accessToken}`; | |
return rp({ | |
method: 'POST', | |
uri: uri, | |
headers: { | |
'Authorization': bearerToken | |
}, | |
body: { | |
'bytesToSign': blob | |
}, | |
json: true | |
}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment