Skip to content

Commit

Permalink
Jwt proposal (#660)
Browse files Browse the repository at this point in the history
* [WIP] Add JWT Auth

* [WIP] Add jwt generation command

* Added jwt_key update

* Added jwt_key to swagger.yml

* Applied JWT auth to fn call

* Added a example of JWT auth

* Set NotBefore field of StandardClaims for avoid “Token used before issued” error

* update readme

* Fixed flag param name

* Fixed README & updated dependencies

* Extract jwt related functions into common package
  • Loading branch information
kunihiko-t authored and c0ze committed Nov 9, 2017
1 parent e1c0012 commit 4b2d82a
Show file tree
Hide file tree
Showing 11 changed files with 251 additions and 6 deletions.
6 changes: 3 additions & 3 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@ 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/routes.md).

### Calling your Function
Expand Down
5 changes: 5 additions & 0 deletions api/models/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type Route struct {
Timeout int32 `json:"timeout"`
IdleTimeout int32 `json:"idle_timeout"`
Config `json:"config"`
JwtKey string `json:"jwt_key"`
}

var (
Expand Down Expand Up @@ -191,6 +192,10 @@ func (r *Route) Update(new *Route) {
if new.MaxConcurrency != 0 {
r.MaxConcurrency = new.MaxConcurrency
}
if new.JwtKey != "" {
r.JwtKey = new.JwtKey
}

if new.Headers != nil {
if r.Headers == nil {
r.Headers = make(http.Header)
Expand Down
8 changes: 8 additions & 0 deletions api/server/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/iron-io/functions/api/models"
"github.com/iron-io/functions/api/runner"
"github.com/iron-io/functions/api/runner/task"
f_common "github.com/iron-io/functions/common"
"github.com/iron-io/runner/common"
uuid "github.com/satori/go.uuid"
)
Expand Down Expand Up @@ -128,6 +129,13 @@ func (s *Server) handleRequest(c *gin.Context, enqueue models.Enqueue) {
route := routes[0]
log = log.WithFields(logrus.Fields{"app": appName, "path": route.Path, "image": route.Image})

if err = f_common.AuthJwt(route.JwtKey, c.Request); err != nil {
log.WithError(err).Error("JWT Authentication Failed")
c.Writer.Header().Set("WWW-Authenticate", "Bearer realm=\"\"")
c.JSON(http.StatusUnauthorized, simpleError(err))
return
}

if s.serve(ctx, c, appName, route, app, path, reqID, payload, enqueue) {
s.FireAfterDispatch(ctx, reqRoute)
return
Expand Down
49 changes: 49 additions & 0 deletions common/jwt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package common

import (
"errors"
"net/http"
"time"

jwt "github.com/dgrijalva/jwt-go"
"github.com/dgrijalva/jwt-go/request"
)

func AuthJwt(signingKey string, req *http.Request) error {
if signingKey == "" {
return nil
}

extractor := request.AuthorizationHeaderExtractor
tokenString, err := extractor.ExtractToken(req)
if err != nil {
return err
}

token, err := jwt.ParseWithClaims(tokenString, &jwt.StandardClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(signingKey), nil
})

if err != nil {
return err
}

if _, ok := token.Claims.(*jwt.StandardClaims); ok && token.Valid {
return nil
}

return errors.New("Invalid token")

}

func GetJwt(signingKey string, expiration int) (string, error) {
now := time.Now().Unix()
claims := &jwt.StandardClaims{
ExpiresAt: time.Unix(now, 0).Add(time.Duration(expiration) * time.Second).Unix(),
IssuedAt: now,
NotBefore: time.Unix(now, 0).Add(time.Duration(-1) * time.Minute).Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
ss, err := token.SignedString([]byte(signingKey))
return ss, err
}
3 changes: 3 additions & 0 deletions docs/swagger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,9 @@ definitions:
type: integer
default: 30
description: Hot functions idle timeout before termination. Value in Seconds
jwt_key:
description: Signing key for JWT
type: string

App:
type: object
Expand Down
58 changes: 58 additions & 0 deletions examples/jwt/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Quick Example for JWT Authentication

This example will show you how to test and deploy a function with JWT Authentication.

```sh
# create your func.yaml file
fn init <YOUR_DOCKERHUB_USERNAME>/<REPO NAME>

# Add
# jwt_key: <Your JWT signing key>
# to your func.yml

# build the function
fn build
# test it
fn run
# push it to Docker Hub
fn push
# Create a route to this function on IronFunctions
fn routes create myapp /jwt


```

If you are going to add jwt authentication to an existing function,
you can simply add `jwt_key` to your func.yml, and update your route
using fn tool update command.

Now you can call your function on IronFunctions:

```sh
# Get token for authentication
fn routes token myapp /jwt
# The token expiration time is 1 hour by default. You can also specify the expiration time explicitly.
# Below example set the token expiration time at 500 seconds :
fn routes token myapp /jwt 500

# The response will include a token :
# {
# "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MDgwNTcwNTEsImlhdCI6MTUwODA1MzQ1MX0.3c_xUaleCdHy_fdU9zFB50j3hqwYWgPZ-EkTXV3VWag"
# }

# Now, you can access your app with a token :
curl -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MDgwNTcwNTEsImlhdCI6MTUwODA1MzQ1MX0.3c_xUaleCdHy_fdU9zFB50j3hqwYWgPZ-EkTXV3VWag' http://localhost:8080/r/myapp/jwt

# or use fn tool
# This will automatically generate a token and make function call :
fn routes call myapp /jwt

```

__important__: Please note that enabling Jwt authentication will require you to authenticate each time you try to call your function.
You won't be able to call your function without a token.

## Dependencies

Be sure your dependencies are in the `vendor/` directory.

17 changes: 17 additions & 0 deletions examples/jwt/func.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package main

import (
"encoding/json"
"fmt"
"os"
)

type Person struct {
Name string
}

func main() {
p := &Person{Name: "World"}
json.NewDecoder(os.Stdin).Decode(p)
fmt.Printf("Hello %v!", p.Name)
}
104 changes: 102 additions & 2 deletions fn/commands/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import (
"net/url"
"os"
"path"
"strconv"
"strings"
"text/tabwriter"

f_common "github.com/iron-io/functions/common"
image_commands "github.com/iron-io/functions/fn/commands/images"
"github.com/iron-io/functions/fn/common"
fnclient "github.com/iron-io/functions_go/client"
Expand Down Expand Up @@ -56,6 +58,10 @@ var routeFlags = []cli.Flag{
Name: "max-concurrency,mc",
Usage: "maximum concurrency for hot container",
},
cli.StringFlag{
Name: "jwt-key,j",
Usage: "Signing key for JWT",
},
cli.DurationFlag{
Name: "timeout",
Usage: "route timeout (eg. 30s)",
Expand Down Expand Up @@ -134,6 +140,13 @@ func Routes() cli.Command {
ArgsUsage: "<app> </path> [property.[key]]",
Action: r.inspect,
},
{
Name: "token",
Aliases: []string{"t"},
Usage: "retrieve jwt for authentication",
ArgsUsage: "<app> </path> [expiration(sec)]",
Action: r.token,
},
},
}
}
Expand Down Expand Up @@ -203,10 +216,28 @@ func (a *routesCmd) call(c *cli.Context) error {
u.Path = path.Join(u.Path, "r", appName, route)
content := image_commands.Stdin()

return callfn(u.String(), content, os.Stdout, c.String("method"), c.StringSlice("e"))
resp, err := a.client.Routes.GetAppsAppRoutesRoute(&apiroutes.GetAppsAppRoutesRouteParams{
Context: context.Background(),
App: appName,
Route: route,
})

if err != nil {
switch err.(type) {
case *apiroutes.GetAppsAppRoutesRouteNotFound:
return fmt.Errorf("error: %s", err.(*apiroutes.GetAppsAppRoutesRouteNotFound).Payload.Error.Message)
case *apiroutes.GetAppsAppRoutesRouteDefault:
return fmt.Errorf("unexpected error: %s", err.(*apiroutes.GetAppsAppRoutesRouteDefault).Payload.Error.Message)
}
return fmt.Errorf("unexpected error: %s", err)
}

rt := resp.Payload.Route

return callfn(u.String(), rt, content, os.Stdout, c.String("method"), c.StringSlice("e"))
}

func callfn(u string, content io.Reader, output io.Writer, method string, env []string) error {
func callfn(u string, rt *models.Route, content io.Reader, output io.Writer, method string, env []string) error {
if method == "" {
if content == nil {
method = "GET"
Expand All @@ -226,6 +257,14 @@ func callfn(u string, content io.Reader, output io.Writer, method string, env []
envAsHeader(req, env)
}

if rt.JwtKey != "" {
ss, err := f_common.GetJwt(rt.JwtKey, 60*60)
if err != nil {
return fmt.Errorf("unexpected error: %s", err)
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", ss))
}

resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("error running route: %s", err)
Expand Down Expand Up @@ -275,6 +314,10 @@ func routeWithFlags(c *cli.Context, rt *models.Route) {
rt.Timeout = &to
}

if j := c.String("jwt-key"); j != "" {
rt.JwtKey = j
}

if len(c.StringSlice("headers")) > 0 {
headers := map[string][]string{}
for _, header := range c.StringSlice("headers") {
Expand Down Expand Up @@ -305,6 +348,10 @@ func routeWithFuncFile(c *cli.Context, rt *models.Route) {
to := int64(ff.Timeout.Seconds())
rt.Timeout = &to
}
if ff.JwtKey != nil && *ff.JwtKey != "" {
rt.JwtKey = *ff.JwtKey
}

if rt.Path == "" && ff.Path != nil {
rt.Path = *ff.Path
}
Expand Down Expand Up @@ -419,6 +466,10 @@ func (a *routesCmd) patchRoute(appName, routePath string, r *fnmodels.Route) err
if r.Timeout != nil {
resp.Payload.Route.Timeout = r.Timeout
}
if r.JwtKey != "" {
resp.Payload.Route.JwtKey = r.JwtKey
}

}

_, err = a.client.Routes.PatchAppsAppRoutesRoute(&apiroutes.PatchAppsAppRoutesRouteParams{
Expand Down Expand Up @@ -572,3 +623,52 @@ func (a *routesCmd) delete(c *cli.Context) error {
fmt.Println(appName, route, "deleted")
return nil
}

func (a *routesCmd) token(c *cli.Context) error {
appName := c.Args().Get(0)
route := cleanRoutePath(c.Args().Get(1))
e := c.Args().Get(2)
expiration := 60 * 60
if e != "" {
var err error
expiration, err = strconv.Atoi(e)
if err != nil {
return fmt.Errorf("invalid expiration: %s", err)
}
}

resp, err := a.client.Routes.GetAppsAppRoutesRoute(&apiroutes.GetAppsAppRoutesRouteParams{
Context: context.Background(),
App: appName,
Route: route,
})

if err != nil {
switch err.(type) {
case *apiroutes.GetAppsAppRoutesRouteNotFound:
return fmt.Errorf("error: %s", err.(*apiroutes.GetAppsAppRoutesRouteNotFound).Payload.Error.Message)
case *apiroutes.GetAppsAppRoutesRouteDefault:
return fmt.Errorf("unexpected error: %s", err.(*apiroutes.GetAppsAppRoutesRouteDefault).Payload.Error.Message)
}
return fmt.Errorf("unexpected error: %s", err)
}

enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", "\t")
jwtKey := resp.Payload.Route.JwtKey
if jwtKey == "" {
return errors.New("Empty JWT Key")
}

// Create the Claims
ss, err := f_common.GetJwt(jwtKey, expiration)
if err != nil {
return fmt.Errorf("unexpected error: %s", err)
}
t := struct {
Token string `json:"token"`
}{Token: ss}
enc.Encode(t)

return nil
}
4 changes: 3 additions & 1 deletion fn/commands/testfn.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"bytes"
"errors"
"fmt"
"github.com/iron-io/functions_go/models"
"net/url"
"os"
"path"
Expand Down Expand Up @@ -174,7 +175,8 @@ func runremotetest(target string, in, expectedOut, expectedErr *string, env map[
os.Setenv(k, v)
restrictedEnv = append(restrictedEnv, k)
}
if err := callfn(target, stdin, &stdout, "", restrictedEnv); err != nil {
dummyRoute := &models.Route{}
if err := callfn(target, dummyRoute, stdin, &stdout, "", restrictedEnv); err != nil {
return fmt.Errorf("%v\nstdout:%s\n", err, stdout.String())
}

Expand Down
Loading

0 comments on commit 4b2d82a

Please sign in to comment.