Skip to content

Commit

Permalink
Add jwtauth middleware (#662)
Browse files Browse the repository at this point in the history
* add middleware for jwt auth

* fill in jwt auth functions

* refactor and add tests

* use viper to get jwt auth key env

* remove iron token

* remove unused import

* Add auth tests for invalid tokens

* refactor auth tests to own file

* add tags to full stack tests

* initial integration test

* complete test tags

* make make file accept tags

* refactor fn to make easier testing

* update integration tests

* add make target for testing individual tags

* update integration tests

* add integration tests to circle ci

* fix integration tests

* add auth integration tests

* set default values at env so that they are always initialized

* add tests for create / delete route

* add jwt route test and route call test

* run server tests during test phase

* add checks to confirm route being created and deleted

* update documentation

* cleanup test files after test
  • Loading branch information
c0ze authored Jan 15, 2018
1 parent c3d25a9 commit 6b5bb41
Show file tree
Hide file tree
Showing 19 changed files with 530 additions and 174 deletions.
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ test:
go test -v $(shell go list ./... | grep -v vendor | grep -v examples | grep -v tool | grep -v fn)
cd fn && $(MAKE) test

test-tag:
go test -v $(shell go list ./... | grep -v vendor | grep -v examples | grep -v tool | grep -v fn) -tags=$(TAG)

test-datastore:
cd api/datastore && go test -v ./...

Expand Down
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,15 @@ curl -H "Content-Type: application/json" -X POST -d '{
}' http://localhost:8080/v1/apps/myapp/routes
```

You can use JWT for [authentication](examples/jwt).
[More on routes](docs/operating/routes.md).

[More on routes](docs/routes.md).
### Authentication

Iron Functions API supports two levels of Authentication in two seperate scopes, service level authentication,
(Which authenticates all requests made to the server from any client) and route level authentication.
Route level authentication is applied whenever a function call made to a specific route.

Please check [Authentication](docs/authentication.md) documentation for more information.

### Calling your Function

Expand Down
2 changes: 1 addition & 1 deletion api/datastore/bolt/bolt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
const tmpBolt = "/tmp/func_test_bolt.db"

func TestDatastore(t *testing.T) {
os.Remove(tmpBolt)
u, err := url.Parse("bolt://" + tmpBolt)
if err != nil {
t.Fatalf("failed to parse url:", err)
Expand All @@ -21,4 +20,5 @@ func TestDatastore(t *testing.T) {
t.Fatalf("failed to create bolt datastore:", err)
}
datastoretest.Test(t, ds)
os.Remove(tmpBolt)
}
2 changes: 2 additions & 0 deletions api/server/apps_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// +build server

package server

import (
Expand Down
148 changes: 148 additions & 0 deletions api/server/integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// +build integration

package server

import (
"context"
"fmt"
"os"
"testing"
"time"

"github.com/iron-io/functions/api/models"
"github.com/iron-io/functions/fn/app"
"github.com/spf13/viper"
"github.com/urfave/cli"
)

var DB_FILE string
var MQ_FILE string
var API_URL string
var PORT int
var funcServer *Server
var Cancel context.CancelFunc
var Ctx context.Context
var fn *cli.App

func setupServer() {
viper.Set(EnvDBURL, fmt.Sprintf("bolt://%s?bucket=funcs", DB_FILE))
viper.Set(EnvMQURL, fmt.Sprintf("bolt://%s", MQ_FILE))
viper.Set(EnvPort, PORT)
Ctx, Cancel = context.WithCancel(context.Background())
funcServer = NewFromEnv(Ctx)
go funcServer.Start(Ctx)
time.Sleep(2 * time.Second)
}

func setupCli() {
viper.Set("API_URL", API_URL)
fn = app.NewFn()
}

func teardown() {
os.Remove(DB_FILE)
os.Remove(MQ_FILE)
Cancel()
time.Sleep(2 * time.Second)
}

func TestIntegration(t *testing.T) {
DB_FILE = "/tmp/bolt.db"
MQ_FILE = "/tmp/bolt_mq.db"
PORT = 8080
API_URL = "http://localhost:8080"
setupServer()
setupCli()
testIntegration(t)
teardown()
}

func TestIntegrationWithAuth(t *testing.T) {
viper.Set("jwt_auth_key", "test")
DB_FILE = "/tmp/bolt_auth.db"
MQ_FILE = "/tmp/bolt_auth_mq.db"
PORT = 8081
API_URL = "http://localhost:8081"
setupServer()
setupCli()
testIntegration(t)
teardown()
}

func testIntegration(t *testing.T) {
// Test list

err := fn.Run([]string{"fn", "apps", "l"})
if err != nil {
t.Error(err)
}

// Test create app

err = fn.Run([]string{"fn", "apps", "c", "test"})
if err != nil {
t.Error(err)
}

filter := &models.AppFilter{}
apps, err := funcServer.Datastore.GetApps(Ctx, filter)

if len(apps) != 1 {
t.Error("fn apps create failed.")
}

if apps[0].Name != "test" {
t.Error("fn apps create failed. - name doesnt match")
}

// Test create route

err = fn.Run([]string{"fn", "routes", "c", "test", "/new-route", "--jwt-key", "route_key"})
if err != nil {
t.Error(err)
}

routeFilter := &models.RouteFilter{}
routes, err := funcServer.Datastore.GetRoutes(Ctx, routeFilter)

if len(routes) != 1 {
t.Error("fn routes create failed.")
}

if routes[0].Path != "/new-route" {
t.Error("fn routes create failed. - path doesnt match")
}

// Test call route

err = fn.Run([]string{"fn", "routes", "call", "test", "/new-route"})
if err != nil {
t.Error(err)
}

// Test delete route

err = fn.Run([]string{"fn", "routes", "delete", "test", "/new-route"})
if err != nil {
t.Error(err)
}

routes, err = funcServer.Datastore.GetRoutes(Ctx, routeFilter)

if len(routes) != 0 {
t.Error("fn routes delete failed.")
}

// Test delete app

err = fn.Run([]string{"fn", "apps", "delete", "test"})
if err != nil {
t.Error(err)
}

apps, err = funcServer.Datastore.GetApps(Ctx, filter)

if len(apps) != 0 {
t.Error("fn apps delete failed.")
}
}
45 changes: 45 additions & 0 deletions api/server/middlewares.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package server

import (
"encoding/json"
"errors"
"fmt"
"net/http"
"time"

"github.com/iron-io/functions/api/models"
"github.com/iron-io/functions/common"
"github.com/spf13/viper"
)

func SetupJwtAuth(funcServer *Server) {
// Add default JWT AUTH if env variable set
if jwtAuthKey := viper.GetString("jwt_auth_key"); jwtAuthKey != "" {
funcServer.AddMiddlewareFunc(func(ctx MiddlewareContext, w http.ResponseWriter, r *http.Request, app *models.App) error {
start := time.Now()
fmt.Println("JwtAuthMiddlewareFunc called at:", start)
ctx.Next()
fmt.Println("Duration:", (time.Now().Sub(start)))
return nil
})
funcServer.AddMiddleware(&JwtAuthMiddleware{})
}
}

type JwtAuthMiddleware struct {
}

func (h *JwtAuthMiddleware) Serve(ctx MiddlewareContext, w http.ResponseWriter, r *http.Request, app *models.App) error {
fmt.Println("JwtAuthMiddleware called")
jwtAuthKey := viper.GetString("jwt_auth_key")

if err := common.AuthJwt(jwtAuthKey, r); err != nil {
w.WriteHeader(http.StatusUnauthorized)
m := map[string]string{"error": "Invalid API Authorization token."}
json.NewEncoder(w).Encode(m)
return errors.New("Invalid API authorization token.")
}

fmt.Println("auth succeeded!")
return nil
}
2 changes: 2 additions & 0 deletions api/server/routes_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// +build server

package server

import (
Expand Down
2 changes: 2 additions & 0 deletions api/server/runner_async_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// +build server

package server

import (
Expand Down
2 changes: 2 additions & 0 deletions api/server/runner_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// +build server

package server

import (
Expand Down
5 changes: 5 additions & 0 deletions api/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ func New(ctx context.Context, ds models.Datastore, mq models.MessageQueue, apiUR

s.Router.Use(prepareMiddleware(ctx))
s.bindHandlers(ctx)
s.setupMiddlewares()

for _, opt := range opts {
opt(s)
Expand Down Expand Up @@ -203,6 +204,10 @@ func (s *Server) Start(ctx context.Context) {
close(s.tasks)
}

func (s *Server) setupMiddlewares() {
SetupJwtAuth(s)
}

func (s *Server) startGears(ctx context.Context) {
// By default it serves on :8080 unless a
// PORT environment variable was defined.
Expand Down
82 changes: 82 additions & 0 deletions api/server/server_auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// +build server

package server

import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing"

"github.com/gin-gonic/gin"
"github.com/iron-io/functions/common"
"github.com/spf13/viper"
)

var UnAuthtestSuite = []struct {
name string
method string
path string
body string
expectedCode int
expectedCacheSize int
}{
{"create my app", "POST", "/v1/apps", `{ "app": { "name": "myapp" } }`, http.StatusUnauthorized, 0},
{"list apps", "GET", "/v1/apps", ``, http.StatusUnauthorized, 0},
{"get app", "GET", "/v1/apps/myapp", ``, http.StatusUnauthorized, 0},
{"add myroute", "POST", "/v1/apps/myapp/routes", `{ "route": { "name": "myroute", "path": "/myroute", "image": "iron/hello" } }`, http.StatusUnauthorized, 0},
{"add myroute2", "POST", "/v1/apps/myapp/routes", `{ "route": { "name": "myroute2", "path": "/myroute2", "image": "iron/error" } }`, http.StatusUnauthorized, 0},
{"get myroute", "GET", "/v1/apps/myapp/routes/myroute", ``, http.StatusUnauthorized, 0},
{"get myroute2", "GET", "/v1/apps/myapp/routes/myroute2", ``, http.StatusUnauthorized, 0},
{"get all routes", "GET", "/v1/apps/myapp/routes", ``, http.StatusUnauthorized, 0},
// These two are currently returning 404 because they dont get created : temporarily using StatusNotFound
// {"execute myroute", "POST", "/r/myapp/myroute", `{ "name": "Teste" }`, http.StatusUnauthorized, 0},
// {"execute myroute2", "POST", "/r/myapp/myroute2", `{ "name": "Teste" }`, http.StatusUnauthorized, 0},
{"execute myroute", "POST", "/r/myapp/myroute", `{ "name": "Teste" }`, http.StatusNotFound, 0},
{"execute myroute2", "POST", "/r/myapp/myroute2", `{ "name": "Teste" }`, http.StatusNotFound, 0},
{"delete myroute", "DELETE", "/v1/apps/myapp/routes/myroute", ``, http.StatusUnauthorized, 0},
{"delete app (fail)", "DELETE", "/v1/apps/myapp", ``, http.StatusUnauthorized, 0},
{"delete myroute2", "DELETE", "/v1/apps/myapp/routes/myroute2", ``, http.StatusUnauthorized, 0},
{"delete app (success)", "DELETE", "/v1/apps/myapp", ``, http.StatusUnauthorized, 0},
{"get deleted app", "GET", "/v1/apps/myapp", ``, http.StatusUnauthorized, 0},
{"get deleteds route on deleted app", "GET", "/v1/apps/myapp/routes/myroute", ``, http.StatusUnauthorized, 0},
}

func routerRequestWithAuth(t *testing.T, router *gin.Engine, method, path string, body io.Reader, setAuth func(*http.Request)) (*http.Request, *httptest.ResponseRecorder) {
req, err := http.NewRequest(method, "http://127.0.0.1:8080"+path, body)
setAuth(req)
if err != nil {
t.Fatalf("Test: Could not create %s request to %s: %v", method, path, err)
}

rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)

return req, rec
}

func setJwtAuth(req *http.Request) {
if jwtAuthKey := viper.GetString("jwt_auth_key"); jwtAuthKey != "" {
jwtToken, err := common.GetJwt(jwtAuthKey, 60*60)
if err == nil {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", jwtToken))
}
}
}

func setBrokenJwtAuth(req *http.Request) {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", "broken token"))
}

func TestFullStackWithAuth(t *testing.T) {
viper.Set("jwt_auth_key", "test")
testFullStack(t, setJwtAuth, testSuite)
teardown()
}

func TestFullStackWithBrokenAuth(t *testing.T) {
viper.Set("jwt_auth_key", "test")
testFullStack(t, setBrokenJwtAuth, UnAuthtestSuite)
teardown()
}
Loading

0 comments on commit 6b5bb41

Please sign in to comment.