Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
342023b
feat: add optional token hashing
gwossum Feb 6, 2025
f45372d
chore: rollback changes to v1/authorization
gwossum Feb 6, 2025
226b492
chore: fix static check issues, take advantage of go1.23 feature
gwossum Feb 6, 2025
32ff23b
chore: replace deprecated call
gwossum Feb 6, 2025
2cb148b
fix: prevent duplicate tokens from being created
gwossum Feb 11, 2025
66d58d5
fix: allow token lookups by all hashing algorithms used in store
gwossum Feb 13, 2025
c40e0cf
chore: variety of minor fixes
gwossum Feb 14, 2025
997e38f
chore: improve comments, error handling, logging, and edge cases
gwossum Feb 24, 2025
d530327
chore: fix spelling and grammar mistakes, remove dead code
gwossum Feb 25, 2025
eaa140d
chore: address comments from PR
gwossum Jul 9, 2025
0b61c32
chore: add tests for misuses of NewAuthorizationHasher
gwossum Jul 14, 2025
c06e7bb
chore: turn some user-facing string literals into constants
gwossum Jul 14, 2025
5494e24
chore: add context to test cases
gwossum Jul 15, 2025
d473f24
chore: update outdated comment
gwossum Aug 25, 2025
ad62021
fix: remove timing attack when comparing raw tokens
gwossum Aug 25, 2025
a6b9cb9
chore: code improvements
gwossum Aug 27, 2025
18ec84a
chore: fix flaky test
gwossum Nov 19, 2025
339ee6d
chore: address PR comments
gwossum Nov 19, 2025
5988c96
fix: use constant time comparison when checking for unhashed tokens
gwossum Nov 19, 2025
5e88aff
feat: add logging if hashing disabled but hashed tokens found
gwossum Nov 19, 2025
19a4057
chore: abstract checking if tokens are clear or set for readability
gwossum Nov 20, 2025
d1b3e1b
chore: updates based on PR comments
gwossum Nov 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 55 additions & 1 deletion auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ var ErrUnableToCreateToken = &errors.Error{
// Authorization is an authorization. 🎉
type Authorization struct {
ID platform.ID `json:"id"`
Token string `json:"token"`
Token string `json:"token,omitempty"`
HashedToken string `json:"hashedToken,omitempty"`
Status Status `json:"status"`
Description string `json:"description"`
OrgID platform.ID `json:"orgID"`
Expand All @@ -35,7 +36,60 @@ type AuthorizationUpdate struct {
Description *string `json:"description,omitempty"`
}

const (
// authTokenClearValue is used to indicate Token or HashedToken are cleared (not set).
authTokenClearValue = ""
)

// IsAuthTokenSet returns true if token is considered set. Applies
// to be both unhashed tokens (Authorization.Token) and
// hashed tokens (Authorization.HashedToken).
func IsAuthTokenSet(token string) bool {
return token != authTokenClearValue
}

// IsTokenSet returns true if Token is set.
func (a *Authorization) IsTokenSet() bool {
return IsAuthTokenSet(a.Token)
}

// IsTokenClear returns true if Token is unset.
func (a *Authorization) IsTokenClear() bool {
return !a.IsTokenSet()
}

// ClearToken clears Token.
func (a *Authorization) ClearToken() {
a.Token = authTokenClearValue
}

// IsHashedTokenSet returns true if HashedToken is set.
func (a *Authorization) IsHashedTokenSet() bool {
return IsAuthTokenSet(a.HashedToken)
}

// IsHashedTokenClear returns true if Token is unset.
func (a *Authorization) IsHashedTokenClear() bool {
return !a.IsHashedTokenSet()
}

// ClearToken clears HashedToken.
func (a *Authorization) ClearHashedToken() {
a.HashedToken = authTokenClearValue
}

// NoTokensSet returns true if neither Token nor HashedToken is set.
func (a *Authorization) NoTokensSet() bool {
return a.IsTokenClear() && a.IsHashedTokenClear()
}

// BothTokensSet returns true if both Token and Hashed token is set.
func (a *Authorization) BothTokensSet() bool {
return a.IsTokenSet() && a.IsHashedTokenSet()
}

// Valid ensures that the authorization is valid.
// Valid does not check if tokens are set properly.
func (a *Authorization) Valid() error {
for _, p := range a.Permissions {
if p.Resource.OrgID != nil && *p.Resource.OrgID != a.OrgID {
Expand Down
6 changes: 3 additions & 3 deletions authorization/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,12 @@ func ErrInvalidAuthIDError(err error) *errors.Error {
}
}

// UnexpectedAuthIndexError is used when the error comes from an internal system.
func UnexpectedAuthIndexError(err error) *errors.Error {
// UnexpectedAuthBucketError is used when the error comes from an internal system.
func UnexpectedAuthBucketError(index []byte, err error) *errors.Error {
var e *errors.Error
if !errors2.As(err, &e) {
e = &errors.Error{
Msg: fmt.Sprintf("unexpected error retrieving auth index; Err: %v", err),
Msg: fmt.Sprintf("unexpected error retrieving auth bucket %q; Err: %v", index, err),
Code: errors.EInternal,
Err: err,
}
Expand Down
148 changes: 148 additions & 0 deletions authorization/hasher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package authorization

import (
"errors"
"fmt"

"github.com/go-crypt/crypt"
"github.com/go-crypt/crypt/algorithm"
influxdb2_algo "github.com/influxdata/influxdb/v2/pkg/crypt/algorithm/influxdb2"
)

var (
ErrNoDecoders = errors.New("no authorization decoders specified")
)

type AuthorizationHasher struct {
// hasher encodes tokens into hashed PHC-encoded tokens.
hasher algorithm.Hash

// decoder decodes hashed PHC-encoded tokens into crypt.Digest objects.
decoder *crypt.Decoder

// allHashers is the list of all hashers which could be used for hashed index lookup.
allHashers []algorithm.Hash
}

const (
DefaultHashVariant = influxdb2_algo.VariantSHA256
DefaultHashVariantName = influxdb2_algo.VariantIdentifierSHA256

// HashVariantNameUnknown is the placeholder name used for unknown or unsupported hash variants.
HashVariantNameUnknown = "N/A"
)

type authorizationHasherOptions struct {
hasherVariant influxdb2_algo.Variant
decoderVariants []influxdb2_algo.Variant
}

type AuthorizationHasherOption func(o *authorizationHasherOptions)

func WithHasherVariant(variant influxdb2_algo.Variant) AuthorizationHasherOption {
return func(o *authorizationHasherOptions) {
o.hasherVariant = variant
}
}

func WithDecoderVariants(variants []influxdb2_algo.Variant) AuthorizationHasherOption {
return func(o *authorizationHasherOptions) {
o.decoderVariants = variants
}
}

// NewAuthorizationHasher creates an AuthorizationHasher for influxdb2 algorithm hashed tokens.
// variantName specifies which token hashing variant to use, with blank indicating to use the default
// hashing variant. By default, all variants of the influxdb2 hashing scheme are supported for
// maximal compatibility.
func NewAuthorizationHasher(opts ...AuthorizationHasherOption) (*AuthorizationHasher, error) {
options := authorizationHasherOptions{
hasherVariant: DefaultHashVariant,
decoderVariants: influxdb2_algo.AllVariants,
}

for _, o := range opts {
o(&options)
}

if len(options.decoderVariants) == 0 {
return nil, fmt.Errorf("error in NewAuthorizationHasher: %w", ErrNoDecoders)
}

// 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 %s for AuthorizationHasher: %w", options.hasherVariant.Prefix(), err)
}

// Create decoder and register all requested decoder variants.
decoder := crypt.NewDecoder()
for _, variant := range options.decoderVariants {
if err := variant.RegisterDecoder(decoder); err != nil {
return nil, fmt.Errorf("registering variant %s with decoder: %w", variant.Prefix(), err)
}
}

// Create all variant hashers needed for requested decoder variants. This is required for operations where
// all potential variations of a raw token must be hashed, such as looking up a hash in the hashed token index.
var allHashers []algorithm.Hash
for _, variant := range options.decoderVariants {
h, err := influxdb2_algo.New(influxdb2_algo.WithVariant(variant))
if err != nil {
return nil, fmt.Errorf("creating hasher %s for authorization service index lookups: %w", variant.Prefix(), err)
}
allHashers = append(allHashers, h)
}

return &AuthorizationHasher{
hasher: hasher,
decoder: decoder,
allHashers: allHashers,
}, nil
}

// Hash generates a PHC-encoded hash of token using the selected hash algorithm variant.
func (h *AuthorizationHasher) Hash(token string) (string, error) {
digest, err := h.hasher.Hash(token)
if err != nil {
return "", fmt.Errorf("hashing raw token failed: %w", err)
}
return digest.Encode(), nil
}

// 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 {
digest, err := hasher.Hash(token)
if err != nil {
variantName := HashVariantNameUnknown
if influxdb_hasher, ok := hasher.(*influxdb2_algo.Hasher); ok {
variantName = influxdb_hasher.Variant().Prefix()
}
return nil, fmt.Errorf("hashing raw token failed (variant=%s): %w", variantName, err)
}
hashes[idx] = digest.Encode()
}
return hashes, nil
}

// AllHashesCount returns the number of hash variants available through AllHashes.
func (h *AuthorizationHasher) AllHashesCount() int {
return len(h.allHashers)
}

// Decode decodes a PHC-encoded hash into a Digest object that can be matched.
func (h *AuthorizationHasher) Decode(phc string) (algorithm.Digest, error) {
return h.decoder.Decode(phc)
}

// Match determines if a raw token matches a PHC-encoded token.
func (h *AuthorizationHasher) Match(phc string, token string) (bool, error) {
digest, err := h.Decode(phc)
if err != nil {
return false, err
}

return digest.MatchAdvanced(token)
}
45 changes: 45 additions & 0 deletions authorization/hasher_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package authorization_test

import (
"testing"

"github.com/go-crypt/crypt/algorithm"
"github.com/influxdata/influxdb/v2/authorization"
influxdb2_algo "github.com/influxdata/influxdb/v2/pkg/crypt/algorithm/influxdb2"
"github.com/stretchr/testify/require"
)

func Test_NewAuthorizationHasher_EmptyDecoderVariants(t *testing.T) {
hasher, err := authorization.NewAuthorizationHasher(
authorization.WithDecoderVariants([]influxdb2_algo.Variant{}),
)

require.ErrorIs(t, err, authorization.ErrNoDecoders)
require.Nil(t, hasher)
}

func TestNewAuthorizationHasher_WithInvalidDecoderVariant(t *testing.T) {
// Test that using an invalid decoder variant returns an error
hasher, err := authorization.NewAuthorizationHasher(
authorization.WithDecoderVariants([]influxdb2_algo.Variant{
influxdb2_algo.Variant(-1), // Invalid variant
}),
)

// Should return an error and nil hasher
require.ErrorIs(t, err, algorithm.ErrParameterInvalid)
require.Contains(t, err.Error(), "registering variant")
require.Nil(t, hasher)
}

func TestNewAuthorizationHasher_WithHasherVariantInvalid(t *testing.T) {
// Test that using VariantNone returns an error
hasher, err := authorization.NewAuthorizationHasher(
authorization.WithHasherVariant(influxdb2_algo.Variant(-1)),
)

// Should return an error and nil hasher
require.ErrorIs(t, err, algorithm.ErrParameterInvalid)
require.ErrorContains(t, err, "creating hasher")
require.Nil(t, hasher)
}
Loading