snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s

This commit is contained in:
upstream-mirror
2026-09-18 01:22:23 +05:00
commit 7e385fc126
8391 changed files with 1766236 additions and 0 deletions

40
server/pkg/auth/errors.go Normal file
View File

@@ -0,0 +1,40 @@
package auth
import (
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/cortezaproject/corteza/server/pkg/locale"
)
func errUnauthorized() error {
return errors.New(
errors.KindUnauthorized,
"unauthorized",
errors.Meta("type", "unauthorized"),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "internal"),
errors.Meta(locale.ErrorMetaKey{}, "auth.errors.unauthorized"),
errors.StackSkip(1),
errors.StackTrimAtFn("http.HandlerFunc.ServeHTTP"),
)
}
func errUnauthorizedScope() error {
return errors.New(
errors.KindUnauthorized,
"unauthorized scope",
errors.Meta("type", "unauthorizedScope"),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "internal"),
errors.Meta(locale.ErrorMetaKey{}, "auth.errors.unauthorizedScope"),
errors.StackSkip(1),
errors.StackTrimAtFn("http.HandlerFunc.ServeHTTP"),
)
}

31
server/pkg/auth/extra.go Normal file
View File

@@ -0,0 +1,31 @@
package auth
import (
"context"
"net/http"
)
type (
ExtraReqInfo struct {
RemoteAddr string
UserAgent string
}
)
func ExtraReqInfoMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), ExtraReqInfo{}, ExtraReqInfo{
RemoteAddr: r.RemoteAddr,
UserAgent: r.UserAgent(),
})))
})
}
func GetExtraReqInfoFromContext(ctx context.Context) ExtraReqInfo {
eti := ctx.Value(ExtraReqInfo{})
if eti != nil {
return eti.(ExtraReqInfo)
} else {
return ExtraReqInfo{}
}
}

View File

@@ -0,0 +1,60 @@
package auth
import (
"fmt"
"strconv"
"strings"
)
type (
identity struct {
id uint64
memberOf []uint64
}
)
// Anonymous constructs and returns new anonymous identity with system anonymous roles
func Anonymous() *identity {
return &identity{
memberOf: AnonymousRoles().IDs(),
}
}
// Authenticated constructs and returns new authenticated identity with assigned roles + system authenticated roles
func Authenticated(id uint64, rr ...uint64) *identity {
return &identity{
id: id,
memberOf: append(rr, AuthenticatedRoles().IDs()...),
}
}
func (i identity) Identity() uint64 {
return i.id
}
func (i identity) Roles() []uint64 {
return i.memberOf
}
func (i identity) Valid() bool {
return i.id > 0
}
func (i identity) String() string {
return fmt.Sprintf("%d", i.Identity())
}
func ExtractFromSubClaim(sub string) (userID uint64, rr []uint64) {
parts := strings.Split(sub, " ")
rr = make([]uint64, len(parts)-1)
for p := range parts {
id, _ := strconv.ParseUint(parts[p], 10, 64)
if p == 0 {
userID = id
} else {
rr[p-1] = id
}
}
return
}

View File

@@ -0,0 +1,32 @@
package auth
import (
"context"
)
type (
identityCtxKey struct{}
)
func SetIdentityToContext(ctx context.Context, identity Identifiable) context.Context {
return context.WithValue(ctx, identityCtxKey{}, identity)
}
// GetIdentityFromContext always returns identity, either valid or anonymous
//
// For anonymous user, it auto appends all anonymous defined on the system
func GetIdentityFromContext(ctx context.Context) Identifiable {
if i := GetIdentityFromContextWithKey(ctx, identityCtxKey{}); i != nil && i.Valid() {
return i
} else {
return Anonymous()
}
}
func GetIdentityFromContextWithKey(ctx context.Context, key interface{}) Identifiable {
if i, ok := ctx.Value(key).(Identifiable); ok {
return i
} else {
return nil
}
}

View File

