Files
ziti/controller/oidc_auth/storage.go
T
2024-04-23 15:04:10 -04:00

1103 lines
31 KiB
Go

package oidc_auth
import (
"context"
"crypto"
"crypto/sha1"
"encoding/json"
"errors"
"fmt"
"github.com/openziti/foundation/v2/errorz"
"net/http"
"strings"
"sync"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/michaelquigley/pfxlog"
"github.com/openziti/foundation/v2/stringz"
"github.com/openziti/ziti/common"
"github.com/openziti/ziti/controller/apierror"
"github.com/openziti/ziti/controller/change"
"github.com/openziti/ziti/controller/model"
"github.com/openziti/ziti/controller/models"
cmap "github.com/orcaman/concurrent-map/v2"
"github.com/google/uuid"
"gopkg.in/square/go-jose.v2"
"github.com/zitadel/oidc/v2/pkg/oidc"
"github.com/zitadel/oidc/v2/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(ctx *change.Context, authRequestId string) (string, error)
CompleteTotpEnrollment(ctx *change.Context, authRequestId, 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)
}
func NewRevocation(tokenId string, expiresAt time.Time) *model.Revocation {
return &model.Revocation{
BaseEntity: models.BaseEntity{
Id: tokenId,
},
ExpiresAt: expiresAt,
}
}
// 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
keys cmap.ConcurrentMap[string, *pubKey]
}
func (s *HybridStorage) StartTotpEnrollment(changeCtx *change.Context, authRequestId string) (string, error) {
authRequest, err := s.GetAuthRequest(authRequestId)
if err != nil {
return "", errorz.NewUnauthorized()
}
return s.env.GetManagers().Mfa.CreateForIdentityId(authRequest.IdentityId, changeCtx)
}
func (s *HybridStorage) CompleteTotpEnrollment(changeCtx *change.Context, authRequestId, code string) error {
authRequest, err := s.GetAuthRequest(authRequestId)
if err != nil {
return errorz.NewUnauthorized()
}
return s.env.GetManagers().Mfa.CompleteTotpEnrollment(authRequest.IdentityId, code, changeCtx)
}
func (s *HybridStorage) AddClient(client *Client) {
s.clients.Set(client.id, client)
}
var _ Storage = &HybridStorage{}
func NewStorage(kid string, publicKey crypto.PublicKey, privateKey crypto.PrivateKey, singingMethod jwt.SigningMethod, config *Config, env model.Env) *HybridStorage {
store := &HybridStorage{
env: env,
signingKey: key{
id: kid,
algorithm: jose.SignatureAlgorithm(singingMethod.Alg()),
privateKey: privateKey,
publicKey: 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](),
}
store.start()
return store
}
// start will run Clean every 10 seconds
func (s *HybridStorage) start() {
s.startOnce.Do(func() {
closeNotify := s.env.GetHostController().GetCloseNotifyChannel()
ticker := time.NewTicker(10 * time.Second)
go func() {
for {
select {
case <-ticker.C:
s.Clean()
case <-closeNotify:
ticker.Stop()
return
}
}
}()
})
}
// 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.IdentityId()
authRequest.AddAmr(authCtx.GetMethod())
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.SecondaryTotpRequired = mfa != nil && mfa.IsVerified
if authCtx.GetMethod() == AuthMethodCert {
if len(authRequest.PeerCerts) == 0 {
authRequest.PeerCerts = authCtx.GetCerts()
}
}
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(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(request op.TokenRequest) (string, *common.AccessClaims, error) {
now := time.Now()
claims := &common.AccessClaims{
AccessTokenClaims: oidc.AccessTokenClaims{
TokenClaims: oidc.TokenClaims{
JWTID: uuid.NewString(),
Issuer: s.config.Issuer,
Subject: request.GetSubject(),
Audience: []string{common.ClaimAudienceOpenZiti},
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{},
}
switch req := request.(type) {
case *AuthRequest:
claims.CustomClaims.ApiSessionId = req.ApiSessionId
claims.CustomClaims.ApplicationId = req.ClientID
claims.CustomClaims.ConfigTypes = req.ConfigTypes
claims.AuthenticationMethodsReferences = req.GetAMR()
claims.CustomClaims.CertFingerprints = req.GetCertFingerprints()
claims.CustomClaims.EnvInfo = req.EnvInfo
claims.CustomClaims.SdkInfo = req.SdkInfo
claims.CustomClaims.RemoteAddress = req.RemoteAddress
claims.AuthTime = oidc.Time(req.AuthTime.Unix())
claims.AccessTokenClaims.AuthenticationMethodsReferences = req.GetAMR()
case *RefreshTokenRequest:
claims.CustomClaims = req.CustomClaims
claims.AuthTime = req.AuthTime
claims.AccessTokenClaims.AuthenticationMethodsReferences = req.GetAMR()
case op.TokenExchangeRequest:
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, fmt.Errorf("invalid token type: %s", claims.CustomClaims.Type)
}
}
claims.CustomClaims = subjectClaims.CustomClaims
claims.AccessTokenClaims.AuthenticationMethodsReferences = req.GetAMR()
}
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)
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(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)
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 {
return nil, nil, fmt.Errorf("failed to parse token")
}
if !parsedToken.Valid {
return nil, nil, fmt.Errorf("invalid refresh_token")
}
if refreshClaims.Type != common.TokenTypeRefresh {
return nil, nil, errors.New("invalid token type")
}
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 {
return nil, nil, fmt.Errorf("failed to parse token")
}
if !parsedToken.Valid {
return nil, nil, fmt.Errorf("invalid refresh_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)
return &RefreshTokenRequest{*token}, err
}
// TerminateSession implements the op.Storage interface
func (s *HybridStorage) TerminateSession(_ context.Context, identityId string, clientID string) error {
now := time.Now()
return s.saveRevocation(NewRevocation(identityId+","+clientID, now.Add(s.config.MaxTokenDuration())))
}
// 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())
if err := s.saveRevocation(revocation); err != nil {
return oidc.ErrServerError()
}
}
revocation := NewRevocation(tokenIDOrToken, time.Now().Add(s.config.MaxTokenDuration()))
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.GetHostController().GetPeerSigners()
for _, cert := range signers {
kid := fmt.Sprintf("%s", 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, fmt.Errorf("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 fmt.Errorf("client not found")
}
//this isn't used and is plain text comparison
if client.secret != clientSecret {
return fmt.Errorf("invalid 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 errors.New("token is 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, scopes []string) (claims map[string]interface{}, err error) {
if err != nil {
return nil, err
}
tokenState, err := TokenStateFromContext(ctx)
if err != nil {
return nil, err
}
tsClaims, err := tokenState.AccessClaims.CustomClaims.ToMap()
return tsClaims, err
}
// 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, _ := s.env.GetServerJwtSigner().Generate(claims)
return token, claims, nil
}
func (s *HybridStorage) saveRevocation(revocation *model.Revocation) error {
return s.env.GetManagers().Revocation.Create(revocation, change.New())
}
func (s *HybridStorage) renewRefreshToken(currentRefreshToken string) (string, *common.RefreshClaims, error) {
_, refreshClaims, err := s.parseRefreshToken(currentRefreshToken)
if err != nil {
return "", nil, fmt.Errorf("invalid refresh token")
}
if err = s.saveRevocation(NewRevocation(refreshClaims.JWTID, refreshClaims.Expiration.AsTime())); err != nil {
return "", nil, err
}
newRefreshClaims := &common.RefreshClaims{
IDTokenClaims: refreshClaims.IDTokenClaims,
CustomClaims: refreshClaims.CustomClaims,
}
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, _ := s.env.GetServerJwtSigner().Generate(newRefreshClaims)
return token, newRefreshClaims, err
}
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)
}
allowedScopes := []string{oidc.ScopeOpenID}
for _, scope := range request.GetScopes() {
if scope == oidc.ScopeOfflineAccess {
allowedScopes = append(allowedScopes, scope)
}
}
request.SetCurrentScopes(allowedScopes)
return nil
}
func (s *HybridStorage) CreateTokenExchangeRequest(_ context.Context, req 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
}