Skip to content

Commit

Permalink
Moving hollow to rivets
Browse files Browse the repository at this point in the history
  • Loading branch information
jakeschuurmans committed Sep 13, 2024
1 parent 8f6e267 commit bb74c50
Show file tree
Hide file tree
Showing 23 changed files with 2,905 additions and 3 deletions.
2 changes: 2 additions & 0 deletions ginauth/doc.go
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
105 changes: 105 additions & 0 deletions ginauth/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
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:err113
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,
// nolint:goerr113
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:err113
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)
}
20 changes: 20 additions & 0 deletions ginauth/genericmiddleware.go
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)
}
25 changes: 25 additions & 0 deletions ginauth/middleware.go
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()})
}
}
100 changes: 100 additions & 0 deletions ginauth/multitokenmiddleware.go
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)
}
}
}
Loading

0 comments on commit bb74c50

Please sign in to comment.