@@ -0,0 +1,26 @@
package auth
type (
Identifiable interface {
Identity() uint64
Roles() []uint64
Valid() bool
String() string
}
//TokenGenerator interface {
// Encode(i Identifiable, clientID uint64, scope ...string) (token []byte, err error)
// Generate(ctx context.Context, i Identifiable, clientID uint64, scope ...string) (token []byte, err error)
//}
//TokenHandler interface {
// TokenGenerator
// HttpVerifier() func(http.Handler) http.Handler
// HttpAuthenticator() func(http.Handler) http.Handler
//}
Signer interface {
Sign(userID uint64, pp ...interface{}) string
Verify(signature string, userID uint64, pp ...interface{}) bool
}
)

View File

@@ -0,0 +1,41 @@
package auth
import (
"sort"
"github.com/cortezaproject/corteza/server/pkg/slice"
)
// ApplyRoleSecurity takes role security params (set of permitted, prohibited and forced roles)
// and applies these rules to the set of given roles
//
// Filtered set of roles is returned
//
// String slices are used intentionally, because of the data source used
func ApplyRoleSecurity(permitted, prohibited, forced []uint64, rr ...uint64) (out []uint64) {
var (
mPermitted = slice.ToUint64BoolMap(permitted)
mProhibited = slice.ToUint64BoolMap(prohibited)
mForced = slice.ToUint64BoolMap(forced)
)
// iterate over user's roles and just append them (obeying allow&deny rules)
// to list of mForced roles
for _, r := range rr {
if (len(mPermitted) == 0 || mPermitted[r]) && !mProhibited[r] {
mForced[r] = true
}
}
out = make([]uint64, 0, len(mForced))
for forcedRoleID := range mForced {
out = append(out, forcedRoleID)
}
// for stable output
sort.Slice(out, func(i, j int) bool {
return out[i] < out[j]
})
return
}

View File

@@ -0,0 +1,56 @@
package auth
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestApplyRoleSecurity(t *testing.T) {
tests := []struct {
name string
permitted []uint64
prohibited []uint64
forced []uint64
roles []uint64
wantOut []uint64
}{
{
"empty",
[]uint64{},
[]uint64{},
[]uint64{},
[]uint64{},
[]uint64{},
},
{
"nil",
nil,
nil,
nil,
nil,
[]uint64{},
},
{
"one",
[]uint64{1},
[]uint64{2},
[]uint64{3},
[]uint64{1, 2},
[]uint64{1, 3},
},
{
"forced only",
[]uint64{},
[]uint64{},
[]uint64{3, 2, 1},
[]uint64{2},
[]uint64{1, 2, 3},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.wantOut, ApplyRoleSecurity(tt.permitted, tt.prohibited, tt.forced, tt.roles...))
})
}
}

45
server/pkg/auth/scope.go Normal file
View File

@@ -0,0 +1,45 @@
package auth
import (
"strings"
"github.com/lestrrat-go/jwx/jwt"
)
const (
scopeDelimiter = " "
)
// CheckJwtScope verifies if required scope is in claim
// We're using interface{} and casting it if needed to simplify usage of the fn by directly
// using it with map[string]interface{} claims type
func CheckJwtScope(token jwt.Token, required ...string) bool {
if token == nil {
return false
}
scopeClaimRaw, has := token.Get("scope")
if !has {
return false
}
scopeClaim, ok := scopeClaimRaw.(string)
if !ok {
return false
}
return CheckScope(scopeClaim, required...)
}
func CheckScope(scope string, required ...string) bool {
scope = scopeDelimiter + strings.TrimSpace(scope) + scopeDelimiter
for _, req := range required {
req = scopeDelimiter + strings.TrimSpace(req) + scopeDelimiter
if strings.Contains(scope, req) {
return true
}
}
return false
}

48
server/pkg/auth/signer.go Normal file
View File

