Skip to content

Commit

Permalink
Add authentication that supports usernames and passwords
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
simonwo committed Jan 25, 2024
1 parent 849dab5 commit d0200be
Show file tree
Hide file tree
Showing 20 changed files with 598 additions and 18 deletions.
2 changes: 1 addition & 1 deletion apps/job-info-consumer/consumer/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/crypto v0.18.0 // indirect
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/net v0.19.0 // indirect
Expand Down
2 changes: 1 addition & 1 deletion apps/job-info-consumer/consumer/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1183,7 +1183,7 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
Expand Down
77 changes: 77 additions & 0 deletions cmd/util/auth/ask.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package auth

import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"os"

"github.com/santhosh-tekuri/jsonschema/v5"
"github.com/spf13/cobra"
"golang.org/x/term"
)

// Returns a responder that responds to authentication requirements of type
// `authn.MethodTypeAsk`. Reads the JSON Schema returned by the `ask` endpoint
// and uses it to ask appropriate questions to the user on their terminal, and
// then returns their response as serialized JSON.
func askResponder(cmd *cobra.Command) responder {
return func(request *json.RawMessage) ([]byte, error) {
compiler := jsonschema.NewCompiler()
compiler.ExtractAnnotations = true

if err := compiler.AddResource("", bytes.NewReader(*request)); err != nil {
return nil, err
}

schema, err := compiler.Compile("")
if err != nil {
return nil, err
}

response := make(map[string]any, len(schema.Properties))
for _, name := range schema.Required {
subschema := schema.Properties[name]

if len(subschema.Types) < 1 {
return nil, fmt.Errorf("invalid schema: property %q has no type", name)
}

typ := subschema.Types[0]
if typ == "object" {
return nil, fmt.Errorf("invalid schema: property %q has non-scalar type", name)
}

fmt.Fprintf(cmd.ErrOrStderr(), "%s: ", name)

var input []byte
var err error

// If the property is marked as write only, assume it is a sensitive
// value and make sure we don't display it in the terminal
if subschema.WriteOnly {
input, err = term.ReadPassword(int(os.Stdin.Fd()))
fmt.Fprintln(cmd.ErrOrStderr())
} else {
reader := bufio.NewScanner(cmd.InOrStdin())
if reader.Scan() {
input = reader.Bytes()
}
err = reader.Err()
}

if err != nil {
return nil, err
}
response[name] = string(input)
}

respBytes, err := json.Marshal(response)
if err != nil {
return nil, err
}

return respBytes, schema.Validate(response)
}
}
9 changes: 5 additions & 4 deletions cmd/util/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ import (

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

var supportedMethods map[authn.MethodType]responder = map[authn.MethodType]responder{
authn.MethodTypeChallenge: challenge.Respond,
}

func RunAuthenticationFlow(cmd *cobra.Command) (string, error) {
supportedMethods := map[authn.MethodType]responder{
authn.MethodTypeChallenge: challenge.Respond,
authn.MethodTypeAsk: askResponder(cmd),
}

client := util.GetAPIClientV2(cmd.Context())
methods, err := client.Auth().Methods(&apimodels.ListAuthnMethodsRequest{})
if err != nil {
Expand Down
19 changes: 19 additions & 0 deletions docs/docs/dev/auth_flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,25 @@ return to the endpoint.
}
```

### `ask` authentication

This method requires the user to manually input some information. This method
can be used to implement username and password authentication, shared secret
authentication, and even 2FA or security question auth.

The required information is represented by a JSON Schema in the object itself.
The implementation should parse the JSON Schema and ask the user questions to
populate an object that is valid by it.

```json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://bacalhau.org/auth/ask",
"type": "object",
"$ref": "https://json-schema.org/draft/2020-12/schema",
}
```

## 2. Run the authn flow and submit the result for an access token

The user agent decides which authentication method to use (e.g. by asking the
Expand Down
19 changes: 19 additions & 0 deletions docs/docs/running-node/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,22 @@ include acceptable client IDs, found by running `bacalhau id`.

Once the node is restarted, only keys in the allowed list will be able to access
any API.

## Username and password access

Users can authenticate using a username and password instead of specifying a
private key for access. Again, this requires installation of an appropriate
policy on the server.

curl -sL https://raw.githubusercontent.com/bacalhau-project/bacalhau/main/pkg/authn/ask/ask_ns_password.rego -o ~/.bacalhau/ask_ns_password.rego
bacalhau config set Node.Auth.Methods.Password.Type ask
bacalhau config set Node.Auth.Methods.Password.PolicyPath ~/.bacalhau/ask_ns_password.rego

Passwords are not stored in plaintext and are salted. The downloaded policy
expects password hashes and salts generated by `scrypt`. To generate a salted
password, the helper script in `pkg/authn/ask/gen_password` can be used:

cd pkg/authn/ask/gen_password && go run .

This will ask for a password and generate a salt and hash to authenticate with
it. Add the encoded username, salt and hash into the `ask_ns_password.rego`.
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ require (
github.com/pkg/errors v0.9.1
github.com/ricochet2200/go-disk-usage/du v0.0.0-20210707232629-ac9918953285
github.com/rs/zerolog v1.31.0
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
github.com/spf13/cobra v1.8.0
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.16.0
Expand All @@ -82,7 +83,7 @@ require (
go.uber.org/mock v0.4.0
go.uber.org/multierr v1.11.0
go.uber.org/zap v1.26.0
golang.org/x/crypto v0.17.0
golang.org/x/crypto v0.18.0
golang.org/x/exp v0.0.0-20230321023759-10a507213a29
gopkg.in/alessio/shellescape.v1 v1.0.0-20170105083845-52074bc9df61
k8s.io/apimachinery v0.29.0
Expand Down Expand Up @@ -373,7 +374,7 @@ require (
golang.org/x/oauth2 v0.13.0 // indirect
golang.org/x/sync v0.6.0
golang.org/x/sys v0.16.0 // indirect
golang.org/x/term v0.16.0 // indirect
golang.org/x/term v0.16.0
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.5.0
golang.org/x/tools v0.16.1 // indirect
Expand Down
5 changes: 4 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1069,6 +1069,8 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
Expand Down Expand Up @@ -1341,8 +1343,9 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
Expand Down
3 changes: 1 addition & 2 deletions ops/aws/canary/lambda/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -304,14 +304,13 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/crypto v0.18.0 // indirect
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/oauth2 v0.13.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/term v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.16.1 // indirect
Expand Down
2 changes: 1 addition & 1 deletion ops/aws/canary/lambda/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1176,7 +1176,7 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
Expand Down
34 changes: 34 additions & 0 deletions pkg/authn/ask/ask_ns_example.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package bacalhau.authn

import rego.v1

schema := {
"type": "object",
"properties": {"magic": {"type": "string"}},
"required": ["magic"],
}

token := t if {
input.magic == "open sesame"

t := io.jwt.encode_sign(
{
"typ": "JWT",
"alg": "RS256",
},
{
"iss": input.nodeId,
"sub": "aladdin",
"aud": [input.nodeId],
"iat": now,
"exp": one_month,
"ns": {
# Read-only access to all namespaces
"*": read_only,
# Writable access to own namespace
"genie": full_access,
},
},
input.signingKey,
)
}
67 changes: 67 additions & 0 deletions pkg/authn/ask/ask_ns_password.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package bacalhau.authn

import rego.v1

# Implements a policy where clients that supply a valid username and password
# are permitted access. Anonymous users are not permitted.
#
# Modify the `userlist` to control what users are permitted access.
# Modify the `ns` key of the token to control what namespaces they can access.

schema := {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"username": {"type": "string"},
"password": {"type": "string", "writeOnly": true},
},
"required": ["username", "password"],
}

now := time.now_ns() / 1000

one_month := time.add_date(time.now_ns(), 0, 1, 0) / 1000

# userlist should be a map of usernames to scrypt-hashed passwords and salts. in
# a simple live setup they can be hard coded here as a map, and then apply
# appropriate file permissions to this policy.
userlist := {
# "username": ["hash", "salt"]
}

valid_user := input.ask.username if {
input.ask.username in userlist

hash := userlist[input.ask.username][0]
salt := userlist[input.ask.username][1]
hash == scrypt(input.ask.password, salt)
}

token := io.jwt.encode_sign(
{
"typ": "JWT",
"alg": "RS256",
},
{
"iss": input.nodeId,
"sub": valid_user,
"aud": [input.nodeId],
"iat": now,
"exp": one_month,
"ns": {
# Read-only access to all namespaces
"*": read_only,
# Writable access to own namespace
valid_user: full_access,
},
},
input.signingKey,
)

namespace_read := 1
namespace_write := 2
namespace_download := 4
namespace_cancel := 8

read_only := bits.and(namespace_read, namespace_download)
full_access := bits.and(bits.and(namespace_write, namespace_cancel), read_only)
Loading

0 comments on commit d0200be

Please sign in to comment.