├── index.js ├── LICENSE ├── package.json ├── src ├── clientMessages.js ├── serverCalls.js ├── clientCalls.js └── commonCalls.js └── README.md /index.js: -------------------------------------------------------------------------------- 1 | const client = require('./src/clientCalls.js') 2 | const server = require('./src/serverCalls.js') 3 | const messages = require('./src/clientMessages') 4 | 5 | client.generateMessage = messages.generateMessage 6 | 7 | module.exports = { 8 | client: client, 9 | server: server 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Hydrogen 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hydrogenplatform/raindrop", 3 | "version": "0.2.7", 4 | "description": "Convenience functions to integrate Raindrop authentication into your project.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "raindrop", 11 | "authentication", 12 | "web3", 13 | "ethereum", 14 | "blockchain", 15 | "fintech", 16 | "two-factor", 17 | "2FA" 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/hydrogen-dev/raindrop-sdk-js" 22 | }, 23 | "author": "Noah Zinsmeister (https://www.hydrogenplatform.com/hydro)", 24 | "license": "MIT", 25 | "devDependencies": { 26 | "eslint": "^4.19.1", 27 | "eslint-config-standard": "^11.0.0", 28 | "eslint-plugin-import": "^2.13.0", 29 | "eslint-plugin-node": "^6.0.1", 30 | "eslint-plugin-promise": "^3.8.0", 31 | "eslint-plugin-standard": "^3.1.0" 32 | }, 33 | "dependencies": { 34 | "create-error": "^0.3.1", 35 | "request": "^2.87.0", 36 | "request-promise-native": "^1.0.5" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/clientMessages.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto') 2 | 3 | function generateMessage () { 4 | return crypto.randomBytes ? generateMessageNode() : generateMessageBrowser() 5 | } 6 | 7 | function generateMessageBrowser () { 8 | var uints 9 | var challenges 10 | do { 11 | // using Uint32Array(1) won't work because bitwise operations convert to signed 32 bit ints, leading to overflows 12 | uints = new Uint16Array(2) 13 | window.crypto.getRandomValues(uints) 14 | // to get two csprns in [0, 1e3) they need 10 random bits each, so we get 10 = 16-6 bytes per uint16 15 | challenges = uints.map(u => { return u >> 6 }) 16 | } while (!challenges.every(u => { return u < 1e3 })) // repeat if numbers >= 1e3, to maintain a uniform distribution 17 | 18 | return Array.from(challenges).map(u => { return u.toString().padStart(3, '0') }).join('') 19 | } 20 | 21 | function generateMessageNode () { 22 | var challenge 23 | do { 24 | // to get a csprn in [0, 1e6) we need 20 random bits, so we get 3 random bytes := 24 bits and shift off 4 bits 25 | challenge = crypto.randomBytes(3).readUIntBE(0, 3) >> 4 26 | } while (challenge >= 1e6) // repeat if number >= 1e6, to maintain a uniform distribution 27 | 28 | return challenge.toString().padStart(6, '0') 29 | } 30 | 31 | module.exports = { 32 | generateMessage: generateMessage 33 | } 34 | -------------------------------------------------------------------------------- /src/serverCalls.js: -------------------------------------------------------------------------------- 1 | const common = require('./commonCalls') 2 | 3 | class RaindropPartner extends common.BasicPartner {} 4 | 5 | RaindropPartner.prototype.whitelist = function (addressToWhitelist) { 6 | var options = { 7 | method: 'POST', 8 | body: {address: addressToWhitelist} 9 | } 10 | 11 | return this.callHydroAPI(`/whitelist`, options) 12 | } 13 | 14 | RaindropPartner.prototype.requestChallenge = function (hydroAddressId) { 15 | var options = { 16 | method: 'POST', 17 | body: { 18 | hydro_address_id: hydroAddressId 19 | } 20 | } 21 | 22 | return this.callHydroAPI('/challenge', options) 23 | } 24 | 25 | RaindropPartner.prototype.authenticate = function (hydroAddressId) { 26 | var options = { 27 | method: 'GET', 28 | qs: { 29 | hydro_address_id: hydroAddressId 30 | } 31 | } 32 | 33 | var receivedVerboseValue = this.verbose 34 | this.verbose = true 35 | 36 | return this.callHydroAPI('/authenticate', options) 37 | .then(result => { 38 | this.verbose = receivedVerboseValue 39 | return { authenticated: true, data: result } 40 | }) 41 | .catch(error => { 42 | this.verbose = receivedVerboseValue 43 | if (error.statusCode === 401) { 44 | return { authenticated: false } 45 | } else { 46 | throw new common.RaindropError(`The call failed. ${error.statusCode} error: ${error.message}.`) 47 | } 48 | }) 49 | } 50 | 51 | module.exports = { 52 | RaindropPartner: RaindropPartner 53 | } 54 | -------------------------------------------------------------------------------- /src/clientCalls.js: -------------------------------------------------------------------------------- 1 | const common = require('./commonCalls') 2 | 3 | class RaindropPartner extends common.BasicPartner { 4 | constructor (config) { 5 | if (!config.applicationId) { 6 | throw new common.RaindropError('Please provide your applicationId in the config: {applicationId: ..., ...}') 7 | } 8 | let clientConfig = Object.assign({}, config) 9 | delete config.applicationId 10 | 11 | super(config) 12 | 13 | this.applicationId = clientConfig.applicationId 14 | } 15 | } 16 | 17 | RaindropPartner.prototype.registerUser = function (hydroID) { 18 | var options = { 19 | method: 'POST', 20 | body: { 21 | hydro_id: hydroID, 22 | application_id: this.applicationId 23 | } 24 | } 25 | 26 | return this.callHydroAPI('/application/client', options) 27 | } 28 | 29 | RaindropPartner.prototype.verifySignature = function (hydroID, challengeString) { 30 | var options = { 31 | method: 'GET', 32 | qs: { 33 | message: challengeString, 34 | hydro_id: hydroID, 35 | application_id: this.applicationId 36 | } 37 | } 38 | 39 | var receivedVerboseValue = this.verbose 40 | this.verbose = true 41 | 42 | return this.callHydroAPI('/verify_signature', options) 43 | .then(result => { 44 | this.verbose = receivedVerboseValue 45 | return { verified: true, data: result } 46 | }) 47 | .catch(error => { 48 | this.verbose = receivedVerboseValue 49 | if (error.statusCode === 401) { 50 | return { verified: false } 51 | } else { 52 | throw new common.RaindropError(`The call failed. ${error.statusCode} error: ${error.message}.`) 53 | } 54 | }) 55 | } 56 | 57 | RaindropPartner.prototype.unregisterUser = function (hydroID) { 58 | var options = { 59 | method: 'DELETE', 60 | qs: { 61 | hydro_id: hydroID, 62 | application_id: this.applicationId 63 | } 64 | } 65 | 66 | return this.callHydroAPI('/application/client', options) 67 | } 68 | 69 | module.exports = { 70 | RaindropPartner: RaindropPartner 71 | } 72 | -------------------------------------------------------------------------------- /src/commonCalls.js: -------------------------------------------------------------------------------- 1 | const createError = require('create-error') 2 | const requestPromise = require('request-promise-native') 3 | 4 | const RaindropError = createError('RaindropError', { 5 | code: 'RaindropError' 6 | }) 7 | 8 | function encodeBasicAuth (id, secret) { 9 | let base64Encoded = Buffer.from(`${id}:${secret}`).toString('base64') 10 | return `Basic ${base64Encoded}` 11 | } 12 | 13 | const setVerbose = Symbol('setVerbose') 14 | const setEnvironment = Symbol('setEnvironment') 15 | const ensureInitialized = Symbol('ensureInitialized') 16 | 17 | class BasicPartner { 18 | constructor (config) { 19 | // check that the passed names are valid... 20 | let requiredOptions = ['clientId', 'clientSecret', 'environment'] 21 | let optionalOptions = ['verbose'] 22 | let acceptableOptions = requiredOptions.concat(optionalOptions) 23 | 24 | if (!Object.keys(config).every(x => acceptableOptions.includes(x))) { 25 | throw new RaindropError(`Invalid option. Valid options are: ${acceptableOptions.toString()}`) 26 | } 27 | 28 | if (!requiredOptions.every(x => x in config)) { 29 | throw new RaindropError(`Missing option. Please include all of: ${requiredOptions.toString()}`) 30 | } 31 | 32 | this.clientId = config.clientId 33 | this.clientSecret = config.clientSecret 34 | 35 | this[setEnvironment](config.environment) 36 | if (config.verbose !== undefined) { 37 | this[setVerbose](config.verbose) 38 | } 39 | 40 | this.initialized = this.refreshToken() 41 | .then(result => { this.initialized = result }) 42 | } 43 | 44 | [setEnvironment] (environment) { 45 | let allowedValues = { 46 | Sandbox: 'https://sandbox.hydrogenplatform.com', 47 | Production: 'https://api.hydrogenplatform.com' 48 | } 49 | 50 | if (!(environment in allowedValues)) { 51 | throw new RaindropError( 52 | `Invalid 'environment' value: Allowed options are: ${Object.keys(allowedValues).toString()}` 53 | ) 54 | } 55 | 56 | this.environment = environment 57 | this.apiURL = allowedValues[environment] 58 | } 59 | 60 | [setVerbose] (flag) { 61 | let allowedValues = [true, false] 62 | if (!allowedValues.includes(flag)) { 63 | throw new RaindropError(`Invalid 'verbose' value: Allowed options are: ${allowedValues.toString()}`) 64 | } 65 | this.verbose = flag 66 | } 67 | 68 | [ensureInitialized] () { 69 | if (!this.initialized) { 70 | throw new RaindropError( 71 | 'Error fetching OAuth token. Check that your credentials were entered correctly, and try calling ' + 72 | 'refreshToken() manually. If the problem persists, please file a Github issue.') 73 | } 74 | } 75 | 76 | refreshToken () { 77 | var options = { 78 | method: 'POST', 79 | timeout: 10000, // 10 seconds 80 | url: `${this.apiURL}/authorization/v1/oauth/token`, 81 | qs: { grant_type: 'client_credentials' }, 82 | headers: { Authorization: encodeBasicAuth(this.clientId, this.clientSecret) }, 83 | json: true 84 | } 85 | 86 | return requestPromise(options) 87 | .then(result => { 88 | this.OAuthToken = result.access_token 89 | return true 90 | }) 91 | .catch(() => { 92 | return false 93 | }) 94 | } 95 | 96 | async callHydroAPI (endpoint, options) { 97 | await this.initialized 98 | this[ensureInitialized]() 99 | 100 | // inject url and authorization 101 | options.headers = { Authorization: `Bearer ${this.OAuthToken}` } 102 | options.url = `${this.apiURL}/hydro/v1${endpoint}` 103 | options.json = true 104 | 105 | // return the data 106 | return requestPromise(options) 107 | .then(result => { 108 | return result 109 | }) 110 | .catch(error => { 111 | if (this.verbose) { 112 | throw error 113 | } else { 114 | throw new RaindropError(`The call failed. ${error.statusCode} error: ${error.message}.`) 115 | } 116 | }) 117 | } 118 | 119 | transactionStatus (transactionHash) { 120 | var options = { 121 | method: 'GET', 122 | qs: { transaction_hash: transactionHash } 123 | } 124 | 125 | return this.callHydroAPI('/transaction', options) 126 | } 127 | } 128 | 129 | module.exports = { 130 | BasicPartner: BasicPartner, 131 | RaindropError: RaindropError 132 | } 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hydro Raindrop 2 | This package provides a suite of convenience functions intended to simplify the integration of Hydro's [Raindrop authentication](https://www.hydrogenplatform.com/hydro) into your project. An equivalent [Python SDK](https://github.com/hydrogen-dev/raindrop-sdk-python) is also available. More information, including detailed API documentation, is available in the [Raindrop documentation](https://www.hydrogenplatform.com/docs/hydro/v1/#Raindrop). Raindrop comes in two flavors: 3 | 4 | ## Client-side Raindrop 5 | Client-side Raindrop is a next-gen 2FA solution. We've open-sourced the [code powering Client-side Raindrop](https://github.com/hydrogen-dev/smart-contracts/tree/master/client-raindrop). 6 | 7 | 8 | ## Server-side Raindrop 9 | Server-side Raindrop is an enterprise-level security protocol to secure APIs and other shared resources. We've open-sourced the [code powering Server-side Raindrop](https://github.com/hydrogen-dev/smart-contracts/tree/master/hydro-token-and-raindrop-enterprise). 10 | 11 | 12 | ## Installation 13 | ### Recommended 14 | Install [raindrop on npm](https://www.npmjs.com/package/@hydrogenplatform/raindrop): 15 | ```shell 16 | npm install -S @hydrogenplatform/raindrop 17 | ``` 18 | 19 | ### Manual 20 | You can also install manually: 21 | - `git clone https://github.com/hydrogen-dev/raindrop-sdk-js.git` 22 | - `cd raindrop-sdk-js` 23 | - `npm install` 24 | 25 | 26 | ## Usage 27 | The `raindrop` package exposes two objects: `raindrop.client` and `raindrop.server`. To start making API calls, you'll need to instantiate a `RaindropPartner` object for the module you'd like to use. The SDK will automatically fetch you an [OAuth token](https://www.hydrogenplatform.com/docs/hydro/v1/#Authentication), and set [your environment](https://www.hydrogenplatform.com/docs/hydro/v1/#Environment). 28 | 29 | ```javascript 30 | const raindrop = require("@hydrogenplatform/raindrop") 31 | ``` 32 | 33 | ## Generic `RaindropPartner` Functions 34 | ### constructor `new RaindropPartner(config)` 35 | To instantiate a new RaindropPartner object in the `client` or `server` modules, you must pass a config object with the following values: 36 | - `config` 37 | - `environment` (required): `Sandbox` | `Production` to set your environment 38 | - `clientId` (required): Your OAuth id for the Hydro API 39 | - `clientSecret` (required): Your OAuth secret for the Hydro API 40 | - `applicationId` (required for `client` calls): Your application id for the Hydro API 41 | - `verbose` (optional): `true` | `false` turns more detailed error reporting on | off 42 | 43 | ### `RaindropPartnerObject.refreshToken()` 44 | Manually refreshes OAuth token. 45 | 46 | ### `RaindropPartnerObject.transactionStatus(transactionHash)` 47 | This function returns true when the transaction referenced by `transactionHash` has been included in a block on the Ethereum blockchain (Rinkeby if the environment is `Sandbox`, Mainnet if the environment is `Production`). 48 | - `transactionHash` (required): Hash of a transaction 49 | 50 | ## Generic `raindrop.client` Functions 51 | 52 | ### `generateMessage()` 53 | Generates a random 6-digit string of integers for users to sign. Uses system-level CSPRNG. 54 | 55 | ## `raindrop.client.RaindropPartner` Functions 56 | Client-side Raindrop initialization code will look like: 57 | 58 | ```javascript 59 | // Client-side Raindrop Setup 60 | const ClientRaindropPartner = new raindrop.client.RaindropPartner({ 61 | environment: "Sandbox", 62 | clientId: "yourId", 63 | clientSecret: "yourSecret", 64 | applicationId: "yourApplicationId" 65 | }) 66 | ``` 67 | 68 | ### `registerUser(HydroID)` 69 | Should be called when a user elects to use Raindrop Client for the first time with your application. 70 | - `HydroID`: the new user's HydroID (the one they used when signing up for Hydro mobile app) 71 | 72 | ### `verifySignature(HydroID, message)` 73 | Should be called each time you need to verify whether a user has signed a message. 74 | - `HydroID`: the HydroID of the user that is meant to have signed `message` 75 | - `message`: a message generated from `generateMessage()` (or any 6-digit numeric code) 76 | 77 | Returns a response object that looks like: `{verified: true, data: {...}}`. The `verified` parameter will only be `true` for successful verification attempts. 78 | 79 | ### `unregisterUser(HydroID)` 80 | Should be called when a user disables Client-side Raindrop with your application. 81 | - `HydroID`: the user's HydroID (the one they used when signing up for Hydro mobile app) 82 | 83 | ## `raindrop.server.RaindropPartner` Functions 84 | Server-side Raindrop initialization code will look like: 85 | 86 | ```javascript 87 | // Server-side Raindrop Setup 88 | const ServerRaindropPartner = new raindrop.server.RaindropPartner({ 89 | environment: "Sandbox", 90 | clientId: "yourId", 91 | clientSecret: "yourSecret" 92 | }) 93 | ``` 94 | 95 | ### `whitelist(addressToWhitelist)` 96 | A one-time call that whitelists a user to authenticate with your API via Server-side Raindrop. 97 | - `addressToWhitelist`: The Ethereum address of the user you're whitelisting 98 | 99 | ### `requestChallenge(hydroAddressId)` 100 | Initiate an authentication attempt on behalf of the user associated with `hydroAddressId`. 101 | - `hydroAddressId`: the `hydro_address_id` of the authenticating user 102 | 103 | ### `authenticate(hydroAddressId)` 104 | Checks whether the user correctly performed the raindrop. 105 | - `hydroAddressId`: the `hydro_address_id` of the user who claims to be authenticated 106 | 107 | Returns a response object that looks like: `{authenticated: true, data: {...}}`. The `authenticated` parameter will only be `true` for successful authentication attempts. 108 | 109 | ## Copyright & License 110 | Copyright 2018 The Hydrogen Technology Corporation under the MIT License. 111 | --------------------------------------------------------------------------------