-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Moving hollow to rivets * lint fixes, change fleetdb reference to main * fix github workflow * remove 2m timeout
- Loading branch information
1 parent
8f6e267
commit 6a13f25
Showing
27 changed files
with
3,019 additions
and
157 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,6 +21,8 @@ jobs: | |
|
||
- name: Test | ||
run: go test ./... | ||
with: | ||
args: -tags testtools | ||
|
||
build: | ||
runs-on: ubuntu-latest | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
// Package ginauth provides a authentication and authorization middleware for use with a gin server | ||
package ginauth |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
package ginauth | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"net/http" | ||
) | ||
|
||
var ( | ||
// ErrInvalidMiddlewareReference the middleware added was invalid | ||
ErrInvalidMiddlewareReference = errors.New("invalid middleware") | ||
|
||
// ErrMiddlewareRemote is the error returned when the middleware couldn't contact the remote endpoint | ||
ErrMiddlewareRemote = errors.New("middleware setup") | ||
|
||
// ErrAuthentication defines a generic authentication error. This specifies that we couldn't | ||
// validate a token for some reason. This is not to be used as-is but is useful for type | ||
// comparison with the `AuthError` struct. | ||
ErrAuthentication = errors.New("authentication error") | ||
|
||
// ErrInvalidSigningKey is the error returned when a token can not be verified because the signing key in invalid | ||
// NOTE(jaosorior): The fact that this is in this package is a little hacky... but it's to not have a | ||
// circular dependency with the ginjwt package. | ||
ErrInvalidSigningKey = errors.New("invalid token signing key") | ||
) | ||
|
||
// AuthError represents an auth error coming from a middleware function | ||
type AuthError struct { | ||
HTTPErrorCode int | ||
err error | ||
} | ||
|
||
// NewAuthenticationError returns an authentication error which is due | ||
// to not being able to determine who's the requestor (e.g. authentication error) | ||
func NewAuthenticationError(msg string) *AuthError { | ||
return &AuthError{ | ||
HTTPErrorCode: http.StatusUnauthorized, | ||
//nolint:goerr113 // it must be dynamic here | ||
err: errors.New(msg), | ||
} | ||
} | ||
|
||
// NewAuthenticationErrorFrom returns an authentication error which is due | ||
// to not being able to determine who's the requestor (e.g. authentication error). | ||
// The error is based on another one (it wraps it). | ||
func NewAuthenticationErrorFrom(err error) *AuthError { | ||
return &AuthError{ | ||
HTTPErrorCode: http.StatusUnauthorized, | ||
err: err, | ||
} | ||
} | ||
|
||
// NewAuthorizationError returns an authorization error which is due to | ||
// not being able to determine what the requestor can do (e.g. authorization error) | ||
func NewAuthorizationError(msg string) *AuthError { | ||
return &AuthError{ | ||
HTTPErrorCode: http.StatusForbidden, | ||
//nolint:goerr113 // it must be dynamic here | ||
err: errors.New(msg), | ||
} | ||
} | ||
|
||
// Error ensures AuthenticationError implements the error interface | ||
func (ae *AuthError) Error() string { | ||
return ae.err.Error() | ||
} | ||
|
||
// Unwrap ensures that we're able to verify that this is indeed | ||
// an authentication error | ||
func (ae *AuthError) Unwrap() error { | ||
return ErrAuthentication | ||
} | ||
|
||
// TokenValidationError specifies that there was an authentication error | ||
// due to the token being invalid | ||
type TokenValidationError struct { | ||
AuthError | ||
} | ||
|
||
// Error ensures AuthenticationError implements the error interface | ||
func (tve *TokenValidationError) Error() string { | ||
return fmt.Sprintf("invalid auth token: %s", &tve.AuthError) | ||
} | ||
|
||
// Unwrap allows TokenValidationError to be detected as an AuthError. | ||
func (tve *TokenValidationError) Unwrap() error { | ||
return &tve.AuthError | ||
} | ||
|
||
// NewTokenValidationError returns a TokenValidationError that wraps the given error | ||
func NewTokenValidationError(err error) error { | ||
return &TokenValidationError{ | ||
AuthError: AuthError{ | ||
HTTPErrorCode: http.StatusUnauthorized, | ||
err: err, | ||
}, | ||
} | ||
} | ||
|
||
// NewInvalidSigningKeyError returns an AuthError that indicates | ||
// that the signing key used to validate the token was not valid | ||
func NewInvalidSigningKeyError() error { | ||
return NewAuthenticationErrorFrom(ErrInvalidSigningKey) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package ginauth | ||
|
||
import ( | ||
"github.com/gin-gonic/gin" | ||
) | ||
|
||
// ClaimMetadata returns the minimal relevant information so middleware | ||
// can set the appropriate metadata to a context (e.g. a gin.Context) | ||
type ClaimMetadata struct { | ||
Subject string | ||
User string | ||
Roles []string | ||
} | ||
|
||
// GenericAuthMiddleware defines middleware that verifies a token coming from a gin.Context. | ||
// Note that this can be stacked together using the MultiTokenMiddleware construct. | ||
type GenericAuthMiddleware interface { | ||
VerifyTokenWithScopes(*gin.Context, []string) (ClaimMetadata, error) | ||
SetMetadata(*gin.Context, ClaimMetadata) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package ginauth | ||
|
||
import ( | ||
"errors" | ||
"net/http" | ||
|
||
"github.com/gin-gonic/gin" | ||
) | ||
|
||
// AbortBecauseOfError aborts a gin context based on a given error | ||
func AbortBecauseOfError(c *gin.Context, err error) { | ||
var authErr *AuthError | ||
|
||
var validationErr *TokenValidationError | ||
|
||
switch { | ||
case errors.As(err, &validationErr): | ||
c.AbortWithStatusJSON(validationErr.HTTPErrorCode, gin.H{"message": "invalid auth token", "error": validationErr.Error()}) | ||
case errors.As(err, &authErr): | ||
c.AbortWithStatusJSON(authErr.HTTPErrorCode, gin.H{"message": authErr.Error()}) | ||
default: | ||
// If we can't cast it, unauthorize anyway | ||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": err.Error()}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
package ginauth | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"sync" | ||
|
||
"github.com/gin-gonic/gin" | ||
) | ||
|
||
// MultiTokenMiddleware Allows for concurrently verifying a token | ||
// using different middleware implementations. This relies on implementing | ||
// the GenericAuthMiddleware interface. | ||
// Only the first detected success will be taken into account. | ||
// Note that middleware objects don't have to be of Middleware type, that's | ||
// only one object that implements the interface. | ||
type MultiTokenMiddleware struct { | ||
verifiers []GenericAuthMiddleware | ||
} | ||
|
||
// NewMultiTokenMiddleware builds a MultiTokenMiddleware object from multiple AuthConfigs. | ||
func NewMultiTokenMiddleware() (*MultiTokenMiddleware, error) { | ||
mtm := &MultiTokenMiddleware{} | ||
mtm.verifiers = make([]GenericAuthMiddleware, 0) | ||
|
||
return mtm, nil | ||
} | ||
|
||
// Add will append another middleware object (or verifier) to the list | ||
// which we'll use to check concurrently | ||
func (mtm *MultiTokenMiddleware) Add(middleware GenericAuthMiddleware) error { | ||
if middleware == nil { | ||
return fmt.Errorf("%w: %s", ErrInvalidMiddlewareReference, "The middleware reference can't be nil") | ||
} | ||
|
||
mtm.verifiers = append(mtm.verifiers, middleware) | ||
|
||
return nil | ||
} | ||
|
||
// AuthRequired is similar to the `AuthRequired` function from the Middleware type | ||
// in the sense that it'll evaluate the scopes and the token coming from the context. | ||
// However, this will concurrently evaluate them with the middlewares configured in this | ||
// struct | ||
func (mtm *MultiTokenMiddleware) AuthRequired(scopes []string) gin.HandlerFunc { | ||
return func(c *gin.Context) { | ||
var wg sync.WaitGroup | ||
|
||
res := make(chan error, len(mtm.verifiers)) | ||
|
||
wg.Add(len(mtm.verifiers)) | ||
|
||
for _, verifier := range mtm.verifiers { | ||
go func(v GenericAuthMiddleware, c *gin.Context, r chan<- error) { | ||
defer wg.Done() | ||
|
||
cm, err := v.VerifyTokenWithScopes(c, scopes) | ||
|
||
if err != nil { | ||
v.SetMetadata(c, cm) | ||
} | ||
|
||
r <- err | ||
}(verifier, c, res) | ||
} | ||
|
||
wg.Wait() | ||
close(res) | ||
|
||
var surfacingErr error | ||
|
||
for err := range res { | ||
if err == nil { | ||
// NOTE(jaosorior): This takes the first non-error as a success. | ||
// It would be quite strange if we would get multiple successes. | ||
return | ||
} | ||
|
||
// initialize surfacingErr. | ||
if surfacingErr == nil { | ||
surfacingErr = err | ||
continue | ||
} | ||
|
||
// If we previously had an error related to having an invalid signing key | ||
// we overwrite the error to be surfaced. We care more about other types of | ||
// errors, such as not having the appropriate scope | ||
// Also, if we previously had an error with the remote endpoint, we override the error. | ||
// This might be a very general error and more specific ones are preferred | ||
// for surfacing. | ||
if errors.Is(surfacingErr, ErrMiddlewareRemote) || errors.Is(surfacingErr, ErrInvalidSigningKey) { | ||
surfacingErr = err | ||
} | ||
} | ||
|
||
if surfacingErr != nil { | ||
AbortBecauseOfError(c, surfacingErr) | ||
} | ||
} | ||
} |
Oops, something went wrong.