Skip to content

Commit 21da3bc

Browse files
authored
[Feature] Added user, exp, and allowed_paths in JWT (#289)
1 parent 80cee7c commit 21da3bc

File tree

4 files changed

+146
-7
lines changed

4 files changed

+146
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## [master](https://github.com/arangodb-helper/arangodb/tree/master) (N/A)
44
- Allow to pass environment variables to processes and standardize argument pass (--envs.<group>.<ENV>=<VALUE> and --args.<group>.<ARG>=<VALUE>)
5+
- Extend JWT Generator functionality by additional fields
56

67
# ArangoDB Starter Changelog Before 0.15.0
78

auth.go

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ package main
2525
import (
2626
"fmt"
2727
"io/ioutil"
28+
"strconv"
2829
"strings"
30+
"time"
2931

3032
"github.com/spf13/cobra"
3133

@@ -34,9 +36,10 @@ import (
3436

3537
var (
3638
cmdAuth = &cobra.Command{
37-
Use: "auth",
38-
Short: "ArangoDB authentication helper commands",
39-
Run: cmdShowUsage,
39+
Use: "auth",
40+
Short: "ArangoDB authentication helper commands",
41+
PersistentPreRunE: persistentAuthPreFunE,
42+
Run: cmdShowUsage,
4043
}
4144
cmdAuthHeader = &cobra.Command{
4245
Use: "header",
@@ -51,6 +54,12 @@ var (
5154
authOptions struct {
5255
jwtSecretFile string
5356
user string
57+
paths []string
58+
exp string
59+
expDuration time.Duration
60+
61+
fieldsOverride []string
62+
fieldsOverrideMap map[string]interface{}
5463
}
5564
)
5665

@@ -62,6 +71,9 @@ func init() {
6271
pf := cmdAuth.PersistentFlags()
6372
pf.StringVar(&authOptions.jwtSecretFile, "auth.jwt-secret", "", "name of a plain text file containing a JWT secret used for server authentication")
6473
pf.StringVar(&authOptions.user, "auth.user", "", "name of a user to authenticate as. If empty, 'super-user' authentication is used")
74+
pf.StringSliceVar(&authOptions.paths, "auth.paths", nil, "a list of allowed pathes. The path must not include the '_db/DBNAME' prefix.")
75+
pf.StringVar(&authOptions.exp, "auth.exp", "", "a time in which token should expire - based on current time in UTC. Supported units: h, m, s (default)")
76+
pf.StringSliceVar(&authOptions.fieldsOverride, "auth.fields", nil, "a list of additional fields set in the token. This flags override one auto-generated in token")
6577
}
6678

6779
// mustAuthCreateJWTToken creates a the JWT token based on authentication options.
@@ -77,7 +89,7 @@ func mustAuthCreateJWTToken() string {
7789
log.Fatal().Err(err).Msgf("Failed to read JWT secret file '%s'", authOptions.jwtSecretFile)
7890
}
7991
jwtSecret := strings.TrimSpace(string(content))
80-
token, err := service.CreateJwtToken(jwtSecret, authOptions.user)
92+
token, err := service.CreateJwtToken(jwtSecret, authOptions.user, "", authOptions.paths, authOptions.expDuration, authOptions.fieldsOverrideMap)
8193
if err != nil {
8294
log.Fatal().Err(err).Msg("Failed to create JWT token")
8395
}
@@ -95,3 +107,66 @@ func cmdAuthTokenRun(cmd *cobra.Command, args []string) {
95107
token := mustAuthCreateJWTToken()
96108
fmt.Println(token)
97109
}
110+
111+
func persistentAuthPreFunE(cmd *cobra.Command, args []string) error {
112+
cmdMain.PersistentPreRun(cmd, args)
113+
114+
if authOptions.exp != "" {
115+
d, err := durationParser(authOptions.exp, "s")
116+
if err != nil {
117+
return err
118+
}
119+
120+
if d < 0 {
121+
return fmt.Errorf("negative duration under --auth.exp is not allowed")
122+
}
123+
124+
authOptions.expDuration = d
125+
}
126+
127+
authOptions.fieldsOverrideMap = map[string]interface{}{}
128+
129+
for _, field := range authOptions.fieldsOverride {
130+
tokens := strings.Split(field, "=")
131+
if len(tokens) == 0 {
132+
return fmt.Errorf("invalid format of the field override: `%s`", field)
133+
}
134+
135+
key := tokens[0]
136+
value := strings.Join(tokens[1:], "=")
137+
var calculatedValue interface{} = value
138+
139+
switch value {
140+
case "true":
141+
calculatedValue = true
142+
case "false":
143+
calculatedValue = false
144+
default:
145+
if i, err := strconv.Atoi(value); err == nil {
146+
calculatedValue = i
147+
}
148+
}
149+
150+
authOptions.fieldsOverrideMap[key] = calculatedValue
151+
}
152+
153+
return nil
154+
}
155+
156+
func durationParser(duration string, defaultUnit string) (time.Duration, error) {
157+
if d, err := time.ParseDuration(duration); err == nil {
158+
return d, nil
159+
} else {
160+
if !strings.HasPrefix(err.Error(), "time: missing unit in duration ") {
161+
return 0, err
162+
}
163+
164+
duration = fmt.Sprintf("%s%s", duration, defaultUnit)
165+
166+
if d, err := time.ParseDuration(duration); err == nil {
167+
return d, nil
168+
} else {
169+
return 0, err
170+
}
171+
}
172+
}

auth_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//
2+
// DISCLAIMER
3+
//
4+
// Copyright 2021 ArangoDB GmbH, Cologne, Germany
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
//
18+
// Copyright holder is ArangoDB GmbH, Cologne, Germany
19+
//
20+
// Author Adam Janikowski
21+
//
22+
23+
package main
24+
25+
import (
26+
"testing"
27+
28+
"github.com/stretchr/testify/require"
29+
)
30+
31+
func parseDuration(t *testing.T, in, out string) {
32+
t.Run(in, func(t *testing.T) {
33+
v, err := durationParser(in, "s")
34+
require.NoError(t, err)
35+
36+
s := v.String()
37+
38+
require.Equal(t, out, s)
39+
})
40+
}
41+
42+
func Test_Auth_DurationTest(t *testing.T) {
43+
parseDuration(t, "5", "5s")
44+
parseDuration(t, "5s", "5s")
45+
parseDuration(t, "5m", "5m0s")
46+
parseDuration(t, "5h", "5h0m0s")
47+
}

service/authentication.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ package service
2424

2525
import (
2626
"net/http"
27+
"time"
2728

2829
jwt "github.com/dgrijalva/jwt-go"
2930
)
@@ -35,19 +36,34 @@ const (
3536

3637
// CreateJwtToken calculates a JWT authorization token based on the given secret.
3738
// If the secret is empty, an empty token is returned.
38-
func CreateJwtToken(jwtSecret, user string) (string, error) {
39+
func CreateJwtToken(jwtSecret, user string, serverId string, paths []string, exp time.Duration, fieldsOverride jwt.MapClaims) (string, error) {
3940
if jwtSecret == "" {
4041
return "", nil
4142
}
43+
if serverId == "" {
44+
serverId = "foo"
45+
}
46+
4247
// Create a new token object, specifying signing method and the claims
4348
// you would like it to contain.
4449
claims := jwt.MapClaims{
4550
"iss": "arangodb",
46-
"server_id": "foo",
51+
"server_id": serverId,
4752
}
4853
if user != "" {
4954
claims["preferred_username"] = user
5055
}
56+
if paths != nil {
57+
claims["allowed_paths"] = paths
58+
}
59+
if exp > 0 {
60+
t := time.Now().UTC()
61+
claims["iat"] = t.Unix()
62+
claims["exp"] = t.Add(exp).Unix()
63+
}
64+
for k, v := range fieldsOverride {
65+
claims[k] = v
66+
}
5167
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
5268

5369
// Sign and get the complete encoded token as a string using the secret
@@ -66,7 +82,7 @@ func addJwtHeader(req *http.Request, jwtSecret string) error {
6682
if jwtSecret == "" {
6783
return nil
6884
}
69-
signedToken, err := CreateJwtToken(jwtSecret, "")
85+
signedToken, err := CreateJwtToken(jwtSecret, "", "", nil, 0, nil)
7086
if err != nil {
7187
return maskAny(err)
7288
}

0 commit comments

Comments
 (0)