├── .npmrc ├── app.yaml ├── doc.png ├── env.json ├── src ├── server.js ├── config │ ├── express.js │ ├── logger.js │ ├── swagger.js │ ├── http.js │ └── swagger-config.js ├── util │ ├── util.js │ └── crypto-util.js ├── api │ └── index.js ├── routes │ └── index.js ├── entities │ └── index.js └── services │ └── index.js ├── .gcloudignore ├── README.md ├── .vscode └── launch.json ├── package.json ├── .gitignore └── LICENSE /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | runtime: nodejs16 -------------------------------------------------------------------------------- /doc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diegomdrs/nubank-token-api/HEAD/doc.png -------------------------------------------------------------------------------- /env.json: -------------------------------------------------------------------------------- 1 | { 2 | "local": { 3 | "PORT": 8080, 4 | "LOG_LEVEL": "debug" 5 | } 6 | } -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | const app = require('./config/express') 2 | 3 | require('dotenv').config() 4 | require('./config/http')(app) 5 | require('./config/swagger')(app) 6 | require('./routes')(app) 7 | -------------------------------------------------------------------------------- /src/config/express.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const bodyParser = require('body-parser') 3 | const app = express() 4 | 5 | app.use(express.json()) 6 | 7 | // app.use(express.urlencoded({ extended: false })) 8 | app.use(bodyParser.json()) 9 | 10 | module.exports = app -------------------------------------------------------------------------------- /src/config/logger.js: -------------------------------------------------------------------------------- 1 | const { pino } = require('pino'); 2 | 3 | const logger = pino({ 4 | transport: { 5 | target: 'pino-pretty', 6 | options: { 7 | ignore: 'pid,hostname' 8 | } 9 | }, 10 | level: process.env.LOG_LEVEL || 'debug', 11 | timestamp: pino.stdTimeFunctions.isoTime, 12 | }) 13 | 14 | module.exports = logger -------------------------------------------------------------------------------- /src/util/util.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parseAuthenticateHeaders: headerContent => { 3 | const chunks = headerContent.split(',') 4 | 5 | return chunks.reduce((map, chunk) => { 6 | const splited = chunk.split('=') 7 | let key = splited[0] 8 | let value = splited[1] 9 | 10 | key = key.trim().replace(' ', '_') 11 | value = value.replace(/"/g, '') 12 | map.set(key, value) 13 | 14 | return map 15 | }, new Map()) 16 | } 17 | } -------------------------------------------------------------------------------- /.gcloudignore: -------------------------------------------------------------------------------- 1 | # This file specifies files that are *not* uploaded to Google Cloud 2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of 3 | # "#!include" directives (which insert the entries of the given .gitignore-style 4 | # file at that point). 5 | # 6 | # For more information, run: 7 | # $ gcloud topic gcloudignore 8 | # 9 | .gcloudignore 10 | # If you would like to upload your .git directory, .gitignore file or files 11 | # from your .gitignore file, remove the corresponding line 12 | # below: 13 | .git 14 | .gitignore 15 | 16 | # Node.js dependencies: 17 | node_modules/ -------------------------------------------------------------------------------- /src/config/swagger.js: -------------------------------------------------------------------------------- 1 | const swaggerUi = require('swagger-ui-express') 2 | const swaggerAutogen = require('swagger-autogen')() 3 | 4 | module.exports = app => { 5 | const doc = require('./swagger-config') 6 | const outputFile = './swagger_output.json' 7 | const endpointsFiles = ['./src/routes/index.js'] 8 | 9 | swaggerAutogen(outputFile, endpointsFiles, doc).then(result => { 10 | if (result.success) { 11 | const swaggerFile = require('../../swagger_output.json') 12 | app.use('/doc', swaggerUi.serve, swaggerUi.setup(swaggerFile)) 13 | } 14 | }); 15 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nubank-token-api 2 | 3 | API para facilitar a criação e atualização do token (Bearer authentication) necessário para o acesso a API pública do Nubank (para consulta de extratos, saldo e etc.). Baseado no [pynubank](https://github.com/andreroggeri/pynubank) 4 | 5 | ## Instalando 6 | 7 | ~~~ 8 | npm install 9 | ~~~ 10 | 11 | ## Uso 12 | 13 | ~~~ 14 | npm start 15 | ~~~ 16 | 17 | A aplicação disponibiliza os seguintes endpoints: 18 | 19 | ### /requestCode 20 | 21 | Requisita o código de segurança a ser enviado para o email cadastrado no Nubank. 22 | 23 | ### /exchangeKey 24 | 25 | Obtém o certificado (base64) do dispositivo habilitado a obter um token. 26 | 27 | ### /getRefreshToken 28 | 29 | Obtém o token de acesso. 30 | 31 | ## Swagger 32 | 33 | Toda a API está documentada no Swagger da aplicação, no endereço [http://localhost:8080/doc](http://localhost:8080/doc). 34 | 35 | ![Doc](doc.png) 36 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "server launch", 9 | "type": "node", 10 | "request": "launch", 11 | "program": "${workspaceFolder}/src/server.js", 12 | "runtimeExecutable": "nodemon", 13 | "cwd": "${workspaceFolder}", 14 | "restart": true, 15 | "skipFiles": [ 16 | "/**" 17 | ], 18 | "console": "integratedTerminal", 19 | "internalConsoleOptions": "neverOpen", 20 | "env": { 21 | "PORT": "8080", 22 | "LOG_LEVEL": "debug" 23 | } 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nubank-token-api", 3 | "version": "1.0.0", 4 | "description": "API para facilitar a criação e atualização do token (Bearer authentication) necessário para o acesso a API pública do Nubank (para consulta de extratos, saldo e etc.). Baseado no projeto https://github.com/andreroggeri/pynubank", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node src/server.js", 9 | "dev": "env-cmd -r ./env.json -e local node src/server.js", 10 | "deploy": "gcloud app deploy", 11 | "start-gendoc": "node swagger.js" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "engines": { 16 | "node": ">=16" 17 | }, 18 | "nodemonConfig": { 19 | "ignore": [ 20 | "swagger_output.json" 21 | ] 22 | }, 23 | "dependencies": { 24 | "dotenv": "^14.2.0", 25 | "env-cmd": "^10.1.0", 26 | "express": "^4.17.2", 27 | "http-status-codes": "^2.2.0", 28 | "node-fetch": "^2.6.6", 29 | "pino": "^7.6.3", 30 | "pino-pretty": "^7.3.0", 31 | "swagger-autogen": "^2.21.1", 32 | "swagger-ui-express": "^4.3.0", 33 | "uuid": "^8.3.2" 34 | }, 35 | "devDependencies": { 36 | "nodemon": "^2.0.15" 37 | } 38 | } -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | const { StatusCodes } = require('http-status-codes') 2 | const { RequestCodeDTO, ResponseRequestCodeDTO, RequestExchangeKeyDTO, ResponseExchangeKeyDTO, RequestGetRefreshTokenDTO, ResponseGetRefreshTokenDTO } = require('../entities') 3 | const { requestCode, exchangeKey, getRefreshToken } = require('../services') 4 | 5 | module.exports = { 6 | requestCode: async (req, resp) => { 7 | try { 8 | const request = new RequestCodeDTO(req.body) 9 | const response = await requestCode(request) 10 | 11 | return resp.json(new ResponseRequestCodeDTO(response)); 12 | } catch (error) { 13 | resp.status(StatusCodes.INTERNAL_SERVER_ERROR).send({ message: error.message }) 14 | } 15 | }, 16 | 17 | exchangeKey: async (req, resp) => { 18 | try { 19 | const request = new RequestExchangeKeyDTO(req.body) 20 | const response = await exchangeKey(request) 21 | 22 | return resp.json(new ResponseExchangeKeyDTO(response)); 23 | } catch (error) { 24 | resp.status(StatusCodes.INTERNAL_SERVER_ERROR).send({ message: error.message }) 25 | } 26 | }, 27 | 28 | getRefreshToken: async (req, resp) => { 29 | try { 30 | const request = new RequestGetRefreshTokenDTO(req.body) 31 | const response = await getRefreshToken(request) 32 | 33 | return resp.json(new ResponseGetRefreshTokenDTO(response)); 34 | } catch (error) { 35 | resp.status(StatusCodes.INTERNAL_SERVER_ERROR).send({ message: error.message }) 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | const { requestCode, exchangeKey, getRefreshToken } = require('../api') 2 | 3 | /** 4 | * Ordem: 5 | * 6 | * 1. /requestCode 7 | * 2. /exchangeKey 8 | * 3. /getRefreshToken 9 | * 10 | * @param {*} app 11 | */ 12 | module.exports = function (app) { 13 | app.post('/requestCode', 14 | /* 15 | #swagger.tags = ['Token'] 16 | #swagger.description = 'Requisita o código de segurança a ser enviado para o email cadastrado no Nubank' 17 | #swagger.parameters['obj'] = { 18 | in: 'body', 19 | required: true, 20 | schema: { $ref: '#/definitions/RequestCodeDTO' } 21 | } 22 | #swagger.responses[200] = { 23 | schema: { $ref: '#/definitions/ResponseRequestCodeDTO' } 24 | } 25 | */ 26 | requestCode) 27 | 28 | app.post('/exchangeKey', 29 | 30 | /* 31 | #swagger.tags = ['Token'] 32 | #swagger.description = 'Obtém o certificado (base64) do dispositivo habilitado a obter um token' 33 | #swagger.parameters['obj'] = { 34 | in: 'body', 35 | required: true, 36 | schema: { $ref: '#/definitions/RequestExchangeKeyDTO' } 37 | } 38 | #swagger.responses[200] = { 39 | schema: { $ref: '#/definitions/ResponseExchangeKeyDTO' } 40 | } 41 | */ 42 | exchangeKey) 43 | app.post('/getRefreshToken', 44 | /* 45 | #swagger.tags = ['Token'] 46 | #swagger.description = 'Obtém o token de acesso' 47 | #swagger.parameters['obj'] = { 48 | in: 'body', 49 | required: true, 50 | schema: { $ref: '#/definitions/RequestGetRefreshTokenDTO' } 51 | } 52 | #swagger.responses[200] = { 53 | schema: { $ref: '#/definitions/ResponseGetRefreshTokenDTO' } 54 | } 55 | */ 56 | getRefreshToken) 57 | } -------------------------------------------------------------------------------- /src/config/http.js: -------------------------------------------------------------------------------- 1 | const logger = require('./logger'); 2 | 3 | module.exports = function (app) { 4 | const http = require('http'); 5 | 6 | /** 7 | * Get port from environment and store in Express. 8 | */ 9 | const port = normalizePort(process.env.PORT || 8080); 10 | app.set('port', port); 11 | 12 | /** 13 | * Create HTTP server. 14 | */ 15 | const server = http.createServer(app); 16 | 17 | /** 18 | * Listen on provided port, on all network interfaces. 19 | */ 20 | server.listen(port); 21 | 22 | server.on('error', onError); 23 | server.on('listening', onListening); 24 | 25 | /** 26 | * Normalize a port into a number, string, or false. 27 | */ 28 | function normalizePort(val) { 29 | const port = parseInt(val, 10); 30 | 31 | if (isNaN(port)) { 32 | // named pipe 33 | return val; 34 | } 35 | 36 | if (port >= 0) { 37 | // port number 38 | return port; 39 | } 40 | 41 | return false; 42 | } 43 | 44 | /** 45 | * Event listener for HTTP server "error" event. 46 | */ 47 | function onError(error) { 48 | if (error.syscall !== 'listen') { 49 | throw error; 50 | } 51 | 52 | const bind = typeof port === 'string' 53 | ? 'Pipe ' + port 54 | : 'Port ' + port; 55 | 56 | // handle specific listen errors with friendly messages 57 | switch (error.code) { 58 | case 'EACCES': 59 | logger.error(bind + ' requires elevated privileges'); 60 | process.exit(1); 61 | break; 62 | case 'EADDRINUSE': 63 | logger.error(bind + ' is already in use'); 64 | process.exit(1); 65 | break; 66 | default: 67 | throw error; 68 | } 69 | } 70 | 71 | /** 72 | * Event listener for HTTP server "listening" event. 73 | */ 74 | function onListening() { 75 | const addr = server.address(); 76 | const bind = typeof addr === 'string' 77 | ? 'pipe ' + addr 78 | : 'port ' + addr.port; 79 | logger.info('Listening on ' + bind) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/entities/index.js: -------------------------------------------------------------------------------- 1 | 2 | class RequestCodeDTO { 3 | constructor(body) { 4 | this.login = body.login 5 | this.password = body.password 6 | this.genCertificateUrl = body.genCertificateUrl 7 | this.deviceName = body.deviceName 8 | } 9 | } 10 | 11 | class ResponseRequestCodeDTO { 12 | constructor({ sentTo, encryptedCode, deviceId, privateKeyBase64, privateKeyCryptoBase64 }) { 13 | this.sentTo = sentTo 14 | this.encryptedCode = encryptedCode 15 | this.deviceId = deviceId 16 | this.privateKey = privateKeyBase64 17 | this.privateKeyCrypto = privateKeyCryptoBase64 18 | } 19 | } 20 | 21 | class RequestExchangeKeyDTO { 22 | constructor(body) { 23 | this.login = body.login 24 | this.password = body.password 25 | this.genCertificateUrl = body.genCertificateUrl 26 | this.deviceName = body.deviceName 27 | 28 | this.deviceId = body.deviceId 29 | this.codeSentByEmail = body.codeSentByEmail 30 | this.encryptedCode = body.encryptedCode 31 | this.privateKey = body.privateKey 32 | this.privateKeyCrypto = body.privateKeyCrypto 33 | } 34 | } 35 | 36 | class ResponseExchangeKeyDTO { 37 | constructor({ certificateBase64, validFrom, validTo }) { 38 | this.certificate = certificateBase64 39 | this.certificateValidFrom = validFrom, 40 | this.certificateValidTo = validTo 41 | } 42 | } 43 | 44 | class RequestGetRefreshTokenDTO { 45 | constructor(body) { 46 | this.login = body.login 47 | this.password = body.password 48 | this.tokenUrl = body.tokenUrl 49 | this.privateKey = body.privateKey 50 | this.certificate = body.certificate 51 | } 52 | } 53 | 54 | class ResponseGetRefreshTokenDTO { 55 | constructor({ refreshToken, refreshBefore }) { 56 | this.refreshToken = refreshToken 57 | this.refreshBefore = refreshBefore 58 | } 59 | } 60 | 61 | module.exports = { 62 | RequestCodeDTO, ResponseRequestCodeDTO, 63 | RequestExchangeKeyDTO, ResponseExchangeKeyDTO, 64 | RequestGetRefreshTokenDTO, ResponseGetRefreshTokenDTO 65 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # swagger-autogen 107 | swagger_output.json -------------------------------------------------------------------------------- /src/util/crypto-util.js: -------------------------------------------------------------------------------- 1 | const { generateKeyPairSync, createPrivateKey, createPublicKey, X509Certificate } = require('crypto') 2 | 3 | const PRIVATE_KEY_ENCODING_TYPE = 'pkcs8' 4 | const PUBLIC_KEY_ENCODING_TYPE = 'spki' 5 | 6 | module.exports = { 7 | extractPublicKeyPEMFromPrivateKeyPEM: privateKeyPEM => { 8 | return createPublicKey({ 9 | key: privateKeyPEM, 10 | format: 'pem' 11 | }).export({ 12 | format: 'pem', 13 | type: PUBLIC_KEY_ENCODING_TYPE 14 | }) 15 | }, 16 | 17 | convertCertificatePEMtoBase64: (certificatePEM) => { 18 | const x509 = new X509Certificate(certificatePEM) 19 | const certificateBase64 = convertDERtoBase64(x509.raw) 20 | return certificateBase64 21 | }, 22 | 23 | convertPrivateKeyBase64ToPEM: privateKeyBase64 => { 24 | const privateKeyDER = Buffer.from(privateKeyBase64, 'base64') 25 | const privateKeyPEM = convertPrivateKeyDERtoPEM(privateKeyDER) 26 | return privateKeyPEM 27 | }, 28 | 29 | convertPrivateKeyPEMToBase64: privateKeyPEM => { 30 | const privateKeyDER = createPrivateKey({ 31 | key: privateKeyPEM, 32 | format: 'pem', 33 | type: PRIVATE_KEY_ENCODING_TYPE 34 | }).export({ format: 'der', type: PRIVATE_KEY_ENCODING_TYPE }) 35 | return convertDERtoBase64(privateKeyDER) 36 | }, 37 | 38 | convertCertificateBase64toPEM: certificateBase64 => { 39 | const certificateDER = Buffer.from(certificateBase64, 'base64'); 40 | const certificatePEM = new X509Certificate(certificateDER).toString() 41 | return certificatePEM 42 | }, 43 | 44 | generateKeyPair: () => { 45 | return generateKeyPairSync('rsa', { 46 | modulusLength: 2048, 47 | privateKeyEncoding: { 48 | type: PRIVATE_KEY_ENCODING_TYPE, 49 | format: 'pem' 50 | }, 51 | publicKeyEncoding: { 52 | type: PUBLIC_KEY_ENCODING_TYPE, 53 | format: 'pem' 54 | } 55 | }) 56 | } 57 | } 58 | 59 | function convertPrivateKeyDERtoPEM(keyDER) { 60 | return createPrivateKey({ 61 | key: keyDER, 62 | format: 'der', 63 | type: PRIVATE_KEY_ENCODING_TYPE 64 | }).export({ format: 'pem', type: PRIVATE_KEY_ENCODING_TYPE }) 65 | } 66 | 67 | function convertDERtoBase64(der) { 68 | return Buffer.from(der).toString('base64') 69 | } 70 | -------------------------------------------------------------------------------- /src/services/index.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch') 2 | const { v4: uuidv4 } = require('uuid') 3 | const { Agent } = require('https') 4 | const { generateKeyPair, convertPrivateKeyPEMToBase64, convertPrivateKeyBase64ToPEM, extractPublicKeyPEMFromPrivateKeyPEM, convertCertificatePEMtoBase64, convertCertificateBase64toPEM } = require('../util/crypto-util') 5 | const { parseAuthenticateHeaders } = require('../util/util') 6 | const { X509Certificate } = require('crypto') 7 | const logger = require('../config/logger') 8 | const { StatusCodes } = require('http-status-codes') 9 | 10 | module.exports = { 11 | requestCode: async (request) => { 12 | const deviceId = uuidv4() 13 | 14 | const { publicKey: publicKeyPEM, privateKey: privateKeyPEM } = generateKeyPair() 15 | const { publicKey: publicKeyCryptoPEM, privateKey: privateKeyCryptoPEM } = generateKeyPair() 16 | 17 | // TODO - Criar classe disso aqui 18 | const payload = { 19 | 'login': request.login, 20 | 'password': request.password, 21 | 'public_key': publicKeyPEM, 22 | 'public_key_crypto': publicKeyCryptoPEM, 23 | 'model': `${request.deviceName} (${deviceId})`, 24 | 'device_id': deviceId 25 | } 26 | 27 | return fetch(request.genCertificateUrl, { 28 | method: 'post', 29 | headers: { 'Content-Type': 'application/json' }, 30 | body: JSON.stringify(payload) 31 | }).then(async res => { 32 | const json = await res.json() 33 | return { headers: res.headers, status: res.status, json } 34 | }).then(({ headers, status, json }) => { 35 | 36 | logger.info(`status: ${status}`) 37 | logger.debug(`json: ${JSON.stringify(json)}`) 38 | 39 | if (status != StatusCodes.UNAUTHORIZED) 40 | throw new Error(`Status code: ${status}`) 41 | 42 | const authCode = headers.get('WWW-Authenticate') 43 | logger.debug(`authCode: ${authCode}`) 44 | 45 | if (!authCode) 46 | throw new Error('Authentication code request failed.') 47 | 48 | const parsed = parseAuthenticateHeaders(authCode) 49 | 50 | const sentTo = parsed.get('sent-to') 51 | logger.info(`sentTo: ${sentTo}`) 52 | 53 | const encryptedCode = parsed.get('device-authorization_encrypted-code') 54 | logger.debug(`encryptedCode: ${encryptedCode}`) 55 | 56 | const privateKeyBase64 = convertPrivateKeyPEMToBase64(privateKeyPEM) 57 | const privateKeyCryptoBase64 = convertPrivateKeyPEMToBase64(privateKeyCryptoPEM) 58 | 59 | return { 60 | sentTo, 61 | encryptedCode, 62 | deviceId, 63 | privateKeyBase64, 64 | privateKeyCryptoBase64 65 | } 66 | }).catch(err => { 67 | logger.error(err) 68 | throw err 69 | }) 70 | }, 71 | 72 | exchangeKey: async (request) => { 73 | const privateKeyPEM = convertPrivateKeyBase64ToPEM(request.privateKey) 74 | const privateKeyCryptoPEM = convertPrivateKeyBase64ToPEM(request.privateKeyCrypto) 75 | 76 | const publicKeyPEM = extractPublicKeyPEMFromPrivateKeyPEM(privateKeyPEM) 77 | const publicKeyCryptoPEM = extractPublicKeyPEMFromPrivateKeyPEM(privateKeyCryptoPEM) 78 | 79 | // TODO - Criar classe disso aqui 80 | const payload = { 81 | 'login': request.login, 82 | 'password': request.password, 83 | 'public_key': publicKeyPEM, 84 | 'public_key_crypto': publicKeyCryptoPEM, 85 | 'model': `${request.deviceName} (${request.deviceId})`, 86 | 'device_id': request.deviceId, 87 | 'code': request.codeSentByEmail, 88 | 'encrypted-code': request.encryptedCode 89 | } 90 | 91 | return fetch(request.genCertificateUrl, { 92 | method: 'post', 93 | headers: { 'Content-Type': 'application/json' }, 94 | body: JSON.stringify(payload) 95 | }).then(async res => { 96 | const json = await res.json() 97 | return { headers: res.headers, status: res.status, json } 98 | }).then(({ status, json }) => { 99 | 100 | logger.info(`status: ${status}`) 101 | logger.debug(`json: ${JSON.stringify(json)}`) 102 | 103 | if (status != StatusCodes.OK) 104 | throw new Error(`Status code: ${status}`) 105 | 106 | const certificate = new X509Certificate(json.certificate) 107 | const certificateBase64 = convertCertificatePEMtoBase64(json.certificate) 108 | 109 | logger.debug(`validFrom: ${certificate.validFrom}`) 110 | logger.debug(`validTo: ${certificate.validTo}`) 111 | 112 | return { 113 | certificateBase64, 114 | validFrom: new Date(certificate.validFrom), 115 | validTo: new Date(certificate.validTo) 116 | } 117 | }).catch(err => { 118 | logger.error(err) 119 | throw err 120 | }) 121 | }, 122 | 123 | getRefreshToken: async (request) => { 124 | const privateKeyPEM = convertPrivateKeyBase64ToPEM(request.privateKey) 125 | const certificatePEM = convertCertificateBase64toPEM(request.certificate) 126 | 127 | const payload = { 128 | 'grant_type': 'password', 129 | 'client_id': 'legacy_client_id', 130 | 'client_secret': 'legacy_client_secret', 131 | 'login': request.login, 132 | 'password': request.password 133 | } 134 | 135 | const cert = { 136 | cert: certificatePEM, 137 | key: privateKeyPEM, 138 | passphrase: '', 139 | } 140 | const agent = new Agent({ ...cert, minVersion: "TLSv1.2", maxVersion: "TLSv1.2" }) 141 | 142 | return fetch(request.tokenUrl, { 143 | agent, 144 | method: 'post', 145 | headers: { 'Content-Type': 'application/json' }, 146 | body: JSON.stringify(payload) 147 | }).then(async res => { 148 | const json = await res.json() 149 | return { status: res.status, json } 150 | }).then(({ status, json }) => { 151 | logger.info(`status: ${status}`) 152 | logger.debug(`json: ${JSON.stringify(json)}`) 153 | 154 | if (status != 200) 155 | throw new Error(`Status code: ${status}`) 156 | 157 | // TODO - Entender access_token vs. access_token 158 | // const accessToken = json.access_token 159 | // logger.debug(`accessToken: ${accessToken}`) 160 | 161 | const refreshToken = json.access_token 162 | logger.debug(`refreshToken: ${refreshToken}`) 163 | 164 | const refreshBefore = json.refresh_before 165 | logger.debug(`refreshBefore: ${refreshBefore}`) 166 | 167 | return { refreshToken, refreshBefore } 168 | }).catch(err => { 169 | logger.error(err) 170 | throw err 171 | }) 172 | }, 173 | } -------------------------------------------------------------------------------- /src/config/swagger-config.js: -------------------------------------------------------------------------------- 1 | 2 | const packageInfo = require('../../package.json') 3 | 4 | module.exports = { 5 | info: { 6 | version: packageInfo.version, 7 | title: packageInfo.name, 8 | description: packageInfo.description 9 | }, 10 | host: "localhost:8080", 11 | basePath: "/", 12 | schemes: ['http', 'https'], 13 | consumes: ['application/json'], 14 | produces: ['application/json'], 15 | tags: [ 16 | { 17 | "name": "Token" 18 | } 19 | ], 20 | '@definitions': { 21 | RequestCodeDTO: { 22 | type: 'object', 23 | properties: { 24 | login: { 25 | type: 'string', 26 | description: 'O CPF com conta no Nubank', 27 | example: 'SEU CPF' 28 | }, 29 | password: { 30 | type: 'string', 31 | description: 'Senha Web do Nubank', 32 | example: 'SENHA WEB DO NUBANK' 33 | }, 34 | deviceName: { 35 | type: 'string', 36 | description: 'Apelido do dispositivo a ser autorizado a obter o token', 37 | example: 'MY DEVICE' 38 | }, 39 | genCertificateUrl: { 40 | type: 'string', 41 | description: 'URL para obtenção do código', 42 | example: 'https://prod-global-webapp-proxy.nubank.com.br/api/proxy/AJxL5LApUVAX0b5R5DnjMw3-9ibnk8UnZg.aHR0cHM6Ly9wcm9kLWdsb2JhbC1hdXRoLm51YmFuay5jb20uYnIvYXBpL2dlbi1jZXJ0aWZpY2F0ZXM' 43 | } 44 | }, 45 | required: ['login','password','deviceName','genCertificateUrl'] 46 | }, 47 | ResponseRequestCodeDTO: { 48 | type: 'object', 49 | properties: { 50 | sentTo: { 51 | type: 'string', 52 | description: 'Email que receberá o código de segurança', 53 | example: 'f**********o@email.com' 54 | }, 55 | encryptedCode: { 56 | type: 'string', 57 | description: 'Código encriptado do dispositivo autorizado', 58 | example: '[...]' 59 | }, 60 | deviceId: { 61 | type: 'string', 62 | description: 'Identificação (uuidv4) do dispositivo', 63 | example: 'cf38650a-b7f3-4dba-bc2f-3d1014a80378' 64 | }, 65 | privateKey: { 66 | type: 'string', 67 | description: 'Base64 da chave privada gerada', 68 | example: '[...]' 69 | }, 70 | privateKeyCrypto: { 71 | type: 'string', 72 | description: 'Base64 da chave privada gerada', 73 | example: '[...]' 74 | } 75 | }, 76 | required: ['sentTo','encryptedCode','deviceId','privateKey','privateKeyCrypto'] 77 | }, 78 | RequestExchangeKeyDTO: { 79 | type: 'object', 80 | properties: { 81 | login: { 82 | type: 'string', 83 | description: 'O CPF com conta no Nubank', 84 | example: 'SEU CPF' 85 | }, 86 | password: { 87 | type: 'string', 88 | description: 'Senha Web do Nubank', 89 | example: 'SENHA WEB DO NUBANK' 90 | }, 91 | deviceName: { 92 | type: 'string', 93 | description: 'Apelido do dispositivo a ser autorizado a obter o token de acesso', 94 | example: 'MY DEVICE' 95 | }, 96 | genCertificateUrl: { 97 | type: 'string', 98 | description: 'URL para obtenção do código', 99 | example: 'https://prod-global-webapp-proxy.nubank.com.br/api/proxy/AJxL5LApUVAX0b5R5DnjMw3-9ibnk8UnZg.aHR0cHM6Ly9wcm9kLWdsb2JhbC1hdXRoLm51YmFuay5jb20uYnIvYXBpL2dlbi1jZXJ0aWZpY2F0ZXM' 100 | }, 101 | codeSentByEmail: { 102 | type: 'string', 103 | description: 'Código de segurança enviado por email', 104 | example: 'yib5e6' 105 | }, 106 | encryptedCode: { 107 | type: 'string', 108 | description: 'Código encriptado do dispositivo autorizado', 109 | example: '[...]' 110 | }, 111 | deviceId: { 112 | type: 'string', 113 | description: 'Identificação (uuidv4) do dispositivo', 114 | example: 'cf38650a-b7f3-4dba-bc2f-3d1014a80378' 115 | }, 116 | privateKey: { 117 | type: 'string', 118 | description: 'Base64 da chave privada gerada', 119 | example: '[...]' 120 | }, 121 | privateKeyCrypto: { 122 | type: 'string', 123 | description: 'Base64 da chave privada gerada', 124 | example: '[...]' 125 | } 126 | }, 127 | required: ['login','password','deviceName','genCertificateUrl','codeSentByEmail','encryptedCode','deviceId','privateKey','privateKeyCrypto'] 128 | }, 129 | ResponseExchangeKeyDTO: { 130 | type: 'object', 131 | properties: { 132 | certificate: { 133 | type: 'string', 134 | description: 'Base64 do certificado do dispositivo autorizado a obter o token de acesso', 135 | example: '[...]' 136 | }, 137 | certificateValidFrom: { 138 | type: 'string', 139 | description: 'Data de início da validade do certificado', 140 | example: '2022-04-22T14:05:27.000Z' 141 | }, 142 | certificateValidTo: { 143 | type: 'string', 144 | description: 'Data final da validade do certificado', 145 | example: '2022-10-19T14:05:27.000Z' 146 | }, 147 | } 148 | }, 149 | RequestGetRefreshTokenDTO: { 150 | type: 'object', 151 | properties: { 152 | login: { 153 | type: 'string', 154 | description: 'O CPF com conta no Nubank', 155 | example: 'SEU CPF' 156 | }, 157 | password: { 158 | type: 'string', 159 | description: 'Senha Web do Nubank', 160 | example: 'SENHA WEB DO NUBANK' 161 | }, 162 | tokenUrl: { 163 | type: 'string', 164 | description: 'URL de obtenção do token', 165 | example: 'https://prod-global-auth.nubank.com.br/api/token' 166 | }, 167 | privateKey: { 168 | type: 'string', 169 | description: 'Base64 da chave privada gerada', 170 | example: '[...]' 171 | }, 172 | certificate: { 173 | type: 'string', 174 | description: 'Base64 do certificado do dispositivo autorizado a obter o token de acesso', 175 | example: '[...]' 176 | }, 177 | }, 178 | required: ['login','password','tokenUrl','privateKey','certificate'] 179 | }, 180 | ResponseGetRefreshTokenDTO: { 181 | type: 'object', 182 | properties: { 183 | refreshToken: { 184 | type: 'string', 185 | description: 'O token (JWT) de segurança', 186 | example: '[...]' 187 | }, 188 | refreshBefore: { 189 | type: 'string', 190 | description: 'Data final da validade do token', 191 | example: '2022-10-19T14:05:27.000Z' 192 | }, 193 | } 194 | }, 195 | } 196 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------