Files
ziti/controller/oidc_auth/storage.go
Andrew Martinez 991ccffbdd fixes openziti/ziti#4222 report isCertExtendable correctly for OIDC cert auth (#4224)
- removes a stray assignment that overwrote isCertExtendable with a literal true on every OIDC certificate authentication
- reports isCertExtendable as false over OIDC for cert authenticators not issued by the network, matching legacy cert auth
- adds coverage asserting the z_ice claim and current-api-session both report a 3rd party CA certificate as not extendable
2026-08-26 15:59:17 -04:00

1509 lines
46 KiB
Go

/*
Copyright NetFoundry Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package oidc_auth
import (
"context"
"crypto/sha1"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"sync"
"time"
"github.com/go-jose/go-jose/v4"
"github.com/openziti/foundation/v2/errorz"
"github.com/openziti/ziti/v2/controller/event"
"github.com/openziti/ziti/v2/controller/jwtsigner"
"github.com/golang-jwt/jwt/v5"
"github.com/michaelquigley/pfxlog"
"github.com/openziti/foundation/v2/stringz"
"github.com/openziti/ziti/v2/common"
"github.com/openziti/ziti/v2/controller/apierror"
"github.com/openziti/ziti/v2/controller/change"
"github.com/openziti/ziti/v2/controller/model"
"github.com/openziti/ziti/v2/controller/models"
cmap "github.com/orcaman/concurrent-map/v2"
"github.com/google/uuid"
"github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/zitadel/oidc/v3/pkg/op"
)
var (
_ op.Storage = &HybridStorage{}
_ op.ClientCredentialsStorage = &HybridStorage{}
)
const JwtTokenPrefix = "ey"
// Storage is a compound interface of op.Storage and custom storage functions
type Storage interface {
op.Storage
// Authenticate attempts to perform authentication on supplied credentials for all known authentication methods
Authenticate(authCtx model.AuthContext, id string, configTypes []string) (*AuthRequest, error)
// VerifyTotp will verify the supplied code for the current authentication request's subject
// A change context is required for the removal of one-time TOTP recovery codes
VerifyTotp(ctx *change.Context, code string, id string) (*AuthRequest, error)
// StartTotpEnrollment will attempt to create an MFA record for the current in scope identity
// if it can. If an MFA record already exists, it will fail, and CompleteTotpEnrollment or
// DeleteTotpEnrollment must be used.
StartTotpEnrollment(ctx *change.Context, authRequestId string) (*model.Mfa, error)
// CompleteTotpEnrollment will validate the supplied code against the current unverified MFA record for
// the identity in scope. If MFA enrollment hasn't been started, StartTotpEnrollment should be used.
// On success the returned AuthRequest reflects the updated enrollment and AMR state.
CompleteTotpEnrollment(ctx *change.Context, authRequestId, code string) (*AuthRequest, error)
// DeleteTotpEnrollment will delete a current MFA record for the identity in scope. If the MFA record
// has been verified, a code must be supplied that passes verification. If the
// record has not been verified, deletion will occur without verifying the code value.
DeleteTotpEnrollment(ctx *change.Context, authRequestId string, code string) error
// IsTokenRevoked will return true if a token has been removed.
// TokenId may be a JWT token id or an identity id
IsTokenRevoked(tokenId string) bool
// AddClient adds an OIDC Client to the registry of valid clients.
AddClient(client *Client)
// GetAuthRequest returns an *AuthRequest by its id
GetAuthRequest(id string) (*AuthRequest, error)
// Managers returns the model managers used to interact with durable storage
Managers() *model.Managers
// UpdateSdkEnvInfo will attempt to take the identity in scope of an AuthRequest and update its durable SDK/ENV
// info.
UpdateSdkEnvInfo(request *AuthRequest) error
// ProcessSecondaryJwts runs the external-JWT secondary authentication module against the
// bearer tokens in securityTokenCtx for the identity identified by authRequest. It is
// called during OIDC and legacy authentication/authorization flows.
ProcessSecondaryJwts(r *http.Request, authRequest *AuthRequest, securityTokenCtx *common.SecurityTokenCtx) (model.AuthResult, error)
// GetPrimaryJwtSignerKeyFunc returns the jwt.Keyfunc used to verify controller-issued
// access and refresh tokens during OIDC flows.
GetPrimaryJwtSignerKeyFunc() jwt.Keyfunc
// GetTokenIssuerCache returns the cache of known token issuers, covering both external
// JWT signers and controller-issued token verifiers.
GetTokenIssuerCache() common.TokenIssuerCache
// Env returns the model.Env that provides access to managers, stores, and configuration.
Env() model.Env
}
func NewRevocation(tokenId string, expiresAt time.Time) *model.Revocation {
return &model.Revocation{
BaseEntity: models.BaseEntity{
Id: tokenId,
},
ExpiresAt: expiresAt,
}
}
var _ Storage = (*HybridStorage)(nil)
// HybridStorage implements the Storage interface
// Authentication requests are not synchronized with other controllers. Authentication must happen entirely
// with one controller. After id, access, and/or refresh tokens are acquired, they may be used at any controller.
// All token revocations are synchronized with other controllers.
type HybridStorage struct {
env model.Env
signingKey key
authRequests cmap.ConcurrentMap[string, *AuthRequest] //authRequest.Id -> authRequest
codes cmap.ConcurrentMap[string, string] //code -> authRequest.Id
clients cmap.ConcurrentMap[string, *Client]
deviceCodes cmap.ConcurrentMap[string, deviceAuthorizationEntry]
userCodes cmap.ConcurrentMap[string, string]
serviceUsers cmap.ConcurrentMap[string, *Client]
startOnce sync.Once
config *Config
batcher *RevocationBatcher
keys cmap.ConcurrentMap[string, *pubKey]
}
// Env returns the model.Env backing this storage, giving callers access to managers,
// stores, and configuration that are outside the OIDC auth package's scope.
func (s *HybridStorage) Env() model.Env {
return s.env
}
// GetPrimaryJwtSignerKeyFunc returns the jwt.Keyfunc that verifies controller-issued JWTs.
func (s *HybridStorage) GetPrimaryJwtSignerKeyFunc() jwt.Keyfunc {
return s.env.JwtSignerKeyFunc
}
// GetTokenIssuerCache returns the shared cache of all known token issuers, spanning both
// controller-issued tokens and externally configured JWT signers.
func (s *HybridStorage) GetTokenIssuerCache() common.TokenIssuerCache {
return s.env.GetTokenIssuerCache()
}
// ProcessSecondaryJwts invokes the ext-JWT authentication module in secondary mode for the
// identity referenced by authRequest, using the bearer tokens already parsed into
// securityTokenCtx. Returns the AuthResult on success or an error describing the failure.
func (s *HybridStorage) ProcessSecondaryJwts(r *http.Request, authRequest *AuthRequest, securityTokenCtx *common.SecurityTokenCtx) (model.AuthResult, error) {
if authRequest == nil {
return nil, errors.New("auth request is nil")
}
if securityTokenCtx == nil {
return nil, errors.New("security token ctx is nil")
}
extJwtModule := s.env.GetAuthRegistry().GetByMethod(AuthMethodExtJwt)
if extJwtModule == nil {
return nil, errors.New("no external jwt module found")
}
identity, err := s.env.GetManagers().Identity.Read(authRequest.IdentityId)
if err != nil {
return nil, errors.New("could not read identity")
}
changeCtx := NewChangeCtx()
authCtx := model.NewAuthContextHttp(r, AuthMethodExtJwt, nil, changeCtx)
authCtx.SetPrimaryIdentity(identity)
authCtx.SetSecurityTokenCtx(securityTokenCtx)
return extJwtModule.Process(authCtx)
}
func (s *HybridStorage) UpdateSdkEnvInfo(request *AuthRequest) error {
identity, err := s.env.GetManagers().Identity.Read(request.IdentityId)
if err != nil {
return err
}
changeCtx := NewChangeCtx()
var envInfo *model.EnvInfo
var sdkInfo *model.SdkInfo
if request.EnvInfo != nil {
envInfo = &model.EnvInfo{
Arch: request.EnvInfo.Arch,
Os: request.EnvInfo.Os,
OsRelease: request.EnvInfo.OsRelease,
OsVersion: request.EnvInfo.OsVersion,
Domain: request.EnvInfo.Domain,
Hostname: request.EnvInfo.Hostname,
}
}
if request.SdkInfo != nil {
sdkInfo = &model.SdkInfo{
AppId: request.SdkInfo.AppID,
AppVersion: request.SdkInfo.AppVersion,
Branch: request.SdkInfo.Branch,
Revision: request.SdkInfo.Revision,
Type: request.SdkInfo.Type,
Version: request.SdkInfo.Version,
}
}
return s.env.GetManagers().Identity.UpdateSdkEnvInfo(identity, envInfo, sdkInfo, changeCtx)
}
func (s *HybridStorage) Managers() *model.Managers {
return s.env.GetManagers()
}
func (s *HybridStorage) StartTotpEnrollment(changeCtx *change.Context, authRequestId string) (*model.Mfa, error) {
authRequest, err := s.GetAuthRequest(authRequestId)
if err != nil {
return nil, errorz.NewUnauthorized()
}
id, err := s.env.GetManagers().Mfa.CreateForIdentityId(authRequest.IdentityId, changeCtx)
if err != nil {
return nil, err
}
return s.env.GetManagers().Mfa.Read(id)
}
func (s *HybridStorage) DeleteTotpEnrollment(changeCtx *change.Context, authRequestId string, code string) error {
authRequest, err := s.GetAuthRequest(authRequestId)
if err != nil {
return errorz.NewUnauthorized()
}
mfaDetail, err := s.env.GetManagers().Mfa.ReadOneByIdentityId(authRequest.IdentityId)
if err != nil {
return err
}
if mfaDetail == nil {
return errorz.NewNotFound()
}
if mfaDetail.IsVerified {
ok, err := s.env.GetManagers().Mfa.Verify(mfaDetail, code, changeCtx)
if err != nil {
return err
}
if !ok {
return apierror.NewInvalidMfaTokenError()
}
//ok fall through
}
return s.env.GetManagers().Mfa.Delete(mfaDetail.Id, changeCtx)
}
func (s *HybridStorage) CompleteTotpEnrollment(changeCtx *change.Context, authRequestId, code string) (*AuthRequest, error) {
authRequest, err := s.GetAuthRequest(authRequestId)
if err != nil {
return nil, errorz.NewUnauthorized()
}
if err = s.env.GetManagers().Mfa.CompleteTotpEnrollment(authRequest.IdentityId, code, changeCtx); err != nil {
return nil, err
}
authRequest.IsTotpEnrolled = true
authRequest.AddAmr(AuthMethodSecondaryTotp)
return authRequest, nil
}
func (s *HybridStorage) AddClient(client *Client) {
s.clients.Set(client.id, client)
}
var _ Storage = &HybridStorage{}
func NewStorage(rootSigner *jwtsigner.TlsJwtSigner, config *Config, env model.Env) *HybridStorage {
store := &HybridStorage{
env: env,
signingKey: key{
id: rootSigner.KeyId(),
algorithm: jose.SignatureAlgorithm(rootSigner.SigningMethod().Alg()),
privateKey: rootSigner.TlsCerts.PrivateKey,
publicKey: rootSigner.TlsCerts.Leaf.PublicKey,
},
authRequests: cmap.New[*AuthRequest](),
codes: cmap.New[string](),
clients: cmap.New[*Client](),
deviceCodes: cmap.New[deviceAuthorizationEntry](),
userCodes: cmap.New[string](),
serviceUsers: cmap.New[*Client](),
config: config,
keys: cmap.New[*pubKey](),
batcher: NewRevocationBatcher(env, config),
}
env.GetManagers().RevocationBatchFlusher = store.FlushRevocationBatcher
store.start()
return store
}
// start will run Clean every 10 seconds and start the revocation batcher.
func (s *HybridStorage) start() {
s.startOnce.Do(func() {
closeNotify := s.env.GetCloseNotifyChannel()
ticker := time.NewTicker(10 * time.Second)
go func() {
for {
select {
case <-ticker.C:
s.Clean()
case <-closeNotify:
ticker.Stop()
return
}
}
}()
s.batcher.Start()
})
}
// FlushRevocationBatcher synchronously drains any pending batched revocations.
// Exposed for testing.
func (s *HybridStorage) FlushRevocationBatcher() {
s.batcher.Flush()
}
// Clean removes abandoned auth requests and associated data
func (s *HybridStorage) Clean() {
var deleteKeys []string
oldest := time.Now().Add(-10 * time.Minute)
s.authRequests.IterCb(func(key string, v *AuthRequest) {
if v.CreationDate.Before(oldest) {
deleteKeys = append(deleteKeys, key)
}
})
for _, key := range deleteKeys {
s.authRequests.Remove(key)
}
//find associated codes and remove
var deleteCodes []string
s.codes.IterCb(func(code string, authId string) {
if stringz.Contains(deleteKeys, authId) {
deleteCodes = append(deleteCodes, code)
}
})
for _, deleteCode := range deleteCodes {
s.codes.Remove(deleteCode)
}
}
// Authenticate will verify supplied credentials and update the primary authentication status of an AuthRequest
func (s *HybridStorage) Authenticate(authCtx model.AuthContext, id string, configTypes []string) (*AuthRequest, error) {
authRequest, ok := s.authRequests.Get(id)
if !ok {
return nil, fmt.Errorf("request not found")
}
result, err := s.env.GetManagers().Authenticator.Authorize(authCtx)
if err != nil {
return nil, err
}
if !result.IsSuccessful() {
return nil, apierror.NewInvalidAuth()
}
authRequest.IdentityId = result.Identity().Id
authRequest.AddAmr(authCtx.GetMethod())
authenticator := result.Authenticator()
if authenticator != nil {
if certAuth := authenticator.ToCert(); certAuth != nil {
authRequest.IsCertExtendRequested = certAuth.IsExtendRequested
authRequest.IsCertKeyRollRequested = certAuth.IsKeyRollRequested
}
}
configTypeIds := s.env.GetManagers().ConfigType.MapConfigTypeNamesToIds(configTypes, authRequest.IdentityId)
for configId := range configTypeIds {
authRequest.ConfigTypes = append(authRequest.ConfigTypes, configId)
}
mfa, err := s.env.GetManagers().Mfa.ReadOneByIdentityId(authRequest.IdentityId)
if err != nil {
return nil, err
}
authRequest.IsTotpEnrolled = mfa != nil && mfa.IsVerified
authRequest.SecondaryTotpRequired = authRequest.IsTotpEnrolled || result.AuthPolicy().Secondary.RequireTotp
extJwtSignerId := stringz.OrEmpty(result.AuthPolicy().Secondary.RequiredExtJwtSigner)
if extJwtSignerId != "" {
authRequest.SecondaryExtJwtSigner, err = s.env.GetManagers().ExternalJwtSigner.Read(extJwtSignerId)
if err != nil {
return nil, err
}
}
if authCtx.GetMethod() == AuthMethodCert {
if len(authRequest.PeerCerts) == 0 {
authRequest.PeerCerts = authCtx.GetCerts()
}
certAuth := result.Authenticator().ToCert()
if certAuth != nil {
authRequest.IsCertExtendable = certAuth.IsIssuedByNetwork
authRequest.IsCertKeyRollRequested = certAuth.IsKeyRollRequested
authRequest.ImproperClientCertChain = result.ImproperClientCertChain()
}
authRequest.CertAllowExpired = result.AuthPolicy().Primary.Cert.AllowExpiredCerts
}
authRequest.AuthenticatorId = result.AuthenticatorId()
return authRequest, nil
}
// IsTokenRevoked returns true or false if a token has been revoked
func (s *HybridStorage) IsTokenRevoked(tokenId string) bool {
revocation, _ := s.env.GetManagers().Revocation.Read(tokenId)
return revocation != nil
}
// VerifyTotp will update and return the AuthRequest associated with `id`
func (s *HybridStorage) VerifyTotp(ctx *change.Context, code string, id string) (*AuthRequest, error) {
code = strings.TrimSpace(code)
id = strings.TrimSpace(id)
if len(code) > 13 || len(id) > 40 {
return nil, errors.New("invalid input")
}
if len(code) == 0 {
return nil, errors.New("code is required")
}
if len(id) == 0 {
return nil, errors.New("invalid request")
}
authRequest, ok := s.authRequests.Get(id)
if !ok {
return nil, errors.New("request not found")
}
if len(authRequest.Amr) == 0 {
return nil, errors.New("request not authorized")
}
totp, err := s.env.GetManagers().Mfa.ReadOneByIdentityId(authRequest.IdentityId)
if err != nil {
return nil, errors.New("could not read totp status")
}
if totp == nil {
return nil, errors.New("totp not found")
}
ok, _ = s.env.GetManagers().Mfa.Verify(totp, code, ctx)
if !ok {
return nil, apierror.NewInvalidMfaTokenError()
}
authRequest.AddAmr(AuthMethodSecondaryTotp)
return authRequest, nil
}
// CreateAuthRequest creates a new AuthRequest based on an incoming request, implements the op.Storage interface
func (s *HybridStorage) CreateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, identityId string) (op.AuthRequest, error) {
httpRequest, err := HttpRequestFromContext(ctx)
if httpRequest == nil || err != nil {
return nil, oidc.ErrServerError()
}
request := &AuthRequest{
AuthRequest: *authReq,
CreationDate: time.Now(),
IdentityId: identityId,
ApiSessionId: uuid.NewString(),
RemoteAddress: httpRequest.RemoteAddr,
}
request.PeerCerts = httpRequest.TLS.PeerCertificates
for _, authHeader := range httpRequest.Header.Values("authorize") {
if strings.HasPrefix(authHeader, "Bearer ") {
request.BearerTokenDetected = true
break
}
}
request.RequestedMethod = httpRequest.URL.Query().Get("method")
configTypeNames := httpRequest.URL.Query()["configTypes"]
configTypeIds := s.env.GetManagers().ConfigType.MapConfigTypeNamesToIds(configTypeNames, identityId)
for configId := range configTypeIds {
request.ConfigTypes = append(request.ConfigTypes, configId)
}
if len(authReq.Prompt) == 1 && authReq.Prompt[0] == "none" {
return nil, oidc.ErrLoginRequired()
}
request.Id = uuid.NewString()
s.authRequests.Set(request.Id, request)
return request, nil
}
// AuthRequestByID implements the op.Storage interface
func (s *HybridStorage) AuthRequestByID(_ context.Context, id string) (op.AuthRequest, error) {
return s.GetAuthRequest(id)
}
// GetAuthRequest returns an AuthRequest by id
func (s *HybridStorage) GetAuthRequest(id string) (*AuthRequest, error) {
request, ok := s.authRequests.Get(id)
if !ok {
return nil, fmt.Errorf("request not found")
}
return request, nil
}
// AuthRequestByCode implements the op.Storage interface
func (s *HybridStorage) AuthRequestByCode(ctx context.Context, code string) (op.AuthRequest, error) {
requestID, ok := func() (string, bool) {
requestID, ok := s.codes.Get(code)
return requestID, ok
}()
if !ok {
return nil, fmt.Errorf("code invalid or expired")
}
return s.AuthRequestByID(ctx, requestID)
}
// SaveAuthCode implements the op.Storage interface
func (s *HybridStorage) SaveAuthCode(_ context.Context, id string, code string) error {
s.codes.Set(code, id)
return nil
}
// DeleteAuthRequest implements the op.Storage interface
func (s *HybridStorage) DeleteAuthRequest(_ context.Context, id string) error {
s.authRequests.Remove(id)
var toRemove []string
s.codes.IterCb(func(key string, v string) {
if v == id {
toRemove = append(toRemove, key)
}
})
for _, mapKey := range toRemove {
s.codes.Remove(mapKey)
}
return nil
}
// CreateAccessToken implements the op.Storage interface
func (s *HybridStorage) CreateAccessToken(ctx context.Context, request op.TokenRequest) (string, time.Time, error) {
accessTokenId, accessClaims, err := s.createAccessToken(ctx, request)
if err != nil {
return "", time.Time{}, err
}
ts, err := TokenStateFromContext(ctx)
if err != nil {
return "", time.Time{}, err
}
ts.AccessClaims = accessClaims
return accessTokenId, accessClaims.Expiration.AsTime(), nil
}
// createAccessToken converts an op.TokenRequest into an access token
func (s *HybridStorage) createAccessToken(ctx context.Context, request op.TokenRequest) (string, *common.AccessClaims, error) {
now := time.Now()
claims := &common.AccessClaims{
AccessTokenClaims: oidc.AccessTokenClaims{
TokenClaims: oidc.TokenClaims{
JWTID: uuid.NewString(),
Issuer: op.IssuerFromContext(ctx),
Subject: request.GetSubject(),
Audience: request.GetAudience(),
Expiration: oidc.Time(now.Add(s.config.AccessTokenDuration).Unix()),
IssuedAt: oidc.Time(now.Unix()),
AuthTime: oidc.Time(now.Unix()),
NotBefore: oidc.Time(now.Unix()),
},
},
CustomClaims: common.CustomClaims{},
}
var eventType = "unhandled"
var csrPem string
var csrApiSessionId string
switch req := request.(type) {
case *AuthRequest:
eventType = event.ApiSessionEventTypeCreated
claims.CustomClaims.ApiSessionId = req.ApiSessionId
claims.CustomClaims.ApplicationId = req.ClientID
claims.CustomClaims.ConfigTypes = req.ConfigTypes
claims.AuthenticationMethodsReferences = req.GetAMR()
// cert backed auth should bind tokens for cert pop, avoid for auth requests that do not establish cert pop prior or during the request
if req.HasAmr(AuthMethodCert) {
if fp := req.GetCertFingerprint(); fp != "" {
claims.CustomClaims.CertFingerprints = []string{fp}
claims.CustomClaims.AuthCertFingerprint = fp
}
}
claims.CustomClaims.EnvInfo = req.EnvInfo
claims.CustomClaims.SdkInfo = req.SdkInfo
claims.CustomClaims.RemoteAddress = req.RemoteAddress
claims.CustomClaims.IsCertExtendable = req.IsCertExtendable
claims.CustomClaims.CertAllowExpired = req.CertAllowExpired
claims.CustomClaims.IsCertExtendRequested = req.IsCertExtendRequested
claims.CustomClaims.IsCertKeyRollRequested = req.IsCertKeyRollRequested
claims.CustomClaims.ImproperClientCertChain = req.ImproperClientCertChain
claims.CustomClaims.AuthenticatorId = req.AuthenticatorId
claims.AuthTime = oidc.Time(req.AuthTime.Unix())
claims.AccessTokenClaims.AuthenticationMethodsReferences = req.GetAMR()
claims.ClientID = req.ClientID
if req.CsrPem != "" {
csrPem = req.CsrPem
csrApiSessionId = req.ApiSessionId
}
case *RefreshTokenRequest:
eventType = event.ApiSessionEventTypeRefreshed
claims.CustomClaims = req.CustomClaims
claims.AuthTime = req.AuthTime
claims.AccessTokenClaims.AuthenticationMethodsReferences = req.GetAMR()
claims.ClientID = req.ClientID
if ts, tsErr := TokenStateFromContext(ctx); tsErr == nil && ts.CsrPem != "" {
csrPem = ts.CsrPem
csrApiSessionId = req.CustomClaims.ApiSessionId
}
case op.TokenExchangeRequest:
eventType = event.ApiSessionEventTypeExchanged
mapClaims := req.GetExchangeSubjectTokenClaims()
subjectClaims := &common.AccessClaims{}
if mapClaims != nil {
jsonStr, err := json.Marshal(mapClaims)
if err != nil {
return "", nil, err
}
err = json.Unmarshal(jsonStr, subjectClaims)
if err != nil {
return "", nil, err
}
} else {
var err error
subjectTokenStr := req.GetExchangeSubjectTokenIDOrToken()
_, subjectClaims, err = s.parseAccessToken(subjectTokenStr)
if err != nil {
return "", nil, err
}
if subjectClaims.CustomClaims.Type != common.TokenTypeAccess && subjectClaims.CustomClaims.Type != common.TokenTypeRefresh {
return "", nil, oidc.ErrInvalidGrant().WithDescription("invalid token type for exchange")
}
claims.Audience = subjectClaims.Audience
claims.AccessTokenClaims.Scopes = subjectClaims.CustomClaims.Scopes
}
claims.CustomClaims = subjectClaims.CustomClaims
// auth_time attests when the subject actually authenticated; carry it through the
// exchange rather than leaving the mint-time default, which would falsely refresh
// authentication-recency semantics (e.g. router-side MFA posture baselines).
claims.AuthTime = subjectClaims.AuthTime
claims.AccessTokenClaims.AuthenticationMethodsReferences = req.GetAMR()
claims.ClientID = req.GetClientID()
if ts, tsErr := TokenStateFromContext(ctx); tsErr == nil && ts.CsrPem != "" {
csrPem = ts.CsrPem
csrApiSessionId = subjectClaims.CustomClaims.ApiSessionId
}
}
claims.AccessTokenClaims.Scopes = request.GetScopes()
claims.CustomClaims.Scopes = request.GetScopes()
claims.CustomClaims.Type = common.TokenTypeAccess
identity, err := s.env.GetManagers().Identity.Read(request.GetSubject())
if err != nil {
return "", nil, err
}
if identity == nil {
return "", nil, fmt.Errorf("identity not found: %s", request.GetSubject())
}
claims.CustomClaims.IsAdmin = identity.IsAdmin
claims.CustomClaims.ExternalId = stringz.OrEmpty(identity.ExternalId)
certGenerated := false
if csrPem != "" {
csrChangeCtx := NewChangeCtx()
apiSession := &model.ApiSession{BaseEntity: models.BaseEntity{Id: csrApiSessionId}}
apiSessionCert, csrErr := s.env.GetManagers().ApiSessionCertificate.CreateFromCSR(
identity, apiSession, true, 365*24*time.Hour, []byte(csrPem), csrChangeCtx,
)
if csrErr != nil {
return "", nil, oidc.ErrInvalidRequest().WithDescription("invalid CSR: %s", csrErr.Error())
}
if claims.CustomClaims.AuthCertFingerprint != "" {
claims.CustomClaims.CertFingerprints = []string{claims.CustomClaims.AuthCertFingerprint, apiSessionCert.Fingerprint}
} else {
claims.CustomClaims.CertFingerprints = []string{apiSessionCert.Fingerprint}
}
if ts, tsErr := TokenStateFromContext(ctx); tsErr == nil {
ts.SessionCertPem = apiSessionCert.PEM
}
certGenerated = true
}
ipAddr := ""
if httpRequest, _ := HttpRequestFromContext(ctx); httpRequest != nil {
ipAddr = httpRequest.RemoteAddr
}
evt := &event.ApiSessionEvent{
Namespace: event.ApiSessionEventNS,
EventType: eventType,
EventSrcId: s.env.GetId(),
Id: claims.ApiSessionId,
Type: event.ApiSessionTypeJwt,
Timestamp: time.Now(),
IdentityId: identity.Id,
IpAddress: ipAddr,
CertGenerated: certGenerated,
}
s.env.GetEventDispatcher().AcceptApiSessionEvent(evt)
return claims.JWTID, claims, nil
}
// CreateAccessAndRefreshTokens implements the op.Storage interface
func (s *HybridStorage) CreateAccessAndRefreshTokens(ctx context.Context, request op.TokenRequest, currentRefreshToken string) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) {
accessTokenId, accessClaims, err := s.createAccessToken(ctx, request)
if err != nil {
return "", "", time.Time{}, err
}
tokenState, err := TokenStateFromContext(ctx)
if err != nil {
return "", "", time.Time{}, err
}
tokenState.AccessClaims = accessClaims
if currentRefreshToken == "" {
refreshToken, refreshClaims, err := s.createRefreshClaims(accessClaims)
tokenState.RefreshClaims = refreshClaims
if err != nil {
return "", "", time.Time{}, err
}
return accessTokenId, refreshToken, accessClaims.Expiration.AsTime(), nil
}
refreshToken, refreshClaims, err := s.renewRefreshToken(currentRefreshToken, accessClaims)
tokenState.RefreshClaims = refreshClaims
if err != nil {
return "", "", time.Time{}, err
}
return accessTokenId, refreshToken, accessClaims.Expiration.AsTime(), nil
}
// parseRefreshToken parses a JWT refresh token
func (s *HybridStorage) parseRefreshToken(tokenStr string) (*jwt.Token, *common.RefreshClaims, error) {
refreshClaims := &common.RefreshClaims{}
parsedToken, err := jwt.ParseWithClaims(tokenStr, refreshClaims, s.env.JwtSignerKeyFunc)
if err != nil || parsedToken == nil {
if errors.Is(err, jwt.ErrTokenExpired) {
return nil, nil, oidc.ErrInvalidGrant().WithDescription("refresh token expired").WithParent(err)
}
return nil, nil, oidc.ErrInvalidGrant().WithDescription("failed to parse refresh token").WithParent(err)
}
if !parsedToken.Valid {
return nil, nil, oidc.ErrInvalidGrant().WithDescription("invalid refresh token")
}
if refreshClaims.Type != common.TokenTypeRefresh {
return nil, nil, oidc.ErrInvalidGrant().WithDescription("invalid token type, expected refresh")
}
return parsedToken, refreshClaims, nil
}
// parseAccessToken parses a JWT access token
func (s *HybridStorage) parseAccessToken(tokenStr string) (*jwt.Token, *common.AccessClaims, error) {
accessClaims := &common.AccessClaims{}
parsedToken, err := jwt.ParseWithClaims(tokenStr, accessClaims, s.env.JwtSignerKeyFunc)
if err != nil || parsedToken == nil {
if errors.Is(err, jwt.ErrTokenExpired) {
return nil, nil, oidc.ErrInvalidGrant().WithDescription("access token expired").WithParent(err)
}
return nil, nil, oidc.ErrInvalidGrant().WithDescription("failed to parse access token").WithParent(err)
}
if !parsedToken.Valid {
return nil, nil, oidc.ErrInvalidGrant().WithDescription("invalid access token")
}
return parsedToken, accessClaims, nil
}
// TokenRequestByRefreshToken implements the op.Storage interface
func (s *HybridStorage) TokenRequestByRefreshToken(_ context.Context, refreshToken string) (op.RefreshTokenRequest, error) {
_, token, err := s.parseRefreshToken(refreshToken)
if err != nil {
return nil, err
}
if s.IsTokenRevoked(token.JWTID) {
return nil, oidc.ErrInvalidGrant().WithDescription("refresh token revoked")
}
return &RefreshTokenRequest{*token}, nil
}
// TerminateSession implements the op.Storage interface
func (s *HybridStorage) TerminateSession(_ context.Context, identityId string, clientID string) error {
now := time.Now()
revocation := NewRevocation(identityId, now.Add(s.config.MaxTokenDuration()))
revocation.Type = common.RevocationTypeIdentity
revocation.IssuedBefore = now
return s.saveOrReplaceRevocation(revocation)
}
var _ op.CanTerminateSessionFromRequest = (*HybridStorage)(nil)
// TerminateSessionFromRequest implements op.CanTerminateSessionFromRequest. The
// op framework prefers it over TerminateSession when present. It revokes the
// specific api-session being terminated, identified by the z_asid claim in the
// end-session request's id_token_hint, so revocation is precise to this session
// (a re-authentication gets a fresh z_asid and is unaffected). When the hint
// carries no api-session id, it falls back to the identity-scoped revocation.
func (s *HybridStorage) TerminateSessionFromRequest(_ context.Context, endSessionRequest *op.EndSessionRequest) (string, error) {
now := time.Now()
expiresAt := now.Add(s.config.MaxTokenDuration())
if claims := endSessionRequest.IDTokenHintClaims; claims != nil {
if apiSessionId, ok := claims.Claims[common.CustomClaimApiSessionId].(string); ok && apiSessionId != "" {
revocation := NewRevocation(apiSessionId, expiresAt)
revocation.Type = common.RevocationTypeApiSession
if err := s.saveOrReplaceRevocation(revocation); err != nil {
return "", oidc.ErrServerError()
}
return endSessionRequest.RedirectURI, nil
}
}
if endSessionRequest.UserID != "" {
revocation := NewRevocation(endSessionRequest.UserID, expiresAt)
revocation.Type = common.RevocationTypeIdentity
revocation.IssuedBefore = now
if err := s.saveOrReplaceRevocation(revocation); err != nil {
return "", oidc.ErrServerError()
}
}
return endSessionRequest.RedirectURI, nil
}
// GetRefreshTokenInfo implements the op.Storage interface
func (s *HybridStorage) GetRefreshTokenInfo(_ context.Context, _ string, token string) (identityId string, tokenID string, err error) {
_, refreshClaims, err := s.parseRefreshToken(token)
if err != nil {
return "", "", op.ErrInvalidRefreshToken
}
return refreshClaims.Subject, token, nil
}
// RevokeToken implements the op.Storage interface
func (s *HybridStorage) RevokeToken(_ context.Context, tokenIDOrToken string, _ string, _ string) *oidc.Error {
if strings.HasPrefix(tokenIDOrToken, JwtTokenPrefix) {
_, claims, err := s.parseRefreshToken(tokenIDOrToken)
if err != nil {
return nil //not a valid token ignore
}
revocation := NewRevocation(claims.JWTID, claims.Expiration.AsTime())
revocation.Type = common.RevocationTypeToken
if err := s.saveRevocation(revocation); err != nil {
return oidc.ErrServerError()
}
return nil
}
revocation := NewRevocation(tokenIDOrToken, time.Now().Add(s.config.MaxTokenDuration()))
revocation.Type = common.RevocationTypeToken
if err := s.saveRevocation(revocation); err != nil {
return oidc.ErrServerError()
}
return nil
}
// SigningKey implements the op.Storage interface
func (s *HybridStorage) SigningKey(_ context.Context) (op.SigningKey, error) {
return &s.signingKey, nil
}
// SignatureAlgorithms implements the op.Storage interface
func (s *HybridStorage) SignatureAlgorithms(context.Context) ([]jose.SignatureAlgorithm, error) {
return []jose.SignatureAlgorithm{s.signingKey.Algorithm()}, nil
}
// KeySet implements the op.Storage interface
func (s *HybridStorage) KeySet(_ context.Context) ([]op.Key, error) {
signers := s.env.GetPeerSigners()
for _, cert := range signers {
kid := fmt.Sprintf("%x", sha1.Sum(cert.Raw))
if _, found := s.keys.Get(kid); found {
continue
}
if newKey := newKeyFromCert(cert, kid); newKey != nil {
s.keys.Set(kid, newKey)
} else {
pfxlog.Logger().
WithField("issuer", cert.Issuer).
WithField("subject", cert.Subject).
WithField("kid", fmt.Sprintf("%x", sha1.Sum(cert.Raw))).
WithField("publicKeyType", fmt.Sprintf("%T", cert.PublicKey)).
Error("could not convert cert to JWKS key, unknown signing method")
}
}
var result []op.Key
//this controller
if !s.IsTokenRevoked(s.signingKey.id) {
result = append(result, &pubKey{key: s.signingKey})
}
//peer controllers
s.keys.IterCb(func(kid string, key *pubKey) {
if !s.IsTokenRevoked(kid) {
result = append(result, key)
}
})
return result, nil
}
// GetClientByClientID implements the op.Storage interface
func (s *HybridStorage) GetClientByClientID(_ context.Context, clientID string) (op.Client, error) {
client, ok := s.clients.Get(clientID)
if !ok {
return nil, oidc.ErrInvalidClient().WithDescription("client not found")
}
return client, nil
}
// AuthorizeClientIDSecret implements the op.Storage interface
func (s *HybridStorage) AuthorizeClientIDSecret(_ context.Context, clientID, clientSecret string) error {
client, ok := s.clients.Get(clientID)
if !ok {
return oidc.ErrInvalidClient().WithDescription("client not found")
}
//this isn't used and is plain text comparison
if client.secret != clientSecret {
return oidc.ErrInvalidClient().WithDescription("invalid client secret")
}
return nil
}
// SetUserinfoFromScopes implements the op.Storage interface.
func (s *HybridStorage) SetUserinfoFromScopes(_ context.Context, _ *oidc.UserInfo, _, _ string, _ []string) error {
return nil
}
// SetUserinfoFromRequest implements the op.CanSetUserinfoFromRequest interface.
func (s *HybridStorage) SetUserinfoFromRequest(_ context.Context, userinfo *oidc.UserInfo, token op.IDTokenRequest, scopes []string) error {
return s.setInfo(userinfo, token.GetSubject(), scopes)
}
// SetUserinfoFromToken implements the op.Storage interface
func (s *HybridStorage) SetUserinfoFromToken(ctx context.Context, userinfo *oidc.UserInfo, tokenID, subject, _ string) error {
httpRequest, err := HttpRequestFromContext(ctx)
if s.IsTokenRevoked(tokenID) {
return oidc.ErrAccessDenied().WithDescription("access token revoked")
}
if err != nil {
return err
}
accessStr, err := getAccessToken(httpRequest)
if err != nil {
return err
}
_, claims, err := s.parseAccessToken(accessStr)
if err != nil {
return err
}
if claims.Type != common.TokenTypeAccess {
return errors.New("token is invalid type")
}
return s.setInfo(userinfo, subject, nil)
}
// SetIntrospectionFromToken implements the op.Storage interface
func (s *HybridStorage) SetIntrospectionFromToken(_ context.Context, _ *oidc.IntrospectionResponse, _, _, _ string) error {
return fmt.Errorf("unsupported")
}
// GetPrivateClaimsFromScopes implements the op.Storage interface
func (s *HybridStorage) GetPrivateClaimsFromScopes(ctx context.Context, identityId, clientID string, scopes []string) (claims map[string]interface{}, err error) {
return s.getPrivateClaims(ctx, identityId, clientID, scopes)
}
func (s *HybridStorage) getPrivateClaims(ctx context.Context, _, _ string, _ []string) (claims map[string]interface{}, err error) {
tokenState, err := TokenStateFromContext(ctx)
if err != nil {
return nil, err
}
tsClaims, err := tokenState.AccessClaims.CustomClaims.ToMap()
if err != nil {
return nil, err
}
if amr := tokenState.AccessClaims.AuthenticationMethodsReferences; len(amr) > 0 {
tsClaims["amr"] = amr
}
if authTime := tokenState.AccessClaims.AuthTime; authTime != 0 {
tsClaims["auth_time"] = int64(authTime)
}
return tsClaims, nil
}
// GetKeyByIDAndClientID implements the op.Storage interface
func (s *HybridStorage) GetKeyByIDAndClientID(_ context.Context, keyID, _ string) (*jose.JSONWebKey, error) {
targetKey, found := s.keys.Get(keyID)
if !found {
return nil, errors.New("key not found")
}
return &jose.JSONWebKey{
KeyID: keyID,
Use: "sig",
Key: targetKey,
}, nil
}
// ValidateJWTProfileScopes implements the op.Storage interface
func (s *HybridStorage) ValidateJWTProfileScopes(_ context.Context, _ string, scopes []string) ([]string, error) {
allowedScopes := make([]string, 0)
for _, scope := range scopes {
if scope == oidc.ScopeOpenID || scope == oidc.ScopeOfflineAccess {
allowedScopes = append(allowedScopes, scope)
}
}
return allowedScopes, nil
}
// Health implements the op.Storage interface
func (s *HybridStorage) Health(_ context.Context) error {
return nil
}
func (s *HybridStorage) createRefreshClaims(accessClaims *common.AccessClaims) (string, *common.RefreshClaims, error) {
claims := &common.RefreshClaims{
IDTokenClaims: oidc.IDTokenClaims{
TokenClaims: accessClaims.TokenClaims,
NotBefore: accessClaims.NotBefore,
},
CustomClaims: accessClaims.CustomClaims,
}
claims.Expiration = oidc.Time(time.Now().Add(s.config.RefreshTokenDuration).Unix())
claims.Type = common.TokenTypeRefresh
token, err := s.env.GetRootTlsJwtSigner().Generate(claims)
if err != nil {
return "", nil, fmt.Errorf("failed to generate refresh token: %w", err)
}
return token, claims, nil
}
func (s *HybridStorage) saveRevocation(revocation *model.Revocation) error {
return s.env.GetManagers().Revocation.Create(revocation, change.New())
}
// saveOrReplaceRevocation persists a revocation that is keyed by a reusable id
// (an identity or api-session id, as opposed to a unique token jti), replacing
// any lingering prior entry so a repeat logout/termination refreshes its cutoff
// rather than colliding.
func (s *HybridStorage) saveOrReplaceRevocation(revocation *model.Revocation) error {
return s.env.GetManagers().Revocation.CreateOrReplace(revocation, change.New())
}
func (s *HybridStorage) renewRefreshToken(currentRefreshToken string, accessClaims *common.AccessClaims) (string, *common.RefreshClaims, error) {
_, refreshClaims, err := s.parseRefreshToken(currentRefreshToken)
if err != nil {
return "", nil, oidc.ErrInvalidGrant().WithDescription("invalid refresh token").WithParent(err)
}
// Best-effort revocation of the old refresh token. Skip if the token is
// about to expire anyway; otherwise queue it for batched creation.
expiresAt := refreshClaims.Expiration.AsTime()
if s.config.RevocationMinTokenLifetime == 0 || time.Until(expiresAt) > s.config.RevocationMinTokenLifetime {
revocation := NewRevocation(refreshClaims.JWTID, expiresAt)
revocation.Type = common.RevocationTypeToken
s.batcher.Add(revocation)
}
// Use the access claims' CustomClaims so that any updates from CSR signing
// (CertFingerprints, AuthCertFingerprint) are propagated to the new refresh token.
newRefreshClaims := &common.RefreshClaims{
IDTokenClaims: refreshClaims.IDTokenClaims,
CustomClaims: accessClaims.CustomClaims,
}
newRefreshClaims.CustomClaims.Type = common.TokenTypeRefresh
now := time.Now()
newRefreshClaims.JWTID = uuid.NewString()
newRefreshClaims.IssuedAt = oidc.Time(now.Unix())
newRefreshClaims.NotBefore = oidc.Time(now.Unix())
newRefreshClaims.Expiration = oidc.Time(now.Add(s.config.RefreshTokenDuration).Unix())
token, err := s.env.GetRootTlsJwtSigner().Generate(newRefreshClaims)
if err != nil {
return "", nil, fmt.Errorf("failed to generate refresh token: %w", err)
}
return token, newRefreshClaims, nil
}
func (s *HybridStorage) setInfo(userInfo *oidc.UserInfo, identityId string, scopes []string) (err error) {
identity, err := s.env.GetManagers().Identity.Read(identityId)
if err != nil {
return err
}
if identity == nil {
return fmt.Errorf("user not found")
}
for _, scope := range scopes {
switch scope {
case oidc.ScopeOpenID:
userInfo.Subject = identity.Id
userInfo.Name = identity.Name
if identity.ExternalId != nil && *identity.ExternalId != "" {
userInfo.AppendClaims(common.CustomClaimExternalId, identity.ExternalId)
}
userInfo.AppendClaims(common.CustomClaimIsAdmin, identity.IsAdmin)
}
}
return nil
}
func tokenTypeToName(oidcType oidc.TokenType) string {
switch oidcType {
case oidc.AccessTokenType:
return "access_token"
case oidc.IDTokenType:
return "id_token"
case oidc.RefreshTokenType:
return "refresh_token"
}
return "unknown_token"
}
// ValidateTokenExchangeRequest implements the op.TokenExchangeStorage interface
func (s *HybridStorage) ValidateTokenExchangeRequest(_ context.Context, request op.TokenExchangeRequest) error {
if request.GetRequestedTokenType() == "" {
request.SetRequestedTokenType(oidc.RefreshTokenType)
}
requestedType := request.GetExchangeSubjectTokenType()
proofType := request.GetExchangeSubjectTokenType()
switch proofType {
case oidc.AccessTokenType:
if requestedType != oidc.AccessTokenType {
return fmt.Errorf("exchanging %s for %s is not supported", tokenTypeToName(proofType), tokenTypeToName(requestedType))
}
case oidc.IDTokenType:
return fmt.Errorf("exchanging %s for any token type is not supported", tokenTypeToName(proofType))
case oidc.RefreshTokenType:
if requestedType != oidc.AccessTokenType && requestedType != oidc.RefreshTokenType {
return fmt.Errorf("exchanging %s for %s is not supported", tokenTypeToName(proofType), tokenTypeToName(requestedType))
}
default:
return fmt.Errorf("exchange subject type (%s) is not supported", proofType)
}
for _, aud := range request.GetAudience() {
if aud != common.ClaimAudienceOpenZiti && aud != common.ClaimLegacyNative {
return fmt.Errorf("invalid audience requested [%s]", aud)
}
}
scopes := request.GetScopes()
if len(scopes) == 1 && scopes[0] == "" {
//no scopes supplied, this is okay, do nothing, fill with defaults
} else {
for _, scope := range request.GetScopes() {
if scope != oidc.ScopeOfflineAccess && scope != oidc.ScopeOpenID {
return fmt.Errorf("invalid scope requested [%s]", scope)
}
}
}
return nil
}
func (s *HybridStorage) CreateTokenExchangeRequest(_ context.Context, _ op.TokenExchangeRequest) error {
return nil
}
// GetPrivateClaimsFromTokenExchangeRequest implements the op.TokenExchangeStorage interface
func (s *HybridStorage) GetPrivateClaimsFromTokenExchangeRequest(ctx context.Context, request op.TokenExchangeRequest) (claims map[string]interface{}, err error) {
claims, err = s.getPrivateClaims(ctx, "", request.GetClientID(), request.GetScopes())
if err != nil {
return nil, err
}
for k, v := range s.getTokenExchangeClaims(ctx, request) {
claims = appendClaim(claims, k, v)
}
return claims, nil
}
// SetUserinfoFromTokenExchangeRequest implements the op.TokenExchangeStorage interface
func (s *HybridStorage) SetUserinfoFromTokenExchangeRequest(ctx context.Context, userinfo *oidc.UserInfo, request op.TokenExchangeRequest) error {
err := s.setInfo(userinfo, request.GetSubject(), request.GetScopes())
if err != nil {
return err
}
for k, v := range s.getTokenExchangeClaims(ctx, request) {
userinfo.AppendClaims(k, v)
}
return nil
}
func (s *HybridStorage) getTokenExchangeClaims(_ context.Context, _ op.TokenExchangeRequest) (claims map[string]interface{}) {
return claims
}
func appendClaim(claims map[string]interface{}, claim string, value interface{}) map[string]interface{} {
if claims == nil {
claims = make(map[string]interface{})
}
claims[claim] = value
return claims
}
type deviceAuthorizationEntry struct {
deviceCode string
userCode string
state *op.DeviceAuthorizationState
}
// StoreDeviceAuthorization implements op.DeviceAuthorizationStorage
func (s *HybridStorage) StoreDeviceAuthorization(_ context.Context, clientID, deviceCode, userCode string, expires time.Time, scopes []string) error {
if _, ok := s.clients.Get(clientID); !ok {
return errors.New("client not found")
}
if _, ok := s.userCodes.Get(userCode); ok {
return op.ErrDuplicateUserCode
}
entry := deviceAuthorizationEntry{
deviceCode: deviceCode,
userCode: userCode,
state: &op.DeviceAuthorizationState{
ClientID: clientID,
Scopes: scopes,
Expires: expires,
},
}
s.deviceCodes.Set(deviceCode, entry)
s.userCodes.Set(userCode, deviceCode)
return nil
}
// GetDeviceAuthorizatonState implements op.DeviceAuthorizationStorage
func (s *HybridStorage) GetDeviceAuthorizatonState(ctx context.Context, clientID, deviceCode string) (*op.DeviceAuthorizationState, error) {
if ctx.Err() != nil {
return nil, ctx.Err()
}
entry, ok := s.deviceCodes.Get(deviceCode)
if !ok || entry.state.ClientID != clientID {
return nil, errors.New("device code not found for client") // is there a standard not found error in the framework?
}
return entry.state, nil
}
// GetDeviceAuthorizationByUserCode implements op.DeviceAuthorizationStorage
func (s *HybridStorage) GetDeviceAuthorizationByUserCode(_ context.Context, userCode string) (*op.DeviceAuthorizationState, error) {
code, ok := s.userCodes.Get(userCode)
if !ok {
return nil, errors.New("user code not found")
}
entry, ok := s.deviceCodes.Get(code)
if !ok {
return nil, errors.New("user code not found")
}
return entry.state, nil
}
// CompleteDeviceAuthorization implements op.DeviceAuthorizationStorage
func (s *HybridStorage) CompleteDeviceAuthorization(_ context.Context, userCode, subject string) error {
code, ok := s.userCodes.Get(userCode)
if !ok {
return errors.New("user code not found")
}
entry, ok := s.deviceCodes.Get(code)
if !ok {
return errors.New("user code not found")
}
entry.state.Subject = subject
entry.state.Done = true
return nil
}
// DenyDeviceAuthorization implements op.DeviceAuthorizationStorage
func (s *HybridStorage) DenyDeviceAuthorization(_ context.Context, userCode string) error {
code, ok := s.userCodes.Get(userCode)
if !ok {
return errors.New("device code not found")
}
authEntry, ok := s.deviceCodes.Get(code)
if !ok {
return errors.New("device auth entry not found")
}
authEntry.state.Denied = true
return nil
}
// AuthRequestDone is used by testing and is not required to implement op.Storage
func (s *HybridStorage) AuthRequestDone(id string) error {
if req, ok := s.authRequests.Get(id); ok {
if req.HasFullAuth() {
return nil
}
return errors.New("additional authentication interactions are required")
}
return errors.New("request not found")
}
// ClientCredentials implements op.ClientCredentialsStorage
func (s *HybridStorage) ClientCredentials(_ context.Context, clientID, clientSecret string) (op.Client, error) {
client, ok := s.serviceUsers.Get(clientID)
if !ok {
return nil, errors.New("wrong service user or password")
}
if client.secret != clientSecret {
return nil, errors.New("wrong service user or password")
}
return client, nil
}
// ClientCredentialsTokenRequest implements op.ClientCredentialsStorage
func (s *HybridStorage) ClientCredentialsTokenRequest(_ context.Context, clientID string, scopes []string) (op.TokenRequest, error) {
client, ok := s.serviceUsers.Get(clientID)
if !ok {
return nil, errors.New("wrong service user or password")
}
return &oidc.JWTTokenRequest{
Subject: client.id,
Audience: []string{clientID},
Scopes: scopes,
}, nil
}
func getAccessToken(r *http.Request) (string, error) {
authHeader := r.Header.Get("authorization")
if authHeader == "" {
return "", errors.New("no auth header")
}
parts := strings.Split(authHeader, "Bearer ")
if len(parts) != 2 {
return "", errors.New("invalid auth header")
}
return parts[1], nil
}