You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@servicecomb.apache.org by ti...@apache.org on 2019/12/13 08:29:45 UTC

[servicecomb-mesher] branch master updated: add oauth2 authorization module (#91)

This is an automated email from the ASF dual-hosted git repository.

tianxiaoliang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/servicecomb-mesher.git


The following commit(s) were added to refs/heads/master by this push:
     new b89c769  add oauth2 authorization module (#91)
b89c769 is described below

commit b89c7696bdac0df8bab00b0b14c15ebe6285fd23
Author: Cong Young <53...@users.noreply.github.com>
AuthorDate: Fri Dec 13 16:29:36 2019 +0800

    add oauth2 authorization module (#91)
---
 cmd/mesher/mesher.go                        |   2 +
 docs/oauth2/oauth2.md                       |  54 +++++++++++
 examples/edge/conf/chassis.yaml             |   2 +-
 go.mod                                      |   1 +
 proxy/handler/oauth2/api.go                 |  39 ++++++++
 proxy/handler/oauth2/oauth2_handler.go      | 137 ++++++++++++++++++++++++++++
 proxy/handler/oauth2/oauth2_handler_test.go | 117 ++++++++++++++++++++++++
 proxy/protocol/http/gateway.go              |   1 +
 8 files changed, 352 insertions(+), 1 deletion(-)

diff --git a/cmd/mesher/mesher.go b/cmd/mesher/mesher.go
index 17e9c31..d8c3369 100644
--- a/cmd/mesher/mesher.go
+++ b/cmd/mesher/mesher.go
@@ -39,6 +39,8 @@ import (
 	_ "github.com/apache/servicecomb-mesher/proxy/pkg/egress/pilot"
 
 	_ "github.com/apache/servicecomb-mesher/proxy/control/istio"
+
+	_ "github.com/apache/servicecomb-mesher/proxy/handler/oauth2"
 )
 
 func main() {
diff --git a/docs/oauth2/oauth2.md b/docs/oauth2/oauth2.md
new file mode 100644
index 0000000..0e82f02
--- /dev/null
+++ b/docs/oauth2/oauth2.md
@@ -0,0 +1,54 @@
+# OAuth2
+
+Mesher provides a high-level general-purpose middleware abstraction layer. One of the abstractions is oauth2, which is free  user learning [complexity inside handler chain](https://docs.go-chassis.com/dev-guides/how-to-implement-handler.html), so that users only need to focus on the development of their own business.
+
+## configuration
+
+Writing business code
+
+When you use authorization code model, you need to implement the follow parameters. Otherwise you need to implement the interface of config in oauth2/api.go.
+
+For example, implement the authorization code model **In oauth2_handler.go**
+
+```go
+    Use(&OAuth2{
+		GrantType: "authorization_code",       // Registration grand type
+		                                       // The default is the authorization code model
+		Authenticate: func(accessToken string, req *http.Request) error {
+            // implement the function 
+			return nil
+		},
+		UseConfig: &oauth2.Config{
+			ClientID:     "",                        // (required, string) your client_ID
+			ClientSecret: "",                        // (required, string) your client_Secret
+			Scopes:       []string{""},              // (optional, string) scope specifies requested permissions
+			RedirectURL:  "",                        // (required, string) URL to redirect users going through the OAuth2 flow, 
+			Endpoint: oauth2.Endpoint{               // (required, string) your auth server endpoint
+				AuthURL:  "",
+				TokenURL: "",
+			},
+		},
+	})
+```
+
+Change the configuration file and add the oauth2 handler to the chain. Note that as authentication, generally speaking,it is a server function, it must be placed in the provider chain.
+
+```yaml
+handler:
+    chain:
+      Consumer:
+        outgoing: 
+      Provider:
+        incoming: oauth2 #provider handlers
+```
+
+## How to use
+
+**oauth2-handler Init**
+- [1] Implement the interface definition in /oauth2/api.go.
+- [2] Adding oauth2's provider handler name oauth2 defined in /oauth2 to providerChain.
+- [3] You must import proxy/handler/oauth2 to init oauth2 handler. All the handlers which are customized for mesher are defined in file cmd/mesher/mesher.go.
+- more details about handler chains in [go-chassis](https://github.com/go-chassis/go-chassis#readme)
+
+
+
diff --git a/examples/edge/conf/chassis.yaml b/examples/edge/conf/chassis.yaml
index de8325b..214bcf6 100644
--- a/examples/edge/conf/chassis.yaml
+++ b/examples/edge/conf/chassis.yaml
@@ -20,7 +20,7 @@ cse:
       Consumer:
         outgoing: router,bizkeeper-consumer,loadbalance,tracing-consumer,transport #consumer handlers
       Provider:
-        incoming: tracing-provider #provider handlers
+        incoming: oauth2,tracing-provider #provider handlers
 
 ## Mesher TLS is base on Go Chassis TLS config,  https://docs.go-chassis.com/user-guides/tls.html
 ssl:
diff --git a/go.mod b/go.mod
index d21e1cb..00ab1a4 100644
--- a/go.mod
+++ b/go.mod
@@ -23,6 +23,7 @@ require (
 	github.com/tetratelabs/go2sky v0.1.1-0.20190703154722-1eaab8035277
 	github.com/urfave/cli v1.20.1-0.20181029213200-b67dcf995b6a
 	golang.org/x/net v0.0.0-20190311183353-d8887717615a
+	golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be
 	golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 // indirect
 	google.golang.org/grpc v1.19.1
 	gopkg.in/inf.v0 v0.9.1 // indirect
diff --git a/proxy/handler/oauth2/api.go b/proxy/handler/oauth2/api.go
new file mode 100644
index 0000000..7c774a3
--- /dev/null
+++ b/proxy/handler/oauth2/api.go
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package oauth2
+
+import (
+	"golang.org/x/oauth2"
+	"net/http"
+)
+
+var auth *OAuth2
+
+// OAuth2 should implement oauth2 server side logic
+// it is singleton
+type OAuth2 struct {
+	GrantType    string                                            // required
+	UseConfig    *oauth2.Config                                    // required
+	Authenticate func(accessToken string, req *http.Request) error // optional
+}
+
+// Use put a custom oauth2 logic
+// then register handler to chassis
+func Use(middleware *OAuth2) {
+	auth = middleware
+}
diff --git a/proxy/handler/oauth2/oauth2_handler.go b/proxy/handler/oauth2/oauth2_handler.go
new file mode 100644
index 0000000..ed50d1e
--- /dev/null
+++ b/proxy/handler/oauth2/oauth2_handler.go
@@ -0,0 +1,137 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package oauth2
+
+import (
+	"context"
+	"errors"
+	"github.com/go-chassis/go-chassis/core/handler"
+	"github.com/go-chassis/go-chassis/core/invocation"
+	"github.com/go-mesh/openlogging"
+	"net/http"
+	"time"
+)
+
+// errors
+var (
+	ErrNoGrandType  = errors.New("no grant_type found")
+	ErrInvalidCode  = errors.New("invalid code")
+	ErrInvalidToken = errors.New("invalid authorization")
+	ErrInvalidAuth  = errors.New("invalid authentication")
+	ErrExpiredToken = errors.New("expired token")
+)
+
+// AuthName is a constant
+const AuthName = "oauth2"
+
+// Handler is is a oauth2 pre process raw data in handler
+type Handler struct {
+}
+
+// Handle is provider
+func (oa *Handler) Handle(chain *handler.Chain, inv *invocation.Invocation, cb invocation.ResponseCallBack) {
+	if req, ok := inv.Args.(*http.Request); ok {
+		grantType := req.FormValue("grant_type")
+		if grantType == "" {
+			WriteBackErr(ErrNoGrandType, http.StatusUnauthorized, cb)
+			return
+		}
+
+		if auth != nil && auth.GrantType == "authorization_code" {
+			if req, ok := inv.Args.(*http.Request); ok {
+				code := req.FormValue("code")
+				if code == "" {
+					WriteBackErr(ErrInvalidCode, http.StatusUnauthorized, cb)
+					return
+				}
+
+				accessToken, err := getToken(code, cb)
+				if err != nil {
+					openlogging.Error("authorization error: " + err.Error())
+					WriteBackErr(ErrInvalidToken, http.StatusUnauthorized, cb)
+					return
+				}
+
+				if auth.Authenticate != nil {
+					err = auth.Authenticate(accessToken, req)
+					if err != nil {
+						openlogging.Error("authentication error: " + err.Error())
+						WriteBackErr(ErrInvalidAuth, http.StatusUnauthorized, cb)
+						return
+					}
+				}
+			}
+		}
+		chain.Next(inv, func(r *invocation.Response) error {
+			return cb(r)
+		})
+	}
+}
+
+// getToken deal with the authorization code and return the token
+func getToken(code string, cb invocation.ResponseCallBack) (accessToken string, err error) {
+	if auth.UseConfig != nil {
+		config := auth.UseConfig
+		token, err := config.Exchange(context.Background(), code)
+		if err != nil {
+			openlogging.Error("get token failed, errors: " + err.Error())
+			WriteBackErr(ErrInvalidCode, http.StatusUnauthorized, cb)
+			return "", err
+		}
+
+		// set the expiry token in 30 minutes
+		token.Expiry = time.Now().Add(30 * 60 * time.Second)
+		if time.Now().After(token.Expiry) {
+			return "", ErrExpiredToken
+		}
+		accessToken = token.AccessToken
+		return accessToken, nil
+	}
+	return "", nil
+}
+
+// Name returns router string
+func (oa *Handler) Name() string {
+	return AuthName
+}
+
+// NewOAuth2 returns new auth handler
+func NewOAuth2() handler.Handler {
+	return &Handler{}
+}
+
+func init() {
+	err := handler.RegisterHandler(AuthName, NewOAuth2)
+	if err != nil {
+		openlogging.Error("register handler error: " + err.Error())
+		return
+	}
+}
+
+// WriteBackErr write err and callback
+func WriteBackErr(err error, status int, cb invocation.ResponseCallBack) {
+	r := &invocation.Response{
+		Err:    err,
+		Status: status,
+	}
+	err = cb(r)
+	if err != nil {
+		openlogging.Error("response error: " + err.Error())
+		return
+	}
+}
diff --git a/proxy/handler/oauth2/oauth2_handler_test.go b/proxy/handler/oauth2/oauth2_handler_test.go
new file mode 100644
index 0000000..ba5df52
--- /dev/null
+++ b/proxy/handler/oauth2/oauth2_handler_test.go
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package oauth2
+
+import (
+	"github.com/go-chassis/go-chassis/core/config"
+	"github.com/go-chassis/go-chassis/core/config/model"
+	"github.com/go-chassis/go-chassis/core/handler"
+	"github.com/go-chassis/go-chassis/core/invocation"
+	"github.com/stretchr/testify/assert"
+	"golang.org/x/oauth2"
+	"net/http"
+	"testing"
+)
+
+func initHandler() handler.Chain {
+	c := handler.Chain{}
+	c.AddHandler(&Handler{})
+	return c
+}
+
+func initInv() *invocation.Invocation {
+
+	config.GlobalDefinition = &model.GlobalCfg{}
+	config.GlobalDefinition.Cse.Handler.Chain.Provider = make(map[string]string)
+	config.GlobalDefinition.Cse.Handler.Chain.Provider["outgoing"] = AuthName
+
+	var i *invocation.Invocation
+
+	i = invocation.New(nil)
+	i.MicroServiceName = "service1"
+	i.SchemaID = "schema1"
+	i.OperationID = "SayHello"
+	i.Endpoint = ""
+
+	return i
+}
+func TestOAuth2_Handle(t *testing.T) {
+	c := initHandler()
+	i := initInv()
+
+	Use(&OAuth2{
+		GrantType: "authorization_code",
+		Authenticate: func(at string, req *http.Request) error {
+			return nil
+		},
+		UseConfig: &oauth2.Config{
+			ClientID:     "",           // (required, string) your client_ID
+			ClientSecret: "",           // (required, string) your client_Secret
+			Scopes:       []string{""}, // (optional, string) scope specifies requested permissions
+			RedirectURL:  "",           // (required, string) URL to redirect users going through the OAuth2 flow
+			Endpoint: oauth2.Endpoint{ // (required, string) your auth server endpoint
+				AuthURL:  "",
+				TokenURL: "",
+			},
+		},
+	})
+
+	t.Run("Invalid grant_type", func(t *testing.T) {
+		req, err := http.NewRequest(http.MethodPost, "https://api/?grant_type=test&code=test", nil)
+		if err != nil {
+			t.Errorf("authorization failed: %s", err.Error())
+			return
+		}
+		i.Args = req
+
+		i.SetHeader("Authorization", "Basic dGVzdDp0ZXN0")
+		c.Next(i, func(r *invocation.Response) error {
+			assert.Error(t, r.Err)
+			return r.Err
+		})
+	})
+
+	t.Run("Normal grant_type", func(t *testing.T) {
+		req, err := http.NewRequest(http.MethodPost, "https://api/?grant_type=authorization_code&code=test", nil)
+		if err != nil {
+			t.Errorf("authorization failed: %s", err.Error())
+			return
+		}
+		i.Args = req
+
+		c.Next(i, func(r *invocation.Response) error {
+			assert.NoError(t, r.Err)
+			return r.Err
+		})
+	})
+
+	t.Run("Null grant_type", func(t *testing.T) {
+		req, err := http.NewRequest(http.MethodPost, "https://api/?grant_type=&code=test", nil)
+		if err != nil {
+			t.Errorf("authorization failed: %s", err.Error())
+			return
+		}
+		i.Args = req
+
+		i.SetHeader("Authorization", "Basic dGVzdDp0ZXN0")
+		c.Next(i, func(r *invocation.Response) error {
+			assert.NoError(t, r.Err)
+			return r.Err
+		})
+	})
+}
diff --git a/proxy/protocol/http/gateway.go b/proxy/protocol/http/gateway.go
index ff8f173..d89dd4d 100644
--- a/proxy/protocol/http/gateway.go
+++ b/proxy/protocol/http/gateway.go
@@ -50,6 +50,7 @@ func HandleIngressTraffic(w http.ResponseWriter, r *http.Request) {
 	inv := &invocation.Invocation{}
 	inv.Reply = rest.NewResponse()
 	inv.Protocol = "rest"
+	inv.Args = r
 	h := make(map[string]string)
 	for k := range r.Header {
 		h[k] = r.Header.Get(k)