@@ -0,0 +1,48 @@
package auth
import (
"crypto/hmac"
"crypto/sha1"
"encoding/hex"
"fmt"
)
type (
hmacSigner struct {
secret []byte
}
)
const (
hmacSumStringLength = 40
)
var (
DefaultSigner Signer
)
func SetupSigner(secret string) (err error) {
DefaultSigner = HmacSigner(secret)
return
}
func HmacSigner(secret string) *hmacSigner {
return &hmacSigner{
secret: []byte(secret),
}
}
func (s hmacSigner) Sign(userID uint64, pp ...interface{}) string {
h := hmac.New(sha1.New, s.secret)
fmt.Fprintf(h, "%d ", userID)
for _, part := range pp {
fmt.Fprintf(h, "%v ", part)
}
return hex.EncodeToString(h.Sum(nil))
}
func (s hmacSigner) Verify(signature string, userID uint64, pp ...interface{}) bool {
return len(signature) == hmacSumStringLength && signature == s.Sign(userID, pp...)
}

90
server/pkg/auth/system.go Normal file
View File

@@ -0,0 +1,90 @@
package auth
import (
"github.com/cortezaproject/corteza/server/system/types"
)
const (
ProvisionUserHandle = "corteza-provisioner"
ServiceUserHandle = "corteza-service"
FederationUserHandle = "corteza-federation"
BypassRoleHandle = "super-admin"
AuthenticatedRoleHandle = "authenticated"
AnonymousRoleHandle = "anonymous"
)
var (
provisionUser *types.User
serviceUser *types.User
federationUser *types.User
authenticatedRoles []*types.Role
anonymousRoles []*types.Role
bypassRoles []*types.Role
)
// SetSystemUsers takes list of users and sets/updates appropriate
// provision, service & fed users
//
// These are then accessed via special *User() fn.
func SetSystemUsers(uu types.UserSet, rr types.RoleSet) {
bpr := rr.FindByHandle(BypassRoleHandle)
for _, u := range uu {
switch u.Handle {
case ProvisionUserHandle:
provisionUser = u.Clone()
provisionUser.SetRoles(bpr.ID)
case ServiceUserHandle:
serviceUser = u.Clone()
serviceUser.SetRoles(bpr.ID)
case FederationUserHandle:
federationUser = u.Clone()
federationUser.SetRoles(bpr.ID)
}
}
}
func SetSystemRoles(rr types.RoleSet) {
for _, r := range rr {
switch r.Handle {
case BypassRoleHandle:
bypassRoles = append(bypassRoles, r.Clone())
case AuthenticatedRoleHandle:
authenticatedRoles = append(authenticatedRoles, r.Clone())
case AnonymousRoleHandle:
anonymousRoles = append(anonymousRoles, r.Clone())
}
}
}
// BypassRoles returns all bypass Roles
func BypassRoles() types.RoleSet {
return bypassRoles
}
// AuthenticatedRoles returns all authenticated Roles
func AuthenticatedRoles() types.RoleSet {
return authenticatedRoles
}
// AnonymousRoles returns all anonymous Roles
func AnonymousRoles() types.RoleSet {
return anonymousRoles
}
// ProvisionUser returns clone of system provision user
func ProvisionUser() *types.User {
return provisionUser.Clone()
}
// ServiceUser returns clone of system service user
func ServiceUser() *types.User {
return serviceUser.Clone()
}
// FederationUser returns clone of system federation user
func FederationUser() *types.User {
return federationUser.Clone()
}

View File

