-
Notifications
You must be signed in to change notification settings - Fork 1
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
Moving hollow to rivets #95
Merged
Merged
Changes from 3 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess we have long tests? Can you list them and drop a KTLO Jira for them so we don't lose track. Having long tests means people won't run them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mhmm, i think i might not need that anymore. I was seeing something weird and added the 2m. I can now run it without the 2m. Ill revert that.