Skip to content

Commit 4b2d82a

Browse files
kunihiko-tc0ze
authored andcommitted
Jwt proposal (#660)
* [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
1 parent e1c0012 commit 4b2d82a

File tree

11 files changed

+251
-6
lines changed

11 files changed

+251
-6
lines changed

Gopkg.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,8 @@ curl -H "Content-Type: application/json" -X POST -d '{
213213
}' http://localhost:8080/v1/apps/myapp/routes
214214
```
215215

216+
You can use JWT for [authentication](examples/jwt).
217+
216218
[More on routes](docs/routes.md).
217219

218220
### Calling your Function

api/models/route.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ type Route struct {
4242
Timeout int32 `json:"timeout"`
4343
IdleTimeout int32 `json:"idle_timeout"`
4444
Config `json:"config"`
45+
JwtKey string `json:"jwt_key"`
4546
}
4647

4748
var (
@@ -191,6 +192,10 @@ func (r *Route) Update(new *Route) {
191192
if new.MaxConcurrency != 0 {
192193
r.MaxConcurrency = new.MaxConcurrency
193194
}
195+
if new.JwtKey != "" {
196+
r.JwtKey = new.JwtKey
197+
}
198+
194199
if new.Headers != nil {
195200
if r.Headers == nil {
196201
r.Headers = make(http.Header)

api/server/runner.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/iron-io/functions/api/models"
1919
"github.com/iron-io/functions/api/runner"
2020
"github.com/iron-io/functions/api/runner/task"
21+
f_common "github.com/iron-io/functions/common"
2122
"github.com/iron-io/runner/common"
2223
uuid "github.com/satori/go.uuid"
2324
)
@@ -128,6 +129,13 @@ func (s *Server) handleRequest(c *gin.Context, enqueue models.Enqueue) {
128129
route := routes[0]
129130
log = log.WithFields(logrus.Fields{"app": appName, "path": route.Path, "image": route.Image})
130131

132+
if err = f_common.AuthJwt(route.JwtKey, c.Request); err != nil {
133+
log.WithError(err).Error("JWT Authentication Failed")
134+
c.Writer.Header().Set("WWW-Authenticate", "Bearer realm=\"\"")
135+
c.JSON(http.StatusUnauthorized, simpleError(err))
136+
return
137+
}
138+
131139
if s.serve(ctx, c, appName, route, app, path, reqID, payload, enqueue) {
132140
s.FireAfterDispatch(ctx, reqRoute)
133141
return

common/jwt.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package common
2+
3+
import (
4+
"errors"
5+
"net/http"
6+
"time"
7+
8+
jwt "github.com/dgrijalva/jwt-go"
9+
"github.com/dgrijalva/jwt-go/request"
10+
)
11+
12+
func AuthJwt(signingKey string, req *http.Request) error {
13+
if signingKey == "" {
14+
return nil
15+
}
16+
17+
extractor := request.AuthorizationHeaderExtractor
18+
tokenString, err := extractor.ExtractToken(req)
19+
if err != nil {
20+
return err
21+
}
22+
23+
token, err := jwt.ParseWithClaims(tokenString, &jwt.StandardClaims{}, func(token *jwt.Token) (interface{}, error) {
24+
return []byte(signingKey), nil
25+
})
26+
27+
if err != nil {
28+
return err
29+
}
30+
31+
if _, ok := token.Claims.(*jwt.StandardClaims); ok && token.Valid {
32+
return nil
33+
}
34+
35+
return errors.New("Invalid token")
36+
37+
}
38+
39+
func GetJwt(signingKey string, expiration int) (string, error) {
40+
now := time.Now().Unix()
41+
claims := &jwt.StandardClaims{
42+
ExpiresAt: time.Unix(now, 0).Add(time.Duration(expiration) * time.Second).Unix(),
43+
IssuedAt: now,
44+
NotBefore: time.Unix(now, 0).Add(time.Duration(-1) * time.Minute).Unix(),
45+
}
46+
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
47+
ss, err := token.SignedString([]byte(signingKey))
48+
return ss, err
49+
}

docs/swagger.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,9 @@ definitions:
384384
type: integer
385385
default: 30
386386
description: Hot functions idle timeout before termination. Value in Seconds
387+
jwt_key:
388+
description: Signing key for JWT
389+
type: string
387390

388391
App:
389392
type: object

examples/jwt/README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Quick Example for JWT Authentication
2+
3+
This example will show you how to test and deploy a function with JWT Authentication.
4+
5+
```sh
6+
# create your func.yaml file
7+
fn init <YOUR_DOCKERHUB_USERNAME>/<REPO NAME>
8+
9+
# Add
10+
# jwt_key: <Your JWT signing key>
11+
# to your func.yml
12+
13+
# build the function
14+
fn build
15+
# test it
16+
fn run
17+
# push it to Docker Hub
18+
fn push
19+
# Create a route to this function on IronFunctions
20+
fn routes create myapp /jwt
21+
22+
23+
```
24+
25+
If you are going to add jwt authentication to an existing function,
26+
you can simply add `jwt_key` to your func.yml, and update your route
27+
using fn tool update command.
28+
29+
Now you can call your function on IronFunctions:
30+
31+
```sh
32+
# Get token for authentication
33+
fn routes token myapp /jwt
34+
# The token expiration time is 1 hour by default. You can also specify the expiration time explicitly.
35+
# Below example set the token expiration time at 500 seconds :
36+
fn routes token myapp /jwt 500
37+
38+
# The response will include a token :
39+
# {
40+
# "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MDgwNTcwNTEsImlhdCI6MTUwODA1MzQ1MX0.3c_xUaleCdHy_fdU9zFB50j3hqwYWgPZ-EkTXV3VWag"
41+
# }
42+
43+
# Now, you can access your app with a token :
44+
curl -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MDgwNTcwNTEsImlhdCI6MTUwODA1MzQ1MX0.3c_xUaleCdHy_fdU9zFB50j3hqwYWgPZ-EkTXV3VWag' http://localhost:8080/r/myapp/jwt
45+
46+
# or use fn tool
47+
# This will automatically generate a token and make function call :
48+
fn routes call myapp /jwt
49+
50+
```
51+
52+
__important__: Please note that enabling Jwt authentication will require you to authenticate each time you try to call your function.
53+
You won't be able to call your function without a token.
54+
55+
## Dependencies
56+
57+
Be sure your dependencies are in the `vendor/` directory.
58+

examples/jwt/func.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
)
8+
9+
type Person struct {
10+
Name string
11+
}
12+
13+
func main() {
14+
p := &Person{Name: "World"}
15+
json.NewDecoder(os.Stdin).Decode(p)
16+
fmt.Printf("Hello %v!", p.Name)
17+
}

fn/commands/routes.go

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ import (
1010
"net/url"
1111
"os"
1212
"path"
13+
"strconv"
1314
"strings"
1415
"text/tabwriter"
1516

17+
f_common "github.com/iron-io/functions/common"
1618
image_commands "github.com/iron-io/functions/fn/commands/images"
1719
"github.com/iron-io/functions/fn/common"
1820
fnclient "github.com/iron-io/functions_go/client"
@@ -56,6 +58,10 @@ var routeFlags = []cli.Flag{
5658
Name: "max-concurrency,mc",
5759
Usage: "maximum concurrency for hot container",
5860
},
61+
cli.StringFlag{
62+
Name: "jwt-key,j",
63+
Usage: "Signing key for JWT",
64+
},
5965
cli.DurationFlag{
6066
Name: "timeout",
6167
Usage: "route timeout (eg. 30s)",
@@ -134,6 +140,13 @@ func Routes() cli.Command {
134140
ArgsUsage: "<app> </path> [property.[key]]",
135141
Action: r.inspect,
136142
},
143+
{
144+
Name: "token",
145+
Aliases: []string{"t"},
146+
Usage: "retrieve jwt for authentication",
147+
ArgsUsage: "<app> </path> [expiration(sec)]",
148+
Action: r.token,
149+
},
137150
},
138151
}
139152
}
@@ -203,10 +216,28 @@ func (a *routesCmd) call(c *cli.Context) error {
203216
u.Path = path.Join(u.Path, "r", appName, route)
204217
content := image_commands.Stdin()
205218

206-
return callfn(u.String(), content, os.Stdout, c.String("method"), c.StringSlice("e"))
219+
resp, err := a.client.Routes.GetAppsAppRoutesRoute(&apiroutes.GetAppsAppRoutesRouteParams{
220+
Context: context.Background(),
221+
App: appName,
222+
Route: route,
223+
})
224+
225+
if err != nil {
226+
switch err.(type) {
227+
case *apiroutes.GetAppsAppRoutesRouteNotFound:
228+
return fmt.Errorf("error: %s", err.(*apiroutes.GetAppsAppRoutesRouteNotFound).Payload.Error.Message)
229+
case *apiroutes.GetAppsAppRoutesRouteDefault:
230+
return fmt.Errorf("unexpected error: %s", err.(*apiroutes.GetAppsAppRoutesRouteDefault).Payload.Error.Message)
231+
}
232+
return fmt.Errorf("unexpected error: %s", err)
233+
}
234+
235+
rt := resp.Payload.Route
236+
237+
return callfn(u.String(), rt, content, os.Stdout, c.String("method"), c.StringSlice("e"))
207238
}
208239

209-
func callfn(u string, content io.Reader, output io.Writer, method string, env []string) error {
240+
func callfn(u string, rt *models.Route, content io.Reader, output io.Writer, method string, env []string) error {
210241
if method == "" {
211242
if content == nil {
212243
method = "GET"
@@ -226,6 +257,14 @@ func callfn(u string, content io.Reader, output io.Writer, method string, env []
226257
envAsHeader(req, env)
227258
}
228259

260+
if rt.JwtKey != "" {
261+
ss, err := f_common.GetJwt(rt.JwtKey, 60*60)
262+
if err != nil {
263+
return fmt.Errorf("unexpected error: %s", err)
264+
}
265+
req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", ss))
266+
}
267+
229268
resp, err := http.DefaultClient.Do(req)
230269
if err != nil {
231270
return fmt.Errorf("error running route: %s", err)
@@ -275,6 +314,10 @@ func routeWithFlags(c *cli.Context, rt *models.Route) {
275314
rt.Timeout = &to
276315
}
277316

317+
if j := c.String("jwt-key"); j != "" {
318+
rt.JwtKey = j
319+
}
320+
278321
if len(c.StringSlice("headers")) > 0 {
279322
headers := map[string][]string{}
280323
for _, header := range c.StringSlice("headers") {
@@ -305,6 +348,10 @@ func routeWithFuncFile(c *cli.Context, rt *models.Route) {
305348
to := int64(ff.Timeout.Seconds())
306349
rt.Timeout = &to
307350
}
351+
if ff.JwtKey != nil && *ff.JwtKey != "" {
352+
rt.JwtKey = *ff.JwtKey
353+
}
354+
308355
if rt.Path == "" && ff.Path != nil {
309356
rt.Path = *ff.Path
310357
}
@@ -419,6 +466,10 @@ func (a *routesCmd) patchRoute(appName, routePath string, r *fnmodels.Route) err
419466
if r.Timeout != nil {
420467
resp.Payload.Route.Timeout = r.Timeout
421468
}
469+
if r.JwtKey != "" {
470+
resp.Payload.Route.JwtKey = r.JwtKey
471+
}
472+
422473
}
423474

424475
_, err = a.client.Routes.PatchAppsAppRoutesRoute(&apiroutes.PatchAppsAppRoutesRouteParams{
@@ -572,3 +623,52 @@ func (a *routesCmd) delete(c *cli.Context) error {
572623
fmt.Println(appName, route, "deleted")
573624
return nil
574625
}
626+
627+
func (a *routesCmd) token(c *cli.Context) error {
628+
appName := c.Args().Get(0)
629+
route := cleanRoutePath(c.Args().Get(1))
630+
e := c.Args().Get(2)
631+
expiration := 60 * 60
632+
if e != "" {
633+
var err error
634+
expiration, err = strconv.Atoi(e)
635+
if err != nil {
636+
return fmt.Errorf("invalid expiration: %s", err)
637+
}
638+
}
639+
640+
resp, err := a.client.Routes.GetAppsAppRoutesRoute(&apiroutes.GetAppsAppRoutesRouteParams{
641+
Context: context.Background(),
642+
App: appName,
643+
Route: route,
644+
})
645+
646+
if err != nil {
647+
switch err.(type) {
648+
case *apiroutes.GetAppsAppRoutesRouteNotFound:
649+
return fmt.Errorf("error: %s", err.(*apiroutes.GetAppsAppRoutesRouteNotFound).Payload.Error.Message)
650+
case *apiroutes.GetAppsAppRoutesRouteDefault:
651+
return fmt.Errorf("unexpected error: %s", err.(*apiroutes.GetAppsAppRoutesRouteDefault).Payload.Error.Message)
652+
}
653+
return fmt.Errorf("unexpected error: %s", err)
654+
}
655+
656+
enc := json.NewEncoder(os.Stdout)
657+
enc.SetIndent("", "\t")
658+
jwtKey := resp.Payload.Route.JwtKey
659+
if jwtKey == "" {
660+
return errors.New("Empty JWT Key")
661+
}
662+
663+
// Create the Claims
664+
ss, err := f_common.GetJwt(jwtKey, expiration)
665+
if err != nil {
666+
return fmt.Errorf("unexpected error: %s", err)
667+
}
668+
t := struct {
669+
Token string `json:"token"`
670+
}{Token: ss}
671+
enc.Encode(t)
672+
673+
return nil
674+
}

fn/commands/testfn.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"bytes"
66
"errors"
77
"fmt"
8+
"github.com/iron-io/functions_go/models"
89
"net/url"
910
"os"
1011
"path"
@@ -174,7 +175,8 @@ func runremotetest(target string, in, expectedOut, expectedErr *string, env map[
174175
os.Setenv(k, v)
175176
restrictedEnv = append(restrictedEnv, k)
176177
}
177-
if err := callfn(target, stdin, &stdout, "", restrictedEnv); err != nil {
178+
dummyRoute := &models.Route{}
179+
if err := callfn(target, dummyRoute, stdin, &stdout, "", restrictedEnv); err != nil {
178180
return fmt.Errorf("%v\nstdout:%s\n", err, stdout.String())
179181
}
180182

0 commit comments

Comments
 (0)