mirror of
https://github.com/hanzo-fi/stack.git
synced 2026-10-30 20:34:43 +00:00
123 lines
3.5 KiB
Go
123 lines
3.5 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/formancehq/stack/libs/go-libs/collectionutils"
|
|
"github.com/formancehq/stack/libs/go-libs/logging"
|
|
"github.com/hashicorp/go-retryablehttp"
|
|
"github.com/zitadel/oidc/v2/pkg/client/rp"
|
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
|
"github.com/zitadel/oidc/v2/pkg/op"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
type jwtAuth struct {
|
|
logger logging.Logger
|
|
httpClient *http.Client
|
|
accessTokenVerifier op.AccessTokenVerifier
|
|
|
|
issuer string
|
|
checkScopes bool
|
|
service string
|
|
}
|
|
|
|
func newOtlpHttpClient(maxRetries int) *http.Client {
|
|
retryClient := retryablehttp.NewClient()
|
|
retryClient.RetryMax = maxRetries
|
|
return retryClient.StandardClient()
|
|
}
|
|
|
|
func newJWTAuth(
|
|
logger logging.Logger,
|
|
readKeySetMaxRetries int,
|
|
issuer string,
|
|
service string,
|
|
checkScopes bool,
|
|
) *jwtAuth {
|
|
return &jwtAuth{
|
|
logger: logger,
|
|
httpClient: newOtlpHttpClient(readKeySetMaxRetries),
|
|
accessTokenVerifier: nil,
|
|
issuer: issuer,
|
|
checkScopes: checkScopes,
|
|
service: service,
|
|
}
|
|
}
|
|
|
|
// Authenticate validates the JWT in the request and returns the user, if valid.
|
|
func (ja *jwtAuth) Authenticate(w http.ResponseWriter, r *http.Request) (bool, error) {
|
|
authHeader := r.Header.Get("authorization")
|
|
if authHeader == "" {
|
|
ja.logger.Error("no authorization header")
|
|
return false, fmt.Errorf("no authorization header")
|
|
}
|
|
|
|
if !strings.HasPrefix(authHeader, strings.ToLower(oidc.PrefixBearer)) &&
|
|
!strings.HasPrefix(authHeader, oidc.PrefixBearer) {
|
|
ja.logger.Error("malformed authorization header")
|
|
return false, fmt.Errorf("malformed authorization header")
|
|
}
|
|
|
|
token := strings.TrimPrefix(authHeader, strings.ToLower(oidc.PrefixBearer))
|
|
token = strings.TrimPrefix(token, oidc.PrefixBearer)
|
|
|
|
accessTokenVerifier, err := ja.getAccessTokenVerifier(r.Context())
|
|
if err != nil {
|
|
ja.logger.Error("unable to create access token verifier", zap.Error(err))
|
|
return false, fmt.Errorf("unable to create access token verifier: %w", err)
|
|
}
|
|
|
|
claims, err := op.VerifyAccessToken[*oidc.AccessTokenClaims](r.Context(), token, accessTokenVerifier)
|
|
if err != nil {
|
|
ja.logger.Error("unable to verify access token", zap.Error(err))
|
|
return false, fmt.Errorf("unable to verify access token: %w", err)
|
|
}
|
|
|
|
if ja.checkScopes {
|
|
scope := claims.Scopes
|
|
|
|
allowed := true
|
|
switch r.Method {
|
|
case http.MethodOptions, http.MethodGet, http.MethodHead, http.MethodTrace:
|
|
allowed = allowed && (collectionutils.Contains(scope, ja.service+":read") || collectionutils.Contains(scope, ja.service+":write"))
|
|
default:
|
|
allowed = allowed && collectionutils.Contains(scope, ja.service+":write")
|
|
}
|
|
|
|
if !allowed {
|
|
ja.logger.Info("not enough scopes")
|
|
return false, fmt.Errorf("missing access, found scopes: '%s' need %s:read|write", strings.Join(scope, ", "), ja.service)
|
|
}
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (ja *jwtAuth) getAccessTokenVerifier(ctx context.Context) (op.AccessTokenVerifier, error) {
|
|
if ja.accessTokenVerifier == nil {
|
|
//discoveryConfiguration, err := client.Discover(ja.Issuer, ja.httpClient)
|
|
//if err != nil {
|
|
// return nil, err
|
|
//}
|
|
|
|
// todo: ugly quick fix
|
|
authServicePort := "8080"
|
|
if fromEnv := os.Getenv("AUTH_SERVICE_PORT"); fromEnv != "" {
|
|
authServicePort = fromEnv
|
|
}
|
|
keySet := rp.NewRemoteKeySet(ja.httpClient, fmt.Sprintf("http://auth:%s/keys", authServicePort))
|
|
|
|
ja.accessTokenVerifier = op.NewAccessTokenVerifier(
|
|
os.Getenv("STACK_PUBLIC_URL")+"/api/auth",
|
|
keySet,
|
|
)
|
|
}
|
|
|
|
return ja.accessTokenVerifier, nil
|
|
}
|