Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add optional token hashing #25982

Draft
wants to merge 9 commits into
base: main-2.x
Choose a base branch
from
Draft

Conversation

gwossum
Copy link
Member

@gwossum gwossum commented Feb 6, 2025

Add optional token hashing with --use-hashed-tokens command line option.

@gwossum gwossum added kind/feature area/2.x OSS 2.0 related issues and PRs labels Feb 6, 2025
@gwossum gwossum self-assigned this Feb 6, 2025
Copy link
Contributor

@davidby-influx davidby-influx left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A variety of smaller issues. I will need to review this again to fully understand the larger changes.

Bucket: "holder",
RetentionPeriodSeconds: 1,
})
if err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

require.NoError?

t.Fatal(err)
})
if err != nil {
t.Fatal(err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

require.NoError?

Copy link
Contributor

@davidby-influx davidby-influx left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My usual add more context to error and log messages and a few more substantive questions and suggestions

@@ -185,21 +182,16 @@ func TestService_handlePostAuthorization(t *testing.T) {
handler.handlePostAuthorization(w, r)

res := w.Result()
content := res.Header.Get("Content-Type")
contentType := res.Header.Get("Content-Type")
body, _ := io.ReadAll(res.Body)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe an error check on ReadAll? probably never fails, though....

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handled now.

}
diff, err := jsonDiff(string(body), tt.wants.body)
require.NoError(t, err)
require.Empty(t, diff)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A little context message in these two would help debugging: diff failed on <whatever this is doing> and
expected no difference in authorization response or something like that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added an error message.

}
diff, err := jsonDiff(string(body), tt.wants.body)
require.NoError(t, err)
require.Empty(t, diff)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added an error message.

t.Errorf("%q. handleDeleteAuthorization() = %v, want %v", tt.name, content, tt.wants.contentType)
require.Equal(t, tt.wants.statusCode, res.StatusCode)
if tt.wants.contentType != "" {
require.Equal(t, tt.wants.contentType, contentType)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mention handleDeleteAuthorization in the log message.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

}

// deleteIndices removes indices for the given token and hashedToken.
func (s *Store) deleteIndices(ctx context.Context, tx kv.Tx, token, hashedToken string) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if this should be more robust. Even if we can't get one bucket, remove the (hashed) token from the other, and if we can't remove one token, still try to remove the other. Would that leave us in a better state for recovery, or not?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If error will get returned up the call tree and cause the BoltDB transaction to be rolled back, so we won't leave BoltDB in an inconsistent state.

Copy link
Contributor

@davidby-influx davidby-influx left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All changes pertain to comments, but important one is whether to delete or include a commented out code section

Add optional token hashing with `--use-hashed-tokens` command line option.
Create duplicate tokens from being created. This is a bug introduced
earlier in this PR. Also improve tests so they detect the bug and use
testify throughout.
Fix a bug that only allowed hashed tokens to be looked up if they used the
currently active hashing algorithm. Also added tests for configuration
migration scenarios (enabling and disabling hashing, changing hashing scheme).
Address PR issues on comments, error handling, and add final token
matching check in `Store.GetAuthorizationByToken`.
Changes in addition to minor cleanups:
- `authentication.Store` can now log info and warnings
- Improved logic in `UpdateAuthorization` when both Token and HashedToken
  are set. Added supporting test cases.
@gwossum gwossum force-pushed the gw/edge306/token_hashing branch from 15f8e92 to 6aa2d1a Compare February 25, 2025 21:42
Copy link

@devanbenz devanbenz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a few comments mostly about extra safety checks and some error messaging. I've been able to get about half way through the PR. Going to step away for a minute to take a little brain break and then finish up the rest of the PR for my first pass.

// Create the hasher used for hashing new tokens before storage.
hasher, err := influxdb2_algo.New(influxdb2_algo.WithVariant(options.hasherVariant))
if err != nil {
return nil, fmt.Errorf("creating hasher for AuthorizationHasher: %w", err)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

display hasherVariant for error?

fmt.Errorf("creating hasher with variant %s for AuthorizationHasher: %w", options.hasherVariant, err)

// AllHashes generates a list of PHC-encoded hashes of token for all deterministic (i.e. non-salted) supported hashes.
func (h *AuthorizationHasher) AllHashes(token string) ([]string, error) {
hashes := make([]string, len(h.allHashers))
for idx, hasher := range h.allHashers {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could there ever be a code path where h.allHashers could be empty and we expect for it not to be? Would it be a good idea to check h.allHashers len and warn on it if its == 0?

for idx, hasher := range h.allHashers {
digest, err := hasher.Hash(token)
if err != nil {
variantName := "N/A"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoding "N/A" here seems weird to me. Could this be an enum or a constant of some sort?

t.Fatal(err)
}
storage, err := NewStore(context.Background(), s, useHashedTokens)
require.NoError(t, err)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add some context to this error so we know what failed

require.NoError(t, err, "creating new store")

t.Fatalf("failed to unmarshal authorization: %v", err)
}
req, err := newPostAuthorizationRequest(tt.args.authorization)
require.NoError(t, err)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to above - add small context string.

return nil
})
if err != nil {
return fmt.Errorf("error migrating hashed tokens: %w", err)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding some more context to the error such as ID and maybe token description? Similar to the s.log.Warn above?

// Redact Token, if needed. This is done at the lowest level so it is impossible to serialize
// raw tokens if hashing is enabled.
if s.useHashedTokens {
// Redact a copy, not the original. The raw Token value is still needed by the caller in some cases.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice thank you for the comment, makes the code below much easier to reason about.

// error is returned.
func (s *Store) transformToken(a *influxdb.Authorization) error {
// Verify Token and HashedToken match if both are set.
if a.Token != "" && a.HashedToken != "" {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since 'a' is a pointer should there be a nil check for safety before field access?

// Note that even if a.HashedToken is set, we will regenerate it here. This ensures
// that a.HashedToken will be stored using the currently configured hashing algorithm.
if hashedToken, err := s.hasher.Hash(a.Token); err != nil {
return fmt.Errorf("error hashing token: %w", err)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any other information such as description or ID?

if auth.HashedToken != "" {
match, err := s.hasher.Match(auth.HashedToken, token)
if err != nil {
return false, fmt.Errorf("error matching hashed token for validation: %w", err)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More information like ID and description?

Copy link

@devanbenz devanbenz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a few more comments to storage_authorization.go I noticed that a bunch of my comments for the require.NoError(..) is similar to what david commented so my apologies for that :P

@@ -71,42 +142,12 @@ func (s *Store) CreateAuthorization(ctx context.Context, tx kv.Tx, a *influxdb.A
a.ID = id
}

// Token must be unique to create authorization.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If s.uniqueAuthToken fails should the mutation of a.HashedToken = hashedToken be reverted/rolled back at all? https://github.com/influxdata/EAR/issues/5819#issuecomment-2686492432

return string(got) == token
}
if len(allHashes) > 0 {
if got, _, _, err := jsonparser.Get(value, "hashedToken"); err == nil {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should you check if err is not nil? It appears that this method will throw errors for a few different reasons. It may be good to add a safety check if err != nil.

allHashes, err := s.hasher.AllHashes(token)
if err != nil {
s.log.Error("error generating hashes in filterPredicateFn", zap.Error(err))
// On error, continue onward. allHashes is empty and we'll effectively ignore hashedToken,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

Copy link

@devanbenz devanbenz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding in a few more comments

Bucket: "holder",
RetentionPeriodSeconds: 1,
})
require.NoError(t, err)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Context string for NoError

if err != nil {
t.Fatal(err)
}
require.NoError(t, err)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

context string for NoError

t.Fatal(err)
}
err = svc.CreateOrganization(context.Background(), otherOrg)
require.NoError(t, err)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

context string for NoError

t.Fatal(err)
}
err := all.Up(context.Background(), zaptest.NewLogger(t), store)
require.NoError(t, err)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Context string for NoError

t.Fatal(err)
}
authStore, err := authorization.NewStore(context.Background(), store, useHashedTokens)
require.NoError(t, err)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Context string for NoError

ctx := context.Background()
store := bolt.NewKVStore(cmd.logger.With(zap.String("system", "bolt-kvstore")), cmd.boltPath)
if err := store.Open(ctx); err != nil {
return err
}
defer store.Close()
defer func() {
rErr = errors.Join(store.Close(), rErr)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For these defers can errors2.Capture be used?
see

defer errors2.Capture(&rErr, dir.Close)()

if t.Token != "" {
token = t.Token
} else if t.HashedToken != "" {
token = "REDACTED"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could "N/A" and "REDACTED" be Enums or consts?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be added? I assume yes because its post fixed with testdata, but, just checking.


for _, want := range tc.want {
actual, err := v2.authSvc.FindAuthorizationByToken(ctx, want.Token)
require.NoError(t, err)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add string context to NoError

}
if diff := cmp.Diff(want.Status, actual.Status); diff != "" {
t.Fatal(diff)
require.NoError(t, err)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String context to NoError

devanbenz
devanbenz previously approved these changes Feb 28, 2025
Copy link

@devanbenz devanbenz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going to pick back up my review next week for a second pass. Consider this first pass finished up.

t.Run(tc.name, func(t *testing.T) { // better do not run in parallel
ctx := context.Background()
log := zaptest.NewLogger(t)
for _, useHashedTokens := range []bool{false, true} {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add strings with context to NoError's in this function.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also did not mean to approve. Dismissing my approval.

@devanbenz devanbenz self-requested a review February 28, 2025 19:00
@devanbenz devanbenz dismissed their stale review February 28, 2025 19:00

Dismissing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/2.x OSS 2.0 related issues and PRs kind/feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants