From e647002ce190e49820e4dd46b51905eda45e5770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Huan=20=28=E6=9D=8E=E5=8D=93=E6=A1=93=29?= Date: Mon, 9 Aug 2021 22:45:16 +0800 Subject: [PATCH] Add authorization support for checking token at server (PoC) (#146) * wip for add token authorization to grpc * workable auth poc * 0.25.2 * finish auth poc * 0.25.3 * 0.25.4 --- examples/auth/.gitignore | 13 +-- examples/auth/README.md | 6 +- examples/auth/client.ts | 40 +++---- examples/auth/generate.sh | 28 ++--- examples/auth/raw-https/README.md | 3 + examples/auth/raw-https/client.ts | 17 +++ examples/auth/raw-https/generate.sh | 17 +++ examples/auth/raw-https/server.ts | 15 +++ examples/auth/server.ts | 157 +++++++++++++++++++++++----- openapi/go.mod | 11 +- openapi/install.sh | 4 + package.json | 2 +- 12 files changed, 237 insertions(+), 76 deletions(-) create mode 100644 examples/auth/raw-https/README.md create mode 100644 examples/auth/raw-https/client.ts create mode 100755 examples/auth/raw-https/generate.sh create mode 100644 examples/auth/raw-https/server.ts diff --git a/examples/auth/.gitignore b/examples/auth/.gitignore index 3ab11ce4..41b14b36 100644 --- a/examples/auth/.gitignore +++ b/examples/auth/.gitignore @@ -1,8 +1,5 @@ -ca.crt -ca.key -client.crt -client.csr -client.key -server.crt -server.csr -server.key +# all generated openssl files +*.crt +*.key +*.csr +*.srl diff --git a/examples/auth/README.md b/examples/auth/README.md index cd6e923b..e3b3afcd 100644 --- a/examples/auth/README.md +++ b/examples/auth/README.md @@ -4,7 +4,11 @@ Use Wechaty TOKEN for authorization. ## DEVELOPMENT -1. Enable debug: `export GRPC_TRACE=all` +Enable debug trace messages: + +```sh +export GRPC_TRACE=all +``` ## GLOSSARY diff --git a/examples/auth/client.ts b/examples/auth/client.ts index f3b6d8d8..d3bc2636 100644 --- a/examples/auth/client.ts +++ b/examples/auth/client.ts @@ -1,3 +1,4 @@ +import { CallMetadataGenerator } from '@grpc/grpc-js/build/src/call-credentials' import fs from 'fs' import { @@ -23,31 +24,30 @@ export async function testDing (client: PuppetClient) { async function main () { const TOKEN = '__token__' + void TOKEN + + const rootCerts = fs.readFileSync('root-ca.crt') + + /** + * With server authentication SSL/TLS and a custom header with token + * https://grpc.io/docs/guides/auth/#with-server-authentication-ssltls-and-a-custom-header-with-token-1 + */ + const metaCallback: CallMetadataGenerator = (_params, callback) => { + const meta = new grpc.Metadata() + // metadata.add('authorization', `Wechaty ${TOKEN}`) + callback(null, meta) + } - const headerCreds = grpc.credentials.createFromMetadataGenerator((_callMetaOptoins, callback) => { - const metadata = new grpc.Metadata() - metadata.add('authorization', `Wechaty ${TOKEN}`) - callback(null, metadata) - }) - - // const certChain = fs.readFileSync('client.crt') - // const privateKey = fs.readFileSync('client.key') - // const rootCerts = null // fs.readFileSync('ca.crt') - void fs - - const creds = grpc.credentials.combineChannelCredentials( - // grpc.credentials.createInsecure(), - grpc.credentials.createSsl(), // rootCerts, privateKey, certChain), - headerCreds, - ) - - // const creds = grpc.credentials.createInsecure() + const channelCred = grpc.credentials.createSsl(rootCerts) + const callCred = grpc.credentials.createFromMetadataGenerator(metaCallback) + const combCreds = grpc.credentials.combineChannelCredentials(channelCred, callCred) const client = new PuppetClient( 'localhost:8788', - creds, + combCreds, { - 'grpc.default_authority': 'puppet_token', + 'grpc.default_authority': '__token__', + 'grpc.ssl_target_name_override': 'wechaty-puppet-service', }, ) diff --git a/examples/auth/generate.sh b/examples/auth/generate.sh index 0ba9fd77..a6d4ad03 100755 --- a/examples/auth/generate.sh +++ b/examples/auth/generate.sh @@ -5,29 +5,31 @@ set -e PASSPHRASE=wechaty +DAYS=3650 +SNI=wechaty-puppet-service # CA -openssl genrsa -passout pass:"$PASSPHRASE" -des3 -out ca.key 4096 -openssl req -passin pass:"$PASSPHRASE" -key ca.key -out ca.crt \ - -x509 -new -days 3650 \ - -subj "/C=US/ST=San Francisco/L=Palo Alto/O=Wechaty/OU=CA/CN=ca" +openssl genrsa -passout pass:"$PASSPHRASE" -des3 -out root-ca.key 4096 +openssl req -passin pass:"$PASSPHRASE" -key root-ca.key -out root-ca.crt \ + -x509 -new -days "$DAYS" \ + -subj "/C=US/ST=San Francisco/L=Palo Alto/O=Wechaty/OU=CA/CN=wechaty-root-ca" # Server -openssl genrsa -passout pass:"$PASSPHRASE" -des3 -out server.key 4096 +openssl genrsa -passout pass:"$PASSPHRASE" -des3 -out server.key 1024 openssl req -passin pass:"$PASSPHRASE" -new -out server.csr -key server.key \ - -subj "/C=US/ST=San Francisco/L=Palo Alto/O=Wechaty/OU=Server/CN=localhost" + -subj "/C=US/ST=San Francisco/L=Palo Alto/O=Wechaty/OU=Puppet/CN=${SNI}" -openssl x509 -req -passin pass:"$PASSPHRASE" -days 3650 -set_serial 01 \ - -CA ca.crt -CAkey ca.key \ +openssl x509 -req -passin pass:"$PASSPHRASE" -days "$DAYS" -set_serial 01 \ + -CA root-ca.crt -CAkey root-ca.key \ -in server.csr -out server.crt openssl rsa -passin pass:"$PASSPHRASE" -in server.key -out server.key # Client -openssl genrsa -passout pass:"$PASSPHRASE" -des3 -out client.key 4096 -openssl req -passin pass:"$PASSPHRASE" -new -key client.key -out client.csr \ - -subj "/C=US/ST=San Francisco/L=Palo Alto/O=Wechaty/OU=Client/CN=localhost" -openssl x509 -passin pass:"$PASSPHRASE" -req -days 3650 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out client.crt -openssl rsa -passin pass:"$PASSPHRASE" -in client.key -out client.key +# openssl genrsa -passout pass:"$PASSPHRASE" -des3 -out client.key 1024 +# openssl req -passin pass:"$PASSPHRASE" -new -key client.key -out client.csr \ +# -subj "/C=US/ST=San Francisco/L=Palo Alto/O=Wechaty/OU=Client/CN=${SNI}" +# openssl x509 -passin pass:"$PASSPHRASE" -req -days "$DAYS" -in client.csr -CA root-ca.crt -CAkey root-ca.key -set_serial 01 -out client.crt +# openssl rsa -passin pass:"$PASSPHRASE" -in client.key -out client.key diff --git a/examples/auth/raw-https/README.md b/examples/auth/raw-https/README.md new file mode 100644 index 00000000..a2fbe3ed --- /dev/null +++ b/examples/auth/raw-https/README.md @@ -0,0 +1,3 @@ +# See + +- [Monkey patching tls in node.js to support self-signed certificates with custom root certificate authorities](https://medium.com/trabe/monkey-patching-tls-in-node-js-to-support-self-signed-certificates-with-custom-root-cas-25c7396dfd2a) diff --git a/examples/auth/raw-https/client.ts b/examples/auth/raw-https/client.ts new file mode 100644 index 00000000..aa11eb17 --- /dev/null +++ b/examples/auth/raw-https/client.ts @@ -0,0 +1,17 @@ +import fs from 'fs' +import https from 'https' + +console.info('faint') + +https.request({ + ca: [fs.readFileSync('./rootCA.crt')], + hostname: '127.0.0.1', + method: 'GET', + path: '/', + port: 6000, + // rejectUnauthorized: false, +}, res => { + res.on('data', data => { + process.stdout.write(data) + }) +}).end() diff --git a/examples/auth/raw-https/generate.sh b/examples/auth/raw-https/generate.sh new file mode 100755 index 00000000..6f3f5940 --- /dev/null +++ b/examples/auth/raw-https/generate.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +# Private key for the root cert +openssl genrsa -des3 -out rootCA.key 4096 + +# root certificate +openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 365 -out rootCA.crt + +# Private key for the server cert +openssl genrsa -out server.key 2048 + +# Signing request for the server +openssl req -new -key server.key -out server.csr + +# Server cert using the root certificate +openssl x509 -req -in server.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial \ + -out server.crt -days 365 -sha256 diff --git a/examples/auth/raw-https/server.ts b/examples/auth/raw-https/server.ts new file mode 100644 index 00000000..036a8bfb --- /dev/null +++ b/examples/auth/raw-https/server.ts @@ -0,0 +1,15 @@ +import fs from 'fs' +import https from 'https' + +const server = https.createServer({ + cert: fs.readFileSync('./server.crt'), + key: fs.readFileSync('./server.key'), + // ca: fs.readFileSync('./rootCA.crt') +}) + +server.on('request', (_req, res) => { + res.writeHead(200) + res.end('Alive!\n') +}) + +server.listen(6000) diff --git a/examples/auth/server.ts b/examples/auth/server.ts index ee162dbf..d990e01b 100644 --- a/examples/auth/server.ts +++ b/examples/auth/server.ts @@ -13,37 +13,145 @@ import { import { puppetServerImpl, } from '../../tests/puppet-server-impl' -import { StatusBuilder } from '@grpc/grpc-js' +import { + StatusBuilder, + Metadata, + UntypedHandleCall, +} from '@grpc/grpc-js' +// import { Http2SecureServer } from 'http2' +import { + sendUnaryData, + ServerUnaryCall, +} from '@grpc/grpc-js/build/src/server-call' -const puppetServerExample: IPuppetServer = { - ...puppetServerImpl, +import http2 from 'http2' - ding: (call, callback) => { - const data = call.request.getData() - console.info(`ding(${data})`) - console.info('metadata:', call.metadata.getMap()) - console.info('getPeer:', call.getPeer()) - console.info('getDeadLine:', call.getDeadline()) +function monkeyPatchMetadataFromHttp2Headers ( + MetadataClass: typeof Metadata, +): void { + const fromHttp2Headers = MetadataClass.fromHttp2Headers + MetadataClass.fromHttp2Headers = function ( + headers: http2.IncomingHttpHeaders + ): Metadata { + const metadata = fromHttp2Headers.call(MetadataClass, headers) + + if (metadata.get('authorization').length <= 0) { + const authority = headers[':authority'] + const authorization = `Wechaty ${authority}` + metadata.set('authorization', authorization) + } + return metadata + } +} + +/** + * The following handlers using `cb` for errors + * handleUnaryCall + * handleClientStreamingCall + */ +type ServiceHandlerCb = (call: ServerUnaryCall, cb: sendUnaryData) => void +/** + * The following handlers using `emit` for errors + * handleServerStreamingCall + * handleBidiStreamingCall + */ +type ServiceHandlerEmit = (call: ServerUnaryCall) => void +type ServiceHandler = ServiceHandlerCb | ServiceHandlerEmit + +/** + * Huan(202108): wrap the following handle calls with authorization: + * - handleUnaryCall + * - handleClientStreamingCall + * - handleServerStreamingCall + * - handleBidiStreamingCall + * + * See: + * https://grpc.io/docs/guides/auth/#with-server-authentication-ssltls-and-a-custom-header-with-token + */ +function authHandler ( + validToken : string, + handler : UntypedHandleCall, +): ServiceHandler { + console.info('wrapAuthHandler', handler.name) + return function ( + call: ServerUnaryCall, + cb?: sendUnaryData, + ) { + // console.info('wrapAuthHandler internal') + + const authorization = call.metadata.get('authorization')[0] + // console.info('authorization', authorization) + + let errMsg = '' + if (typeof authorization === 'string') { + if (authorization.startsWith('Wechaty ')) { + const token = authorization.substring(8 /* 'Wechaty '.length */) + if (token === validToken) { + + return handler( + call as any, + cb as any, + ) + + } else { + errMsg = `Invalid Wechaty TOKEN "${token}"` + } + } else { + const type = authorization.split(/\s+/)[0] + errMsg = `Invalid authorization type: "${type}"` + } + } else { + errMsg = 'No Authorization found.' + } + /** + * Not authorized + */ const error = new StatusBuilder() .withCode(GrpcServerStatus.UNAUTHENTICATED) - .withDetails('The server need "Authorization: Wechaty TOKEN" to accept the request') + .withDetails(errMsg) .withMetadata(call.metadata) .build() - void error - callback(null, new DingResponse()) + if (cb) { + cb(error) + } else if ('emit' in call) { + call.emit('error', error) + } else { + throw new Error('no callback and call is not emit-able') + } + } +} + +const wechatyAuthToken = (validToken: string) => ( + puppetServer: IPuppetServer, +) => { + for (const [key, val] of Object.entries(puppetServer)) { + puppetServer[key] = authHandler(validToken, val) + } + return puppetServer +} + +monkeyPatchMetadataFromHttp2Headers(Metadata) + +const puppetServerExample: IPuppetServer = { + ...puppetServerImpl, - // console.info('getDeadLine:', call.) - // callback(error, new DingResponse()) + ding: (call, callback) => { + const data = call.request.getData() + console.info(`ding(${data})`) + console.info('authorization:', call.metadata.getMap()['authorization']) + callback(null, new DingResponse()) }, } async function main () { + const puppetServerExampleWithAuth = wechatyAuthToken('__token__')(puppetServerExample) + const server = new grpc.Server() server.addService( PuppetService, - puppetServerExample, + puppetServerExampleWithAuth, ) const serverBindPromise = util.promisify( @@ -52,19 +160,18 @@ async function main () { void fs - const rootCerts: null | Buffer = null // fs.readFileSync('ca.crt') - const keyCertPairs: grpc.KeyCertPair[] = [] - // [{ - // cert_chain : fs.readFileSync('server.crt'), - // private_key : fs.readFileSync('server.key'), - // }] - const checkClientCertificate = false + const rootCerts: null | Buffer = fs.readFileSync('root-ca.crt') + void rootCerts + const keyCertPairs: grpc.KeyCertPair[] = [{ + cert_chain : fs.readFileSync('server.crt'), + private_key : fs.readFileSync('server.key'), + }] + // const checkClientCertificate = false const port = await serverBindPromise( - '127.0.0.1:8788', + '0.0.0.0:8788', // grpc.ServerCredentials.createInsecure(), - grpc.ServerCredentials.createSsl(rootCerts, keyCertPairs, checkClientCertificate), - + grpc.ServerCredentials.createSsl(null, keyCertPairs) //, checkClientCertificate), ) console.info('Listen on port:', port) server.start() diff --git a/openapi/go.mod b/openapi/go.mod index 0be16c01..4e07be4c 100644 --- a/openapi/go.mod +++ b/openapi/go.mod @@ -3,12 +3,7 @@ module github.com/wechaty/grpc/openapi go 1.16 require ( - github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b - github.com/golang/protobuf v1.4.3 // indirect - github.com/google/protobuf v3.15.0+incompatible // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.1.0 - github.com/wechaty/go-grpc v0.18.12 // indirect - google.golang.org/grpc v1.34.0 - google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 - google.golang.org/protobuf v1.25.0 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.5.0 // indirect + google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 // indirect + google.golang.org/protobuf v1.27.1 // indirect ) diff --git a/openapi/install.sh b/openapi/install.sh index 9e17a69c..363e976d 100755 --- a/openapi/install.sh +++ b/openapi/install.sh @@ -7,6 +7,10 @@ SCRIPTPATH="$( cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 ; pwd -P )" THIRD_PARTY_DIR="${SCRIPTPATH}/../third-party/" function go_install () { + # + # Huan(202108): https://github.com/golang/go/issues/44129 + # workaround: `go get ...` first. + # go install \ github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway \ github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2 \ diff --git a/package.json b/package.json index 3438ddc2..a5d37d32 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wechaty-grpc", - "version": "0.25.1", + "version": "0.25.4", "description": "gRPC for Wechaty", "main": "dist/src/mod.js", "typings": "dist/src/mod.d.js",