@@ -0,0 +1,394 @@
package auth
import (
"context"
"fmt"
"strconv"
"strings"
"time"
"github.com/cortezaproject/corteza/server/pkg/payload"
"github.com/go-oauth2/oauth2/v4"
"github.com/go-oauth2/oauth2/v4/generates"
"github.com/go-oauth2/oauth2/v4/models"
"github.com/lestrrat-go/jwx/jwa"
"github.com/lestrrat-go/jwx/jwk"
"github.com/lestrrat-go/jwx/jwt"
"github.com/spf13/cast"
)
type (
tokenIssuer struct {
defaultRequest *TokenRequest
// store issued tokens
store tokenIssuerStore
// lookup for issued tokens
lookup tokenIssuerLookup
// generator for issued tokens
generator tokenIssuerGenerator
// signer for issued tokens
signer tokenIssuerSigner
}
// IssuerOptFn modify toeknIssuer
IssuerOptFn func(*tokenIssuer) error
TokenRequest struct {
AccessToken string
RefreshToken string
Expiration time.Duration
Audience string
Issuer string
IssuedAt time.Time
ClientID uint64
UserID uint64
Roles []uint64
Scope []string
}
// IssueOptFn functions modify TokenRequest
IssueOptFn func(*TokenRequest) error
tokenIssuerStore func(context.Context, TokenRequest) error
tokenIssuerLookup func(context.Context, string) error
tokenIssuerGenerator func(context.Context, TokenRequest) (string, string, error)
tokenIssuerSigner func(token jwt.Token) ([]byte, error)
)
var (
TokenIssuer *tokenIssuer
// wrapper around time.Now() that will aid service testing
now = func() *time.Time {
c := time.Now().Truncate(time.Second)
return &c
}
)
// NewTokenIssuer initializes and returns new instance of JWT manager
func NewTokenIssuer(opt ...IssuerOptFn) (issuer *tokenIssuer, err error) {
issuer = &tokenIssuer{
defaultRequest: &TokenRequest{Issuer: "cortezaproject.org"},
store: func(ctx context.Context, request TokenRequest) error {
// elegantly handle unconfigured store
return fmt.Errorf("token issuer store not configured")
},
lookup: func(context.Context, string) error {
// elegantly handle unconfigured lookup
return fmt.Errorf("token issuer lookup not configured")
},
signer: func(token jwt.Token) ([]byte, error) {
// elegantly handle unconfigured signer
return nil, fmt.Errorf("token issuer signer not configured")
},
generator: DefaultAccessTokenGenerator,
}
return issuer, applyToIssuer(issuer, opt...)
}
// Issue issues new access token, stores it and returns signed JWT.
func (i *tokenIssuer) Issue(ctx context.Context, opt ...IssueOptFn) (_ []byte, err error) {
var req = i.newTokenRequest()
if err = req.apply(opt...); err != nil {
return
}
if len(req.AccessToken+req.RefreshToken) > 0 {
panic("can not issue new token with preset access and refresh tokens, " +
"this is most likely an implementation mistake")
}
if req.AccessToken, req.RefreshToken, err = i.generator(ctx, *req); err != nil {
return
}
if err = i.store(ctx, *req); err != nil {
return
}
return i.sign(req)
}
func (i *tokenIssuer) Sign(opt ...IssueOptFn) (_ []byte, err error) {
var req = i.newTokenRequest()
if err = req.apply(opt...); err != nil {
return
}
return i.sign(req)
}
func (i *tokenIssuer) sign(req *TokenRequest) ([]byte, error) {
if token, err := makeToken(req); err != nil {
return nil, err
} else {
return i.signer(token)
}
}
// Returns new token request and copies relevant values from the default token request on the issuer
func (i *tokenIssuer) newTokenRequest() *TokenRequest {
return &TokenRequest{
Issuer: i.defaultRequest.Issuer,
ClientID: i.defaultRequest.ClientID,
Expiration: i.defaultRequest.Expiration,
IssuedAt: *now(),
}
}
// Validate performs token validation by checking existence of access-token in the store
func (i *tokenIssuer) Validate(ctx context.Context, token jwt.Token) (err error) {
if err = i.lookup(ctx, token.JwtID()); err != nil {
return errUnauthorized()
}
return nil
}
func makeToken(req *TokenRequest) (_ jwt.Token, err error) {
var (
roles = make([]string, len(req.Roles))
token = jwt.New()
toString = func(i uint64) string {
return strconv.FormatUint(i, 10)
}
)
if len(req.Scope) == 0 {
// for backward compatibility we default
// unset scope to profile & api
req.Scope = []string{"profile", "api"}
}
if req.IssuedAt.IsZero() {
req.IssuedAt = *now()
}
for i, r := range req.Roles {
roles[i] = toString(r)
}
// The key part: store access token as JWT ID claim.
// Claim will be extracted when JWT is validated and checked
if err = token.Set(jwt.JwtIDKey, req.AccessToken); err != nil {
return
}
if err = token.Set(jwt.SubjectKey, toString(req.UserID)); err != nil {
return
}
if err = token.Set(jwt.ExpirationKey, now().Add(req.Expiration).Unix()); err != nil {
return
}
if req.Audience != "" {
if err = token.Set(jwt.AudienceKey, req.Audience); err != nil {
return
}
}
if err = token.Set(jwt.IssuerKey, req.Issuer); err != nil {
return
}
if err = token.Set(jwt.IssuedAtKey, req.IssuedAt.Unix()); err != nil {
return
}
if err = token.Set("clientID", toString(req.ClientID)); err != nil {
return
}
if err = token.Set("scope", strings.Join(req.Scope, " ")); err != nil {
return
}
if err = token.Set("roles", roles); err != nil {
return
}
return token, nil
}
// IdentityFromToken decodes sub & roles claims into identity
func IdentityFromToken(token jwt.Token) *identity {
if token == nil {
return Anonymous()
}
var (
roles, _ = token.Get("roles")
)
return Authenticated(
cast.ToUint64(token.Subject()),
payload.ParseUint64s(cast.ToStringSlice(roles))...,
)
}
// DefaultAccessTokenGenerator uses token generator from oauth2 lib
func DefaultAccessTokenGenerator(ctx context.Context, req TokenRequest) (string, string, error) {
return generates.NewAccessGenerate().Token(
ctx,
&oauth2.GenerateBasic{
Client: &models.Client{
ID: strconv.FormatUint(req.ClientID, 10),
},
UserID: strconv.FormatUint(req.UserID, 10),
CreateAt: *now(),
TokenInfo: nil,
Request: nil,
},
true,
)
}
// WithSecretSigner configures token issuer with
func WithSecretSigner(secret string) IssuerOptFn {
return func(tm *tokenIssuer) (err error) {
if len(secret) == 0 {
return fmt.Errorf("JWK missing")
}
var key jwk.Key
if key, err = jwk.New([]byte(secret)); err != nil {
return fmt.Errorf("could not parse JWK: %w", err)
}
tm.signer = func(token jwt.Token) ([]byte, error) {
return jwt.Sign(token, jwa.HS512, key)
}
return nil
}
}
// WithDefaultClientID configures ID of the default auth client
func WithDefaultClientID(ID uint64) IssuerOptFn {
return func(tm *tokenIssuer) (err error) {
tm.defaultRequest.ClientID = ID
return
}
}
// WithDefaultExpiration configures default token expiration time
func WithDefaultExpiration(exp time.Duration) IssuerOptFn {
return func(tm *tokenIssuer) (err error) {
tm.defaultRequest.Expiration = exp
return
}
}
// WithDefaultIssuer configures default issuer claim
func WithDefaultIssuer(iss string) IssuerOptFn {
return func(tm *tokenIssuer) (err error) {
tm.defaultRequest.Issuer = iss
return
}
}
// WithStore configures store function
func WithStore(fn tokenIssuerStore) IssuerOptFn {
return func(tm *tokenIssuer) (err error) {
tm.store = fn
return
}
}
// WithLookup configures lookup function
func WithLookup(fn tokenIssuerLookup) IssuerOptFn {
return func(tm *tokenIssuer) (err error) {
tm.lookup = fn
return
}
}
// WithGenerator configures generator function
func WithGenerator(fn tokenIssuerGenerator) IssuerOptFn {
return func(tm *tokenIssuer) (err error) {
tm.generator = fn
return
}
}
// WithSigner configures signer function
func WithSigner(fn tokenIssuerSigner) IssuerOptFn {
return func(tm *tokenIssuer) (err error) {
tm.signer = fn
return
}
}
func (req *TokenRequest) apply(opt ...IssueOptFn) (err error) {
for _, fn := range opt {
if err = fn(req); err != nil {
return
}
}
return
}
func WithExpiration(e time.Duration) IssueOptFn {
return func(t *TokenRequest) (err error) {
t.Expiration = e
return
}
}
func WithIdentity(i Identifiable) IssueOptFn {
return func(t *TokenRequest) (err error) {
t.UserID = i.Identity()
t.Roles = i.Roles()
return
}
}
func WithAccessToken(access string) IssueOptFn {
return func(t *TokenRequest) (err error) {
t.AccessToken = access
return
}
}
func WithScope(ss ...string) IssueOptFn {
return func(t *TokenRequest) (err error) {
t.Scope = ss
return
}
}
func WithAudience(aud string) IssueOptFn {
return func(t *TokenRequest) (err error) {
t.Audience = aud
return
}
}
func WithClientID(id uint64) IssueOptFn {
return func(t *TokenRequest) (err error) {
t.ClientID = id
return
}
}
func applyToIssuer(tm *tokenIssuer, opt ...IssuerOptFn) (err error) {
for _, fn := range opt {
if err = fn(tm); err != nil {
return
}
}
return
}

