Skip to content

Commit a859785

Browse files
committed
Add authentication that supports usernames and passwords
This commit adds `ask` mode authentication that allows node operators to configure Bacalhau to ask the user for arbitrary information to be used as a credential. This method can be used to implement basic usernames and passwords, shared secrets, security questions and even 2FA. The associated policy additionally returns a JSON Schema to show what information is required. The CLI uses the schema to ask the user for the right information. In the future, the Web UI will do this as well.
1 parent 849dab5 commit a859785

File tree

20 files changed

+598
-18
lines changed

20 files changed

+598
-18
lines changed

apps/job-info-consumer/consumer/go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ require (
323323
go.uber.org/multierr v1.11.0 // indirect
324324
go.uber.org/zap v1.26.0 // indirect
325325
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
326-
golang.org/x/crypto v0.17.0 // indirect
326+
golang.org/x/crypto v0.18.0 // indirect
327327
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect
328328
golang.org/x/mod v0.14.0 // indirect
329329
golang.org/x/net v0.19.0 // indirect

apps/job-info-consumer/consumer/go.sum

+1-1
Original file line numberDiff line numberDiff line change
@@ -1183,7 +1183,7 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0
11831183
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
11841184
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
11851185
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
1186-
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
1186+
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
11871187
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
11881188
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
11891189
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=

cmd/util/auth/ask.go

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package auth
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"encoding/json"
7+
"fmt"
8+
"os"
9+
10+
"github.com/santhosh-tekuri/jsonschema/v5"
11+
"github.com/spf13/cobra"
12+
"golang.org/x/term"
13+
)
14+
15+
// Returns a responder that responds to authentication requirements of type
16+
// `authn.MethodTypeAsk`. Reads the JSON Schema returned by the `ask` endpoint
17+
// and uses it to ask appropriate questions to the user on their terminal, and
18+
// then returns their response as serialized JSON.
19+
func askResponder(cmd *cobra.Command) responder {
20+
return func(request *json.RawMessage) ([]byte, error) {
21+
compiler := jsonschema.NewCompiler()
22+
compiler.ExtractAnnotations = true
23+
24+
if err := compiler.AddResource("", bytes.NewReader(*request)); err != nil {
25+
return nil, err
26+
}
27+
28+
schema, err := compiler.Compile("")
29+
if err != nil {
30+
return nil, err
31+
}
32+
33+
response := make(map[string]any, len(schema.Properties))
34+
for _, name := range schema.Required {
35+
subschema := schema.Properties[name]
36+
37+
if len(subschema.Types) < 1 {
38+
return nil, fmt.Errorf("invalid schema: property %q has no type", name)
39+
}
40+
41+
typ := subschema.Types[0]
42+
if typ == "object" {
43+
return nil, fmt.Errorf("invalid schema: property %q has non-scalar type", name)
44+
}
45+
46+
fmt.Fprintf(cmd.ErrOrStderr(), "%s: ", name)
47+
48+
var input []byte
49+
var err error
50+
51+
// If the property is marked as write only, assume it is a sensitive
52+
// value and make sure we don't display it in the terminal
53+
if subschema.WriteOnly {
54+
input, err = term.ReadPassword(int(os.Stdin.Fd()))
55+
fmt.Fprintln(cmd.ErrOrStderr())
56+
} else {
57+
reader := bufio.NewScanner(cmd.InOrStdin())
58+
if reader.Scan() {
59+
input = reader.Bytes()
60+
}
61+
err = reader.Err()
62+
}
63+
64+
if err != nil {
65+
return nil, err
66+
}
67+
response[name] = string(input)
68+
}
69+
70+
respBytes, err := json.Marshal(response)
71+
if err != nil {
72+
return nil, err
73+
}
74+
75+
return respBytes, schema.Validate(response)
76+
}
77+
}

cmd/util/auth/auth.go

