diff --git a/cmd/token/token.go b/cmd/token/token.go index 7e59b22..0485b1d 100644 --- a/cmd/token/token.go +++ b/cmd/token/token.go @@ -25,6 +25,8 @@ import ( "time" "github.com/apigee/apigee-remote-service-cli/shared" + "github.com/apigee/apigee-remote-service-envoy/server" + "github.com/lestrrat-go/jwx/jwa" "github.com/lestrrat-go/jwx/jwk" "github.com/lestrrat-go/jwx/jws" "github.com/lestrrat-go/jwx/jwt" @@ -41,10 +43,11 @@ const ( type token struct { *shared.RootArgs - clientID string - clientSecret string - file string - truncate int + clientID string + clientSecret string + file string + truncate int + internalJWTDuration time.Duration } // Cmd returns base command @@ -68,6 +71,7 @@ func Cmd(rootArgs *shared.RootArgs, printf shared.FormatFn) *cobra.Command { c.AddCommand(cmdCreateToken(t, printf)) c.AddCommand(cmdInspectToken(t, printf)) c.AddCommand(cmdRotateCert(t, printf)) + c.AddCommand(cmdCreateInternalJWT(t, printf)) return c } @@ -162,6 +166,38 @@ func cmdRotateCert(t *token, printf shared.FormatFn) *cobra.Command { return c } +func cmdCreateInternalJWT(t *token, printf shared.FormatFn) *cobra.Command { + c := &cobra.Command{ + Use: "internal", + Short: "Create a JWT token for authorizing remote-service API calls (hybrid only)", + Long: "Create a JWT token for authorizing remote-service API calls (hybrid only)", + Args: cobra.NoArgs, + + RunE: func(cmd *cobra.Command, _ []string) error { + if !t.IsGCPManaged { + return fmt.Errorf("generating internal JWT only valid for hybrid") + } + if t.internalJWTDuration > 60*time.Minute { + return fmt.Errorf("JWT should not be valid for longer than 1 hour") + } + + token, err := t.createInternalJWT(printf) + if err != nil { + return errors.Wrap(err, "creating internal JWT") + } + printf(token) + return nil + }, + } + + // need to add this config flag here specifically to have it checked first + c.Flags().StringVarP(&t.ConfigPath, "config", "c", "", "Path to Apigee Remote Service config file") + c.Flags().DurationVarP(&t.internalJWTDuration, "duration", "", 10*time.Minute, "Valid time of the internal JWT from creation") + _ = c.MarkFlagRequired("config") + + return c +} + func (t *token) createToken(printf shared.FormatFn) (string, error) { tokenReq := &tokenRequest{ ClientID: t.clientID, @@ -191,6 +227,24 @@ func (t *token) createToken(printf shared.FormatFn) (string, error) { return tokenRes.Token, nil } +func (t *token) createInternalJWT(printf shared.FormatFn) (string, error) { + if t.ServerConfig == nil { + return "", fmt.Errorf("tenant not found. requires a valid config file") + } + token, err := server.NewToken(t.internalJWTDuration) + if err != nil { + return "", err + } + privateKey := t.ServerConfig.Tenant.PrivateKey + kid := t.ServerConfig.Tenant.PrivateKeyID + signed, err := server.SignJWT(token, jwa.RS256, privateKey, kid) + if err != nil { + return "", err + } + internalJWT := bytes.NewBuffer(signed).String() + return internalJWT, nil +} + func (t *token) inspectToken(in io.Reader, printf shared.FormatFn) error { var file = in if t.file != "" { diff --git a/cmd/token/token_test.go b/cmd/token/token_test.go index 9d4aaed..1027ce4 100644 --- a/cmd/token/token_test.go +++ b/cmd/token/token_test.go @@ -15,9 +15,13 @@ package token import ( + "bytes" "crypto/rand" "crypto/rsa" + "crypto/x509" + "encoding/base64" "encoding/json" + "encoding/pem" "fmt" "io" "io/ioutil" @@ -26,14 +30,17 @@ import ( "os" "strings" "testing" + "time" "github.com/apigee/apigee-remote-service-cli/cmd" "github.com/apigee/apigee-remote-service-cli/shared" "github.com/apigee/apigee-remote-service-cli/testutil" + "github.com/apigee/apigee-remote-service-envoy/server" "github.com/lestrrat-go/jwx/jwa" "github.com/lestrrat-go/jwx/jwk" "github.com/lestrrat-go/jwx/jwt" "github.com/spf13/cobra" + "gopkg.in/yaml.v3" ) func remoteServiceHandler(t *testing.T) http.Handler { @@ -410,3 +417,120 @@ func generateJWK(t *testing.T) (*rsa.PrivateKey, jwk.Key) { return privateKey, key } + +func TestCreateInternalJWT(t *testing.T) { + config := generateConfig(t) + + tmpFile, err := ioutil.TempFile("", "config.yaml") + if err != nil { + t.Fatalf("%v", err) + } + if _, err := tmpFile.Write(config); err != nil { + t.Fatalf("%v", err) + } + defer os.Remove(tmpFile.Name()) + + print := testutil.Printer("TestCreateInternalJWT") + + // a good command + rootArgs := &shared.RootArgs{} + flags := []string{"token", "internal", "--config", tmpFile.Name()} + rootCmd := cmd.GetRootCmd(flags, print.Printf) + shared.AddCommandWithFlags(rootCmd, rootArgs, Cmd(rootArgs, print.Printf)) + + if err := rootCmd.Execute(); err != nil { + t.Errorf("want no error: %v", err) + } + + if len(print.Prints) != 1 { + t.Errorf("want 1 output, got %d", len(print.Prints)) + } + + // missing config + rootArgs = &shared.RootArgs{} + flags = []string{"token", "internal", "-r", "dummy"} + rootCmd = cmd.GetRootCmd(flags, print.Printf) + shared.AddCommandWithFlags(rootCmd, rootArgs, Cmd(rootArgs, print.Printf)) + + err = rootCmd.Execute() + testutil.ErrorContains(t, err, "required flag(s) \"config\" not set") + + // duration too long + rootArgs = &shared.RootArgs{} + flags = []string{"token", "internal", "--config", tmpFile.Name(), "--duration", "80m"} + rootCmd = cmd.GetRootCmd(flags, print.Printf) + shared.AddCommandWithFlags(rootCmd, rootArgs, Cmd(rootArgs, print.Printf)) + + err = rootCmd.Execute() + testutil.ErrorContains(t, err, "JWT should not be valid for longer than 1 hour") +} + +func generateConfig(t *testing.T) []byte { + var yamlBuffer bytes.Buffer + yamlEncoder := yaml.NewEncoder(&yamlBuffer) + yamlEncoder.SetIndent(2) + + privateKey, key := generateJWK(t) + + config := server.DefaultConfig() + config.Tenant.RemoteServiceAPI = "https://RUNTIME/remote-service" + config.Tenant.OrgName = "hi" + config.Tenant.EnvName = "test" + config.Analytics.FluentdEndpoint = "apigee-udca-hi-test-1q2w3e4r.apigee:20001" + if err := yamlEncoder.Encode(config); err != nil { + t.Fatal(err) + } + configYAML := yamlBuffer.String() + data := map[string]string{"config.yaml": configYAML} + + configCRD := server.ConfigMapCRD{ + APIVersion: "v1", + Kind: "ConfigMap", + Metadata: server.Metadata{ + Name: "apigee-remote-service-envoy", + Namespace: "apigee", + }, + Data: data, + } + + yamlBuffer.Reset() + yamlEncoder = yaml.NewEncoder(&yamlBuffer) + yamlEncoder.SetIndent(2) + if err := yamlEncoder.Encode(configCRD); err != nil { + t.Fatal(err) + } + + privateKeyBytes := pem.EncodeToMemory(&pem.Block{Type: server.PEMKeyType, + Bytes: x509.MarshalPKCS1PrivateKey(privateKey)}) + jwksBytes, _ := json.Marshal(&jwk.Set{ + Keys: []jwk.Key{key}, + }) + props := map[string]string{server.SecretPropsKIDKey: time.Now().Format(time.RFC3339)} + propsBuf := new(bytes.Buffer) + if err := server.WriteProperties(propsBuf, props); err != nil { + t.Fatal(err) + } + + secretData := map[string]string{ + server.SecretJKWSKey: base64.StdEncoding.EncodeToString(jwksBytes), + server.SecretPrivateKey: base64.StdEncoding.EncodeToString(privateKeyBytes), + server.SecretPropsKey: base64.StdEncoding.EncodeToString(propsBuf.Bytes()), + } + + secretCRD := server.SecretCRD{ + APIVersion: "v1", + Kind: "Secret", + Type: "Opaque", + Metadata: server.Metadata{ + Name: "hi-test-policy-secret", + Namespace: "apigee", + }, + Data: secretData, + } + + if err := yamlEncoder.Encode(secretCRD); err != nil { + t.Fatal(err) + } + + return yamlBuffer.Bytes() +} diff --git a/go.mod b/go.mod index 5b6707b..aa4e78d 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ go 1.13 // replace github.com/apigee/apigee-remote-service-envoy => ../apigee-remote-service-envoy require ( - github.com/apigee/apigee-remote-service-envoy v1.0.0-pre.1 + github.com/apigee/apigee-remote-service-envoy v1.0.0-pre.1.0.20200729234108-088a461ba862 github.com/apigee/apigee-remote-service-golib v1.0.0-pre.1 github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d github.com/lestrrat-go/jwx v1.0.3 diff --git a/go.sum b/go.sum index 7b9eac9..54d4d2b 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 h1:Hs82Z41s6SdL1CELW+XaDYmOH4hkBN4/N9og/AsOv7E= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/apigee/apigee-remote-service-envoy v1.0.0-pre.1 h1:Adu3rYTjZhsiABkmJA+bTUCfGCvfMOR+7Mc0ubKY8g8= -github.com/apigee/apigee-remote-service-envoy v1.0.0-pre.1/go.mod h1:WAL8U/tH+W1QaXtWFuQkdHg/G/REDp2Oe71/dc9ai/I= +github.com/apigee/apigee-remote-service-envoy v1.0.0-pre.1.0.20200729234108-088a461ba862 h1:6OWBKSRUfx9keh1GOawWQktqiNlJRw999Jgbz/gDPrM= +github.com/apigee/apigee-remote-service-envoy v1.0.0-pre.1.0.20200729234108-088a461ba862/go.mod h1:WAL8U/tH+W1QaXtWFuQkdHg/G/REDp2Oe71/dc9ai/I= github.com/apigee/apigee-remote-service-golib v1.0.0-pre.1 h1:VVkN5tIqewCJs7hOdhdyxRBjsXIywMCzs4ChMrbkD8I= github.com/apigee/apigee-remote-service-golib v1.0.0-pre.1/go.mod h1:C8zgor6NPXg1e8pVdOxAM3N3w4QmxLcY9lnLJl/VOd8= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= @@ -97,6 +97,7 @@ github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= @@ -207,6 +208,7 @@ go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKY go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.15.0 h1:ZZCA22JRF2gQE5FoNmhmrf7jeJJ2uhqDUNRYKm8dvmM= go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=