Skip to content

Commit 1bd0dc2

Browse files
AbineshECADstephengaudetstephengaudet
authored
JWT Feature (#367)
* JWT as a middleware authenticator * Nil check for JWT * user credentials verification * Integration test for JWT * signature invalid issue fixed * Removed unwanted debug changes * Removes debug prints * test code refactor * Documentation for JWT * Doc page * Spelling correction * Doc corrections * Spell correction * Spell correction * config read issues fixed * Sample config added to document * Doc correction * Spell corrections * Client authorization sidebar change in docs * Client authorization sidebar change in docs * Client authorization sidebar change in docs * SidebarHeader code alignment change * token expiry optional * token expiry description added * doc update on tok_expiry * minor changes as per discussion * doc tab corrections * tok_exp changes as per discussion * minor variable name changes * credentials check logic updated * sg add an integration test for jwt * login endpoint added * login serve issue fix * fix jwt integration test * add more jwt tests * username and password error messages match * unit test case for Login handler * AuthHandler UT * JWT and authorized_keys are mutually exclusive, can't use both * sg-integrationtest add bad input test * JWT users per PKH & credentials rotation * Unit tests * nil check for jwt users list * UT refactor * fix integration test * per PKH config change * error messages got JWT label * secert validation changes * fix jwt unit test new secret length constraint * fix jwt unit test new secret length constraint * fix jwt unit test * fix integration test - new secret constraint * add (failing) jwt password rotation integration test * secret & password validation changes * add jwt per pkh integration tests * new and old user cred accepted in parallel till the exp time * integration test - add password complexity 16 characters * integrationtest - fix password rotation testcase - requires new secret * fix for issue #372 * error message added * fix broken test * fix broken test * old expiry logic change * fix password rotation integration test * tidy test * small jwt doc improvements * jwt doc fix typo * bump octez version in integration tests * update octez version in integration tests --------- Co-authored-by: stephengaudet <[email protected]> Co-authored-by: stephengaudet <[email protected]>
1 parent e391f50 commit 1bd0dc2

File tree

16 files changed

+1415
-12
lines changed

16 files changed

+1415
-12
lines changed

Diff for: cmd/commands/serve.go

+13
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ package commands
22

33
import (
44
"context"
5+
"fmt"
56
"net/http"
67
"time"
78

89
"github.com/ecadlabs/signatory/pkg/auth"
10+
"github.com/ecadlabs/signatory/pkg/middlewares"
911
"github.com/ecadlabs/signatory/pkg/server"
1012
log "github.com/sirupsen/logrus"
1113
"github.com/spf13/cobra"
@@ -24,6 +26,17 @@ func NewServeCommand(c *Context) *cobra.Command {
2426
Signer: c.signatory,
2527
}
2628

29+
if c.config.Server.JWTConfig != nil {
30+
if c.config.Server.AuthorizedKeys != nil {
31+
return fmt.Errorf("cannot use both JWT and static authorized keys")
32+
}
33+
mw := middlewares.NewMiddleware(c.config.Server.JWTConfig)
34+
if err := c.config.Server.JWTConfig.CheckUpdateNewCred(); err != nil {
35+
return err
36+
}
37+
srvConf.MidWare = mw
38+
}
39+
2740
if c.config.Server.AuthorizedKeys != nil {
2841
ak, err := auth.StaticAuthorizedKeys(c.config.Server.AuthorizedKeys.List()...)
2942
if err != nil {

Diff for: docs/jwt_auth.md

+163
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
---
2+
id: jwt
3+
title: JWT
4+
---
5+
6+
# JWT - Signatory interaction
7+
8+
The diagram represents a sequence of interactions between a client and a Signatory service, which appears to be a service that handles cryptographic signing operations. Here is a breakdown of the sequence of interactions:
9+
10+
```mermaid
11+
sequenceDiagram
12+
participant Client
13+
participant Signatory
14+
Client->>Signatory: Provide credentials
15+
Signatory->>Client: Verify credentials
16+
Note over Signatory: Create JWT
17+
Signatory->>Client: Send JWT
18+
Note over Client: Store JWT
19+
Client->>Signatory: Request signing operation (include JWT)
20+
Signatory->>Signatory: Validate JWT signature
21+
Note over Signatory: Check claims (public key, roles, permissions)
22+
alt JWT is valid and client is authorized
23+
Signatory->>Signatory: Sign operation with private key
24+
Signatory->>Client: Send signed operation
25+
else JWT is invalid or client is unauthorized
26+
Signatory->>Client: Access denied (error message)
27+
end
28+
```
29+
30+
1. The client provides its credentials to the Signatory service.
31+
2. The Signatory service verifies the credentials provided by the client.
32+
3. If the credentials are valid, the Signatory service creates a JSON Web Token (JWT) and sends it to the client.
33+
4. The client stores the JWT for later use.
34+
5. The client requests a signing operation from the Signatory service, and includes the JWT in the request.
35+
6. The Signatory service validates the JWT signature and checks the claims in the JWT (such as the public key, roles, and permissions).
36+
7. If the JWT is valid and the client is authorized, the Signatory service signs the operation with its private key and sends the signed operation back to the client.
37+
8. If the JWT is invalid or the client is unauthorized, the Signatory service sends an access denied error message to the client.
38+
9. When the token expires or any other token authentication failures happen, the client can start again from 1.
39+
40+
## Sample Signatory JWT configuration
41+
42+
`jwt_exp` is the time duration (in minutes) for which the token is valid and it is optional. When not provided the token expiry is 60 minutes, otherwise it is `current time + jwt_exp`.
43+
44+
`secret` is the secret used to sign the JWT token.
45+
46+
```yaml
47+
server:
48+
address: :6732
49+
utility_address: :9583
50+
jwt:
51+
users:
52+
user_name1:
53+
password: password1
54+
secret: secret1
55+
jwt_exp: 234
56+
user_name2:
57+
password: password2
58+
secret: secret2
59+
jwt_exp: 73
60+
```
61+
62+
## Sample client code which used in the integration test.
63+
64+
```go
65+
//Send user credentials to receive a new token
66+
url := "http://localhost:6732/login" + pub.Hash().String()
67+
client := &http.Client{}
68+
69+
req, err := http.NewRequest("POST", url, nil)
70+
require.NoError(t, err)
71+
72+
req.Header.Add("Content-Type", "application/json")
73+
req.Header.Add("username", "user1")
74+
req.Header.Add("password", "pass123")
75+
time.Sleep(2 * time.Second)
76+
77+
res, err := client.Do(req)
78+
require.NoError(t, err)
79+
require.Equal(t, 201, res.StatusCode)
80+
81+
body, err := ioutil.ReadAll(res.Body)
82+
require.NoError(t, err)
83+
require.NotEmpty(t, body)
84+
85+
res.Body.Close()
86+
87+
// Send request using received token
88+
89+
client = &http.Client{}
90+
91+
req, err = http.NewRequest("GET", url, nil)
92+
require.NoError(t, err)
93+
94+
req.Header.Add("Content-Type", "application/json")
95+
req.Header.Add("Authorization", "Bearer "+string(body))
96+
req.Header.Add("username", "user1")
97+
time.Sleep(2 * time.Second)
98+
99+
res, err = client.Do(req)
100+
require.NoError(t, err)
101+
require.Equal(t, 200, res.StatusCode)
102+
103+
defer res.Body.Close()
104+
body, err = ioutil.ReadAll(res.Body)
105+
require.NoError(t, err)
106+
require.NotEmpty(t, body)
107+
108+
fmt.Println("Response-GET-PKH: ", string(body))
109+
```
110+
111+
## Credentials rotation
112+
113+
The credentials can be rotated by updating the configuration file and restarting the Signatory service.
114+
115+
Older credentials can be removed from the configuration file after the new credentials are added and signatory is up and serving. The Signatory service will continue to accept the older credentials until the `old_cred_exp` time expires. If any error occurs with expiry time, the Signatory service will stop accepting the older credentials immediately. The `old_cred_exp` field is `GMT` expressed in `YYYY-MM-DD hh:mm:ss` format.
116+
117+
### sample configuration file:
118+
119+
```yaml
120+
server:
121+
address: :6732
122+
utility_address: :9583
123+
jwt:
124+
users:
125+
user_name1:
126+
password: password1
127+
secret: secret1
128+
jwt_exp: 234
129+
old_cred_exp: "2006-01-02 15:04:05"
130+
new_data:
131+
password: password1
132+
secret: secret1
133+
jwt_exp: 35
134+
```
135+
136+
## JWT users for each PKH
137+
138+
The JWT users can be configured for each PKH in the configuration file. Even if the JWT client provides a valid token, the request will be rejected if the user is not configured for that PKH requested for signing.
139+
If no JWT users are configured for a PKH, then any JWT token will be accepted for that PKH.
140+
141+
### sample configuration file:
142+
143+
```yaml
144+
tezos:
145+
tz3cbDCwrqFqfx1dBhHoXTwZ9FG3MDrtyMs6:
146+
jwt_users:
147+
- user_name1
148+
- user_name2
149+
log_payloads: true
150+
allow:
151+
block:
152+
endorsement:
153+
preendorsement:
154+
generic:
155+
- transaction
156+
- reveal
157+
```
158+
159+
## Important security note:
160+
161+
TLS should be taken care by the user who configures JWT as the authentication mechanism in Signatory for the clients.
162+
163+
The configuration file also contains sensitive information when using JWT with Signatory, so that file must also be secure.

Diff for: go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ require (
4343
github.com/enceve/crypto v0.0.0-20160707101852-34d48bb93815 // indirect
4444
github.com/go-playground/locales v0.14.1 // indirect
4545
github.com/go-playground/universal-translator v0.18.1 // indirect
46+
github.com/golang-jwt/jwt v3.2.2+incompatible
4647
github.com/golang-jwt/jwt/v5 v5.0.0-rc.1
4748
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
4849
github.com/golang/protobuf v1.5.3 // indirect

Diff for: go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
4545
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
4646
github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
4747
github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
48+
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
49+
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
4850
github.com/golang-jwt/jwt/v5 v5.0.0-rc.1 h1:tDQ1LjKga657layZ4JLsRdxgvupebc0xuPwRNuTfUgs=
4951
github.com/golang-jwt/jwt/v5 v5.0.0-rc.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
5052
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=

Diff for: integration_test/.env.current

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
export OCTEZ_VERSION=${ARCH}_v16.0-rc3
1+
export OCTEZ_VERSION=${ARCH}_v16.1
22
export PROTOCOL=Mumbai

Diff for: integration_test/.env.next

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
export OCTEZ_VERSION=${ARCH}_v17.0-beta1
1+
export OCTEZ_VERSION=${ARCH}_v17.0
22
export PROTOCOL=Nairobi

Diff for: integration_test/config.go

+23-3
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,37 @@ type Config struct {
1616
Tezos TezosConfig `yaml:"tezos"`
1717
}
1818

19+
type JwtConfig struct {
20+
Users map[string]*JwtUserData `yaml:"users"`
21+
}
22+
23+
type JwtUserData struct {
24+
Password string `yaml:"password"`
25+
Secret string `yaml:"secret"`
26+
Exp uint64 `yaml:"jwt_exp"`
27+
CredExp string `yaml:"old_cred_exp,omitempty"`
28+
NewCred *JwtNewCred `yaml:"new_data,omitempty"`
29+
}
30+
31+
type JwtNewCred struct {
32+
Password string `yaml:"password"`
33+
Secret string `yaml:"secret"`
34+
Exp uint64 `yaml:"jwt_exp"`
35+
}
36+
1937
type ServerConfig struct {
20-
Address string `yaml:"address"`
21-
UtilityAddress string `yaml:"utility_address"`
22-
Keys []string `yaml:"authorized_keys,omitempty"`
38+
Address string `yaml:"address"`
39+
UtilityAddress string `yaml:"utility_address"`
40+
Keys []string `yaml:"authorized_keys,omitempty"`
41+
Jwt JwtConfig `yaml:"jwt,omitempty"`
2342
}
2443

2544
type TezosConfig = map[string]*TezosPolicy
2645

2746
type TezosPolicy struct {
2847
Allow map[string][]string `yaml:"allow"`
2948
LogPayloads bool `yaml:"log_payloads"`
49+
JwtUsers []string `yaml:"jwt_users,omitempty"`
3050
}
3151

3252
type VaultConfig struct {

0 commit comments

Comments
 (0)