+5-4
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@ import (
1919

2020
type responder = func(request *json.RawMessage) (response []byte, err error)
2121

22-
var supportedMethods map[authn.MethodType]responder = map[authn.MethodType]responder{
23-
authn.MethodTypeChallenge: challenge.Respond,
24-
}
25-
2622
func RunAuthenticationFlow(cmd *cobra.Command) (string, error) {
23+
supportedMethods := map[authn.MethodType]responder{
24+
authn.MethodTypeChallenge: challenge.Respond,
25+
authn.MethodTypeAsk: askResponder(cmd),
26+
}
27+
2728
client := util.GetAPIClientV2(cmd.Context())
2829
methods, err := client.Auth().Methods(&apimodels.ListAuthnMethodsRequest{})
2930
if err != nil {

docs/docs/dev/auth_flow.md

+19
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,25 @@ return to the endpoint.
8383
}
8484
```
8585

86+
### `ask` authentication
87+
88+
This method requires the user to manually input some information. This method
89+
can be used to implement username and password authentication, shared secret
90+
authentication, and even 2FA or security question auth.
91+
92+
The required information is represented by a JSON Schema in the object itself.
93+
The implementation should parse the JSON Schema and ask the user questions to
94+
populate an object that is valid by it.
95+
96+
```json
97+
{
98+
"$schema": "https://json-schema.org/draft/2020-12/schema",
99+
"$id": "https://bacalhau.org/auth/ask",
100+
"type": "object",
101+
"$ref": "https://json-schema.org/draft/2020-12/schema",
102+
}
103+
```
104+
86105
## 2. Run the authn flow and submit the result for an access token
87106

88107
The user agent decides which authentication method to use (e.g. by asking the

docs/docs/running-node/auth.md

+19
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,22 @@ include acceptable client IDs, found by running `bacalhau id`.
4747

4848
Once the node is restarted, only keys in the allowed list will be able to access
4949
any API.
50+
51+
## Username and password access
52+
53+
Users can authenticate using a username and password instead of specifying a
54+
private key for access. Again, this requires installation of an appropriate
55+
policy on the server.
56+
57+
curl -sL https://raw.githubusercontent.com/bacalhau-project/bacalhau/main/pkg/authn/ask/ask_ns_password.rego -o ~/.bacalhau/ask_ns_password.rego
58+
bacalhau config set Node.Auth.Methods.Password.Type ask
59+
bacalhau config set Node.Auth.Methods.Password.PolicyPath ~/.bacalhau/ask_ns_password.rego
60+
61+
Passwords are not stored in plaintext and are salted. The downloaded policy
62+
expects password hashes and salts generated by `scrypt`. To generate a salted
63+
password, the helper script in `pkg/authn/ask/gen_password` can be used:
64+
65+
cd pkg/authn/ask/gen_password && go run .
66+
67+
This will ask for a password and generate a salt and hash to authenticate with
68+
it. Add the encoded username, salt and hash into the `ask_ns_password.rego`.

go.mod

+3-2
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ require (
5656
github.com/pkg/errors v0.9.1
5757
github.com/ricochet2200/go-disk-usage/du v0.0.0-20210707232629-ac9918953285
5858
github.com/rs/zerolog v1.31.0
59+
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
5960
github.com/spf13/cobra v1.8.0
6061
github.com/spf13/pflag v1.0.5
6162
github.com/spf13/viper v1.16.0
@@ -82,7 +83,7 @@ require (
8283
go.uber.org/mock v0.4.0
8384
go.uber.org/multierr v1.11.0
8485
go.uber.org/zap v1.26.0
85-
golang.org/x/crypto v0.17.0
86+
golang.org/x/crypto v0.18.0
8687
golang.org/x/exp v0.0.0-20230321023759-10a507213a29
8788
gopkg.in/alessio/shellescape.v1 v1.0.0-20170105083845-52074bc9df61
8889
k8s.io/apimachinery v0.29.0
@@ -373,7 +374,7 @@ require (
373374
golang.org/x/oauth2 v0.13.0 // indirect
374375
golang.org/x/sync v0.6.0
375376
golang.org/x/sys v0.16.0 // indirect
376-
golang.org/x/term v0.16.0 // indirect
377+
golang.org/x/term v0.16.0
377378
golang.org/x/text v0.14.0 // indirect
378379
golang.org/x/time v0.5.0
379380
golang.org/x/tools v0.16.1 // indirect

go.sum

+4-1
Original file line numberDiff line numberDiff line change
@@ -1069,6 +1069,8 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
10691069
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
10701070
github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
10711071
github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
1072+
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
1073+
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
10721074
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
10731075
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
10741076
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
@@ -1341,8 +1343,9 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0
13411343
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
13421344
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
13431345
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
1344-
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
13451346
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
1347+
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
1348+
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
13461349
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
13471350
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
13481351
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=

ops/aws/canary/lambda/go.mod

+1-2
Original file line numberDiff line numberDiff line change
@@ -304,14 +304,13 @@ require (
304304
go.uber.org/multierr v1.11.0 // indirect
305305
go.uber.org/zap v1.26.0 // indirect
306306
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
307-
golang.org/x/crypto v0.17.0 // indirect
307+
golang.org/x/crypto v0.18.0 // indirect
308308
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect
309309
golang.org/x/mod v0.14.0 // indirect
310310
golang.org/x/net v0.19.0 // indirect
311311
golang.org/x/oauth2 v0.13.0 // indirect
312312
golang.org/x/sync v0.6.0 // indirect
313313
golang.org/x/sys v0.16.0 // indirect
314-
golang.org/x/term v0.16.0 // indirect
315314
golang.org/x/text v0.14.0 // indirect
316315
golang.org/x/time v0.5.0 // indirect
317316
golang.org/x/tools v0.16.1 // indirect

ops/aws/canary/lambda/go.sum

+1-1
Original file line numberDiff line numberDiff line change
@@ -1176,7 +1176,7 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0
11761176
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
11771177
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
11781178
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
1179-
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
1179+
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
11801180
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
11811181
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
11821182
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=

pkg/authn/ask/ask_ns_example.rego

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package bacalhau.authn
2+
3+
import rego.v1
4+
5+
schema := {
6+
"type": "object",
7+
"properties": {"magic": {"type": "string"}},
8+
"required": ["magic"],
9+
}
10+
11+
token := t if {
12+
input.magic == "open sesame"
13+
14+
t := io.jwt.encode_sign(
15+
{
16+
"typ": "JWT",
17+
"alg": "RS256",
18+
},
19+
{
20+
"iss": input.nodeId,
21+
"sub": "aladdin",
22+
"aud": [input.nodeId],
23+
"iat": now,
24+
"exp": one_month,
25+
"ns": {
26+
# Read-only access to all namespaces
27+
"*": read_only,
28+
# Writable access to own namespace
29+
"genie": full_access,
30+
},
31+
},
32+
input.signingKey,
33+
)
34+
}

pkg/authn/ask/ask_ns_password.rego

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package bacalhau.authn
2+
3+
import rego.v1
4+
5+
# Implements a policy where clients that supply a valid username and password
6+
# are permitted access. Anonymous users are not permitted.
7+
#
8+
# Modify the `userlist` to control what users are permitted access.
9+
# Modify the `ns` key of the token to control what namespaces they can access.
10+
11+
schema := {
12+
"$schema": "http://json-schema.org/draft-07/schema#",
13+
"type": "object",
14+
"properties": {
15+
"username": {"type": "string"},
16+
"password": {"type": "string", "writeOnly": true},
17+
},
18+
"required": ["username", "password"],
19+
}
20+
21+
now := time.now_ns() / 1000
22+
23+
one_month := time.add_date(time.now_ns(), 0, 1, 0) / 1000
24+
25+
# userlist should be a map of usernames to scrypt-hashed passwords and salts. in
26+
# a simple live setup they can be hard coded here as a map, and then apply
27+
# appropriate file permissions to this policy.
28+
userlist := {
29+
# "username": ["hash", "salt"]
30+
}
31+
32+
valid_user := input.ask.username if {
33+
input.ask.username in userlist
34+
35+
hash := userlist[input.ask.username][0]
36+
salt := userlist[input.ask.username][1]
37+
hash == scrypt(input.ask.password, salt)
38+
}
39+
40+
token := io.jwt.encode_sign(
41+
{
42+
"typ": "JWT",
43+
"alg": "RS256",
44+
},
45+
{
46+
"iss": input.nodeId,
47+
"sub": valid_user,
48+
"aud": [input.nodeId],
49+
"iat": now,
50+
"exp": one_month,
51+
"ns": {
52+
# Read-only access to all namespaces
53+
"*": read_only,
54+
# Writable access to own namespace
55+
valid_user: full_access,
56+
},
57+
},
58+
input.signingKey,
59+
)
60+
61+
namespace_read := 1
62+
namespace_write := 2
63+
namespace_download := 4
64+
namespace_cancel := 8
65+
66+
read_only := bits.and(namespace_read, namespace_download)
67+
full_access := bits.and(bits.and(namespace_write, namespace_cancel), read_only)

0 commit comments

Comments
 (0)