View File

@@ -0,0 +1,44 @@
package auth
import (
"testing"
"github.com/lestrrat-go/jwx/jwt"
"github.com/stretchr/testify/require"
)
func TestIdentityDecoding(t *testing.T) {
var (
req = require.New(t)
ii = []Identifiable{
&identity{id: 1, memberOf: []uint64{}},
&identity{id: 2, memberOf: []uint64{2, 3, 4}},
}
tm, err = NewTokenIssuer(WithSecretSigner("test"))
)
req.NoError(err)
for _, i := range ii {
t.Run(i.String(), func(t *testing.T) {
var (
req = require.New(t)
token jwt.Token
signed []byte
)
signed, err = tm.Sign(WithIdentity(i))
req.NoError(err)
token, err = jwt.Parse(signed)
req.NoError(err)
ift := IdentityFromToken(token)
req.Equal(i.Identity(), ift.Identity())
req.Equal(i.Roles(), ift.Roles())
})
}
}

View File

@@ -0,0 +1,101 @@
package auth
import (
"context"
"fmt"
"net/http"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/go-chi/jwtauth"
"github.com/lestrrat-go/jwx/jwa"
"github.com/lestrrat-go/jwx/jwk"
"github.com/lestrrat-go/jwx/jwt"
)
var (
HttpTokenVerifier func(http.Handler) http.Handler
)
// verifier returns a jwt verification middleware
//
// Tasks
// 1. picks token from header, query or cookie
// 2. extracts identity (if any) and adds it into request context
//
// In there is no token
func verifier(ja *jwtauth.JWTAuth) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token, err := jwtauth.VerifyRequest(ja, r, jwtauth.TokenFromHeader, jwtauth.TokenFromQuery, jwtauth.TokenFromCookie)
ctx := r.Context()
if token != nil && err == nil {
if err = TokenIssuer.Validate(ctx, token); err != nil {
errors.ProperlyServeHTTP(w, r, err, false)
return
}
}
ctx = jwtauth.NewContext(ctx, token, err)
ctx = SetIdentityToContext(ctx, IdentityFromToken(token))
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// TokenVerifierMiddlewareWithSecretSigner returns HTTP handler with simple jwa.HS512 + secret verifier
//
// This should be 1:1 with token issuer!
func TokenVerifierMiddlewareWithSecretSigner(secret string) (_ func(http.Handler) http.Handler, err error) {
if len(secret) == 0 {
return nil, fmt.Errorf("JWK missing")
}
var key jwk.Key
if key, err = jwk.New([]byte(secret)); err != nil {
return nil, fmt.Errorf("could not parse JWK: %w", err)
}
return verifier(jwtauth.New(jwa.HS512.String(), key, nil)), nil
}
// HttpTokenValidator checks if there is a token with identity and matching scope claim
//
// Empty scope defaults to "api"!
func HttpTokenValidator(scope ...string) func(http.Handler) http.Handler {
if len(scope) == 0 {
// ensure that scope is not empty
scope = []string{"api"}
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := verifyToken(r.Context(), scope...)
if err != nil && !errors.Is(err, jwtauth.ErrNoTokenFound) {
errors.ProperlyServeHTTP(w, r, err, false)
return
}
next.ServeHTTP(w, r)
})
}
}
// pulls token from context and validates scope & access-token
func verifyToken(ctx context.Context, scope ...string) (err error) {
var token jwt.Token
if token, _, err = jwtauth.FromContext(ctx); err != nil {
return
}
if token == nil {
return errUnauthorized()
}
if len(scope) > 0 && !CheckJwtScope(token, scope...) {
return errUnauthorizedScope()
}
return
}