Files
Dušan Borovčanin 168e8b90cb
Property Based Tests / api-test (push) Has been cancelled
Continuous Delivery / lint-and-build (push) Has been cancelled
Deploy GitHub Pages / swagger-ui (push) Has been cancelled
CI Pipeline / Lint Proto (push) Has been cancelled
CI Pipeline / Detect Changes (push) Has been cancelled
Continuous Delivery / Build and Push Docker Images (push) Has been cancelled
CI Pipeline / lint-and-build (push) Has been cancelled
CI Pipeline / Test ${{ matrix.module }} (push) Has been cancelled
CI Pipeline / Upload Coverage (push) Has been cancelled
NOISSUE - Move rules engine, alarms, reports, journal and notifications to EE (#3552)
Signed-off-by: dusan <borovcanindusan1@gmail.com>
2026-07-30 17:55:57 +02:00

160 lines
3.5 KiB
Go

// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package atom
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"strings"
"sync"
"time"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/lestrrat-go/jwx/v2/jws"
"github.com/lestrrat-go/jwx/v2/jwt"
)
var ErrInvalidBearerToken = errors.New("invalid bearer token")
const jwksCacheDuration = 5 * time.Minute
type TokenVerifier struct {
jwksURL string
issuer string
audience string
httpClient *http.Client
client *Client
cache jwk.Set
cachedAt time.Time
mu sync.RWMutex
}
func NewTokenVerifier(cfg Config) *TokenVerifier {
timeout := cfg.Timeout
if timeout == 0 {
timeout = defaultTimeout
}
return &TokenVerifier{
jwksURL: cfg.JWKSURL,
issuer: cfg.JWTIssuer,
audience: cfg.JWTAudience,
client: NewClient(cfg),
httpClient: &http.Client{
Timeout: timeout,
},
}
}
func (v *TokenVerifier) VerifyTokenClaims(ctx context.Context, token string) (TokenClaims, error) {
if strings.HasPrefix(token, "atom_") {
res, err := v.client.Introspect(ctx, token)
if err != nil || !res.Active || res.EntityID == "" {
return TokenClaims{}, ErrInvalidBearerToken
}
return TokenClaims{
SubjectID: res.EntityID,
SessionID: res.SessionID,
TenantID: res.TenantID,
}, nil
}
set, err := v.fetchJWKS(ctx, false)
if err != nil {
return TokenClaims{}, err
}
tkn, err := v.parseVerifiedToken(token, set)
if err != nil {
set, refreshErr := v.fetchJWKS(ctx, true)
if refreshErr == nil {
tkn, err = v.parseVerifiedToken(token, set)
}
}
if err != nil {
return TokenClaims{}, ErrInvalidBearerToken
}
claims := TokenClaims{
SubjectID: tkn.Subject(),
ExpiresAt: tkn.Expiration().Unix(),
IssuedAt: tkn.IssuedAt().Unix(),
}
if claims.SubjectID == "" {
return TokenClaims{}, ErrInvalidBearerToken
}
if sid, ok := stringClaim(tkn, "sid"); ok {
claims.SessionID = sid
}
if tid, ok := stringClaim(tkn, "tid"); ok {
claims.TenantID = tid
}
return claims, nil
}
func (v *TokenVerifier) fetchJWKS(ctx context.Context, force bool) (jwk.Set, error) {
if !force {
v.mu.RLock()
if v.cache != nil && time.Since(v.cachedAt) < jwksCacheDuration {
set := v.cache
v.mu.RUnlock()
return set, nil
}
v.mu.RUnlock()
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, v.jwksURL, nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/json")
res, err := v.httpClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
body, _ := io.ReadAll(io.LimitReader(res.Body, 1024))
return nil, fmt.Errorf("fetch atom jwks: status=%d body=%s", res.StatusCode, string(body))
}
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
set, err := jwk.Parse(body)
if err != nil {
return nil, err
}
v.mu.Lock()
v.cache = set
v.cachedAt = time.Now()
v.mu.Unlock()
return set, nil
}
func (v *TokenVerifier) parseVerifiedToken(token string, set jwk.Set) (jwt.Token, error) {
options := []jwt.ParseOption{
jwt.WithValidate(true),
jwt.WithKeySet(set, jws.WithInferAlgorithmFromKey(true)),
}
if v.issuer != "" {
options = append(options, jwt.WithIssuer(v.issuer))
}
if v.audience != "" {
options = append(options, jwt.WithAudience(v.audience))
}
return jwt.Parse(
[]byte(token),
options...,
)
}
func stringClaim(tkn jwt.Token, name string) (string, bool) {
value, ok := tkn.Get(name)
if !ok {
return "", false
}
str, ok := value.(string)
return str, ok
}