Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TT-13391] Move upstream OAuth to EE #6684

Merged
merged 52 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
924680c
TT-13391, initial commit
andrei-tyk Oct 29, 2024
47661d7
TT-13391, moved upstream oauth and all required dependencies (ctx/eve…
andrei-tyk Oct 29, 2024
0488b65
TT-13391, cleaned up server.go of unused code
andrei-tyk Oct 29, 2024
30c1f3c
Merge branch 'master' into TT-13391-move-upstream-o-auth-to-ee
andrei-tyk Oct 29, 2024
7f7ddb6
TT-13391, moved context methods back to ctx package
andrei-tyk Oct 30, 2024
ece4b77
Revert changes and alias model.EventMetaDefault
Oct 30, 2024
8dcc979
Revert fireEvent rename
Oct 30, 2024
d25dd1b
Revert rename of ctxGetData
Oct 30, 2024
08060ce
Revert broken test anyway
Oct 30, 2024
39d9863
Fix scope
Oct 30, 2024
1ae231a
Remove dependency interfaces
Oct 30, 2024
680488a
Get rid of gateway api extensions
Oct 30, 2024
3a7cfc2
Propose httpctx
Oct 30, 2024
e312a93
Revert httputil.Ctx
Oct 30, 2024
46424d0
Update httpctx build
Oct 30, 2024
dde1f09
Clean up EncodeRequestToEvent with alias, fix reverse proxy test
Oct 30, 2024
8755694
Revert ctx pkg
Oct 30, 2024
a71c123
Add setCtxValue shim
Oct 30, 2024
ce41f7f
Add setContext shim
Oct 30, 2024
2459516
Some import reverts
Oct 30, 2024
c53531f
Revert needless ws change
Oct 30, 2024
d8f15ea
Put down EmitUpstreamOAuth, delete APISpec
Oct 30, 2024
f92a52a
Clean up storage coupling
Oct 30, 2024
b2ae0e5
Decouple storage
Oct 30, 2024
4c32859
Clean up Provider{} setup
Oct 30, 2024
573f4d8
Update ee/middleware/upstreamoauth/middleware.go
titpetric Oct 30, 2024
9f86f25
Update ee/middleware/upstreamoauth/middleware.go
titpetric Oct 30, 2024
4c037f2
Rename to FireEvent
Oct 30, 2024
5dd8a41
Clean up stuttering naming
Oct 30, 2024
0a00ad8
Rename PasswordClient
Oct 30, 2024
bb6498b
Revert whitespace
Oct 30, 2024
fa647f2
Revert whitespace
Oct 30, 2024
72adb18
Move cache to new file, reorder args
Oct 30, 2024
7c506ac
Move to provider_test
Oct 30, 2024
68d9ee8
Make test change smaller
Oct 30, 2024
c387ebb
Whitespace for clarity
Oct 30, 2024
fa259eb
Rename tests by convention
Oct 30, 2024
f5d504d
TT-13391, fixed clashing keys
andrei-tyk Oct 30, 2024
7c4d96d
TT-13391, fixed usage of GetPaddedString instead of RightPad2Len
andrei-tyk Oct 30, 2024
d641292
Merge branch 'master' into TT-13391-move-upstream-o-auth-to-ee
andrei-tyk Oct 30, 2024
6a9f162
TT-13391, fixed linting issues
andrei-tyk Oct 30, 2024
c096f8a
TT-13391, fixed schema for extraMetadata on OAS
andrei-tyk Oct 30, 2024
b12ab41
TT-13391, cr feedback implementation
andrei-tyk Oct 30, 2024
e015c73
Merge branch 'master' into TT-13391-move-upstream-o-auth-to-ee
andrei-tyk Oct 31, 2024
f35573b
Merge branch 'master' into TT-13391-move-upstream-o-auth-to-ee
andrei-tyk Oct 31, 2024
22d8d12
TT-13391, fixed context pointer magic
andrei-tyk Oct 31, 2024
822305f
Merge branch 'master' into TT-13391-move-upstream-o-auth-to-ee
andrei-tyk Oct 31, 2024
d2b33f0
Merge remote-tracking branch 'origin/master' into TT-13391-move-upstr…
andrei-tyk Nov 3, 2024
9f4fa71
TT-13391, fixed merge conflict
andrei-tyk Nov 3, 2024
015f7d2
TT-13391, test is outdated as our new get context function accounts f…
andrei-tyk Nov 5, 2024
6373753
TT-13391, code quality improvements
andrei-tyk Nov 5, 2024
affb5b6
TT-13391, linting
andrei-tyk Nov 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions ee/middleware/upstreamoauth/middleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package upstreamoauth

import (
"fmt"
"net/http"

"github.com/sirupsen/logrus"

"github.com/TykTechnologies/tyk/header"
"github.com/TykTechnologies/tyk/internal/event"
"github.com/TykTechnologies/tyk/internal/httputil"
"github.com/TykTechnologies/tyk/internal/model"
)

// Middleware implements upstream OAuth middleware.
type Middleware struct {
Spec model.MergedAPI
Gw Gateway

Base BaseMiddleware

clientCredentialsStorageHandler Storage
passwordStorageHandler Storage
}

// Middleware implements model.Middleware.
var _ model.Middleware = &Middleware{}

// NewMiddleware returns a new instance of Middleware.
func NewMiddleware(gw Gateway, mw BaseMiddleware, spec model.MergedAPI, ccStorageHandler Storage, pwStorageHandler Storage) *Middleware {
return &Middleware{
Base: mw,
Gw: gw,
Spec: spec,
clientCredentialsStorageHandler: ccStorageHandler,
passwordStorageHandler: pwStorageHandler,
}
}

// Logger returns a logger with middleware filled out.
func (m *Middleware) Logger() *logrus.Entry {
return m.Base.Logger().WithField("mw", m.Name())
}

// Name returns the name for the middleware.
func (m *Middleware) Name() string {
return MiddlewareName
}

// EnabledForSpec checks if streaming is enabled on the config.
func (m *Middleware) EnabledForSpec() bool {
if !m.Spec.UpstreamAuth.IsEnabled() {
return false
}

if !m.Spec.UpstreamAuth.OAuth.Enabled {
return false
}

return true
}

// Init initializes the middleware.
func (m *Middleware) Init() {
m.Logger().Debug("Initializing Upstream basic auth Middleware")
}

// ProcessRequest will handle upstream OAuth.
func (m *Middleware) ProcessRequest(_ http.ResponseWriter, r *http.Request, _ interface{}) (error, int) {
provider, err := NewOAuthHeaderProvider(m.Spec.UpstreamAuth.OAuth)
if err != nil {
return fmt.Errorf("failed to get OAuth header provider: %w", err), http.StatusInternalServerError
}

payload, err := provider.getOAuthToken(r, m)
if err != nil {
return fmt.Errorf("failed to get OAuth token: %w", err), http.StatusInternalServerError
}

upstreamOAuthProvider := Provider{
HeaderName: header.Authorization,
AuthValue: payload,
}

headerName := provider.getHeaderName(m)
if headerName != "" {
upstreamOAuthProvider.HeaderName = headerName
}

if provider.headerEnabled(m) {
headerName := provider.getHeaderName(m)
if headerName != "" {
upstreamOAuthProvider.HeaderName = headerName
}
}

httputil.SetUpstreamAuth(r, upstreamOAuthProvider)
return nil, http.StatusOK
}

// FireEvent emits an upstream OAuth event with an optional custom message.
func (mw *Middleware) FireEvent(r *http.Request, e event.Event, message string, apiId string) {
if message == "" {
message = event.String(e)
}
mw.Base.FireEvent(e, EventUpstreamOAuthMeta{
EventMetaDefault: model.NewEventMetaDefault(r, message),
APIID: apiId,
})
}
54 changes: 54 additions & 0 deletions ee/middleware/upstreamoauth/model.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package upstreamoauth

import (
"time"

"github.com/TykTechnologies/tyk/apidef"
"github.com/TykTechnologies/tyk/ctx"
"github.com/TykTechnologies/tyk/internal/httpctx"
"github.com/TykTechnologies/tyk/internal/model"
)

const (
ErrorEventName = "UpstreamOAuthError"
MiddlewareName = "UpstreamOAuth"

ClientCredentialsAuthorizeType = "clientCredentials"
PasswordAuthorizeType = "password"
andrei-tyk marked this conversation as resolved.
Show resolved Hide resolved
)

// BaseMiddleware is the subset of BaseMiddleware APIs that the middleware uses.
type BaseMiddleware interface {
model.LoggerProvider
FireEvent(name apidef.TykEvent, meta interface{})
}

// Gateway is the subset of Gateway APIs that the middleware uses.
type Gateway interface {
model.ConfigProvider
}

// Type Storage is a subset of storage.RedisCluster
type Storage interface {
GetKey(key string) (string, error)
SetKey(string, string, int64) error
Lock(key string, timeout time.Duration) (bool, error)
}

type ClientCredentialsOAuthProvider struct{}

type PerAPIClientCredentialsOAuthProvider struct{}

type PasswordOAuthProvider struct{}

type TokenData struct {
Token string `json:"token"`
ExtraMetadata map[string]interface{} `json:"extra_metadata"`
}

var (
ctxData = httpctx.NewValue[map[string]any](ctx.ContextData)

CtxGetData = ctxData.Get
CtxSetData = ctxData.Set
)
204 changes: 204 additions & 0 deletions ee/middleware/upstreamoauth/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
package upstreamoauth

import (
"crypto/sha256"
"encoding/hex"
"fmt"
"net/http"
"strings"
"time"

"github.com/sirupsen/logrus"
"golang.org/x/oauth2"
oauth2clientcredentials "golang.org/x/oauth2/clientcredentials"

"github.com/TykTechnologies/tyk/apidef"
"github.com/TykTechnologies/tyk/internal/model"
)

// Provider implements upstream auth provider.
type Provider struct {
// Logger is the logger to be used.
Logger *logrus.Entry
// HeaderName is the header name to be used to fill upstream auth with.
HeaderName string
// AuthValue is the value of auth header.
AuthValue string
}

// Fill sets the request's HeaderName with AuthValue
func (u Provider) Fill(r *http.Request) {
if r.Header.Get(u.HeaderName) != "" {
u.Logger.WithFields(logrus.Fields{
"header": u.HeaderName,
}).Info("Authorization header conflict detected: Client header overwritten by Gateway upstream authentication header.")
}
r.Header.Set(u.HeaderName, u.AuthValue)
}

type OAuthHeaderProvider interface {
andrei-tyk marked this conversation as resolved.
Show resolved Hide resolved
// getOAuthToken returns the OAuth token for the request.
getOAuthToken(r *http.Request, mw *Middleware) (string, error)
// getHeaderName returns the header name for the OAuth token.
getHeaderName(mw *Middleware) string
//
headerEnabled(mw *Middleware) bool
}

func NewOAuthHeaderProvider(oauthConfig apidef.UpstreamOAuth) (OAuthHeaderProvider, error) {
if !oauthConfig.IsEnabled() {
return nil, fmt.Errorf("upstream OAuth is not enabled")
}

switch {
case len(oauthConfig.AllowedAuthorizeTypes) == 0:
return nil, fmt.Errorf("no OAuth configuration selected")
case len(oauthConfig.AllowedAuthorizeTypes) > 1:
return nil, fmt.Errorf("both client credentials and password authentication are provided")
case oauthConfig.AllowedAuthorizeTypes[0] == ClientCredentialsAuthorizeType:
return &ClientCredentialsOAuthProvider{}, nil
case oauthConfig.AllowedAuthorizeTypes[0] == PasswordAuthorizeType:
return &PasswordOAuthProvider{}, nil
default:
return nil, fmt.Errorf("no valid OAuth configuration provided")
}
}

func (p *ClientCredentialsOAuthProvider) getOAuthToken(r *http.Request, mw *Middleware) (string, error) {
client := ClientCredentialsClient{mw}
token, err := client.GetToken(r)
if err != nil {
return handleOAuthError(r, mw, err)
}

return fmt.Sprintf("Bearer %s", token), nil
}

func handleOAuthError(r *http.Request, mw *Middleware, err error) (string, error) {
mw.FireEvent(r, ErrorEventName, err.Error(), mw.Spec.APIID)
return "", err
}

func (p *ClientCredentialsOAuthProvider) getHeaderName(OAuthSpec *Middleware) string {
return OAuthSpec.Spec.UpstreamAuth.OAuth.ClientCredentials.Header.Name
}

func (p *ClientCredentialsOAuthProvider) headerEnabled(OAuthSpec *Middleware) bool {
return OAuthSpec.Spec.UpstreamAuth.OAuth.ClientCredentials.Header.Enabled
}

func newOAuth2ClientCredentialsConfig(OAuthSpec *Middleware) oauth2clientcredentials.Config {
return oauth2clientcredentials.Config{
ClientID: OAuthSpec.Spec.UpstreamAuth.OAuth.ClientCredentials.ClientID,
ClientSecret: OAuthSpec.Spec.UpstreamAuth.OAuth.ClientCredentials.ClientSecret,
TokenURL: OAuthSpec.Spec.UpstreamAuth.OAuth.ClientCredentials.TokenURL,
Scopes: OAuthSpec.Spec.UpstreamAuth.OAuth.ClientCredentials.Scopes,
}
}

func newOAuth2PasswordConfig(OAuthSpec *Middleware) oauth2.Config {
return oauth2.Config{
ClientID: OAuthSpec.Spec.UpstreamAuth.OAuth.PasswordAuthentication.ClientID,
ClientSecret: OAuthSpec.Spec.UpstreamAuth.OAuth.PasswordAuthentication.ClientSecret,
Endpoint: oauth2.Endpoint{
TokenURL: OAuthSpec.Spec.UpstreamAuth.OAuth.PasswordAuthentication.TokenURL,
},
Scopes: OAuthSpec.Spec.UpstreamAuth.OAuth.PasswordAuthentication.Scopes,
}
}

type ClientCredentialsClient struct {
mw *Middleware
}

type PasswordClient struct {
mw *Middleware
}

func generateClientCredentialsCacheKey(config apidef.UpstreamOAuth, apiId string) string {
key := fmt.Sprintf(
"cc-%s|%s|%s|%s",
apiId,
config.ClientCredentials.ClientID,
config.ClientCredentials.TokenURL,
strings.Join(config.ClientCredentials.Scopes, ","))

hash := sha256.New()
hash.Write([]byte(key))
return hex.EncodeToString(hash.Sum(nil))
}

func retryGetKeyAndLock(cacheKey string, cache Storage) (string, error) {
const maxRetries = 10
const retryDelay = 100 * time.Millisecond

var tokenData string
var err error

for i := 0; i < maxRetries; i++ {
tokenData, err = cache.GetKey(cacheKey)
if err == nil {
return tokenData, nil
}

lockKey := cacheKey + ":lock"
ok, err := cache.Lock(lockKey, time.Second*5)
if err == nil && ok {
return "", nil
}

time.Sleep(retryDelay)
}

return "", fmt.Errorf("failed to acquire lock after retries: %w", err)
}

func SetExtraMetadata(r *http.Request, keyList []string, metadata map[string]interface{}) {
contextDataObject := CtxGetData(r)
if contextDataObject == nil {
contextDataObject = make(map[string]interface{})
}
for _, key := range keyList {
if val, ok := metadata[key]; ok && val != "" {
contextDataObject[key] = val
}
}
CtxSetData(r, contextDataObject)
}

// EventUpstreamOAuthMeta is the metadata structure for an upstream OAuth event
type EventUpstreamOAuthMeta struct {
model.EventMetaDefault
APIID string
}

func (p *PasswordOAuthProvider) getOAuthToken(r *http.Request, mw *Middleware) (string, error) {
client := PasswordClient{mw}
token, err := client.GetToken(r)
if err != nil {
return handleOAuthError(r, mw, err)
}

return fmt.Sprintf("Bearer %s", token), nil
}

func (p *PasswordOAuthProvider) getHeaderName(OAuthSpec *Middleware) string {
return OAuthSpec.Spec.UpstreamAuth.OAuth.PasswordAuthentication.Header.Name
}

func (p *PasswordOAuthProvider) headerEnabled(OAuthSpec *Middleware) bool {
return OAuthSpec.Spec.UpstreamAuth.OAuth.PasswordAuthentication.Header.Enabled
}

func generatePasswordOAuthCacheKey(config apidef.UpstreamOAuth, apiId string) string {
key := fmt.Sprintf(
"pw-%s|%s|%s|%s",
apiId,
config.PasswordAuthentication.ClientID,
config.PasswordAuthentication.ClientSecret,
strings.Join(config.PasswordAuthentication.Scopes, ","))

hash := sha256.New()
hash.Write([]byte(key))
return hex.EncodeToString(hash.Sum(nil))
}
26 changes: 26 additions & 0 deletions ee/middleware/upstreamoauth/provider_client_credentials.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package upstreamoauth

import (
"context"
"net/http"

"golang.org/x/oauth2"
)

func (cache *ClientCredentialsClient) ObtainToken(ctx context.Context) (*oauth2.Token, error) {
cfg := newOAuth2ClientCredentialsConfig(cache.mw)
tokenSource := cfg.TokenSource(ctx)
return tokenSource.Token()
}

func (cache *ClientCredentialsClient) GetToken(r *http.Request) (string, error) {
cacheKey := generateClientCredentialsCacheKey(cache.mw.Spec.UpstreamAuth.OAuth, cache.mw.Spec.APIID)
secret := cache.mw.Gw.GetConfig().Secret
extraMetadata := cache.mw.Spec.UpstreamAuth.OAuth.ClientCredentials.ExtraMetadata

obtainTokenFunc := func(ctx context.Context) (*oauth2.Token, error) {
return cache.ObtainToken(ctx)
}

return getToken(r, cacheKey, obtainTokenFunc, secret, extraMetadata, cache.mw.clientCredentialsStorageHandler)
}
Loading
Loading