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-13185] Implement Password Flow OAuth #6649

Open
wants to merge 33 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
9805eb2
TT-13184, working version, some refactoring to be done
andrei-tyk Oct 14, 2024
040aca6
TT-13184, applied some code review feedback from the retry-request va…
andrei-tyk Oct 14, 2024
6b4ef4a
TT-13184, fixed variable naming
andrei-tyk Oct 14, 2024
e6c0eb5
TT-13184, fixed comment of variable that was failing CI
andrei-tyk Oct 14, 2024
40a85a0
TT-13184, godocs fixes
andrei-tyk Oct 14, 2024
04b2cb9
TT-13184, CR feedback implementation
andrei-tyk Oct 14, 2024
c020678
TT-13184, fixed typo
andrei-tyk Oct 14, 2024
a359234
TT-13184, more godocs
andrei-tyk Oct 14, 2024
6f26abc
TT-13184, make lint
andrei-tyk Oct 14, 2024
d4bcd96
TT-13184, removed extra fields
andrei-tyk Oct 14, 2024
57c4ad7
Merge branch 'master' into TT-13184-upstream-oauth2
andrei-tyk Oct 14, 2024
3178207
TT-13184, ensured cached token expires correctly in redis; added some…
andrei-tyk Oct 16, 2024
6114705
Merge branch 'TT-13184-upstream-oauth2' of github.com:TykTechnologies…
andrei-tyk Oct 16, 2024
3ad4f07
Merge branch 'master' into TT-13184-upstream-oauth2
andrei-tyk Oct 16, 2024
26e19ce
TT-13184, CR feedback implementation
andrei-tyk Oct 16, 2024
1e93a4d
TT-13184, fixed golang-ci lint actionable inputs
andrei-tyk Oct 16, 2024
5143c21
Merge branch 'master' into TT-13184-upstream-oauth2
andrei-tyk Oct 16, 2024
909ef6b
TT-13185, initial implementation of password oauth2 flow
andrei-tyk Oct 16, 2024
9309853
TT-13184, linting issues
andrei-tyk Oct 16, 2024
ad6ff30
TT-13185, working variant
andrei-tyk Oct 16, 2024
fe46a0b
TT-13184, fixed schema issues
andrei-tyk Oct 16, 2024
1f74f51
TT-13184, fixed schema issues part 2
andrei-tyk Oct 16, 2024
90599fd
Merge branch 'TT-13184-upstream-oauth2' into TT-13185-implement-oauth…
andrei-tyk Oct 16, 2024
95d8107
TT-13185, linting issues
andrei-tyk Oct 16, 2024
ef32f7e
TT-13185, fixed schema issues
andrei-tyk Oct 16, 2024
e49443e
TT-13184, fixed linter issues
andrei-tyk Oct 16, 2024
bb1b3e2
Merge branch 'TT-13184-upstream-oauth2' into TT-13185-implement-oauth…
andrei-tyk Oct 16, 2024
db442a6
TT-13185, linter fixes
andrei-tyk Oct 16, 2024
4f9b416
TT-13185, implemented CR feedback and added schema changes
andrei-tyk Oct 22, 2024
cad722f
Merge branch 'master' into TT-13185-implement-oauth-password-flow
andrei-tyk Oct 22, 2024
bed31cb
TT-13185, implemented CR feedback 2
andrei-tyk Oct 22, 2024
0870da6
TT-13185, implemented CR feedback 3
andrei-tyk Oct 22, 2024
a171643
TT-13185, moved headerName to clientCredentials and passwordAuthentic…
andrei-tyk Oct 23, 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
21 changes: 21 additions & 0 deletions apidef/api_definitions.go
Original file line number Diff line number Diff line change
Expand Up @@ -808,11 +808,32 @@ type UpstreamOAuth struct {
Enabled bool `bson:"enabled" json:"enabled"`
// ClientCredentials holds the client credentials for upstream OAuth2 authentication.
ClientCredentials ClientCredentials `bson:"client_credentials" json:"client_credentials"`
// PasswordAuthentication holds the configuration for upstream OAauth password authentication flow.
PasswordAuthentication PasswordAuthentication `bson:"password_authentication,omitempty" json:"passwordAuthentication,omitempty"`
// HeaderName is the custom header name to be used for upstream basic authentication.
// Defaults to `Authorization`.
HeaderName string `bson:"header_name" json:"header_name,omitempty"`
}

// PasswordAuthentication holds the configuration for upstream OAuth2 password authentication flow.
type PasswordAuthentication struct {
ClientAuthData
// Enabled activates upstream OAuth2 password authentication.
Enabled bool `bson:"enabled" json:"enabled"`
// Username is the username to be used for upstream OAuth2 password authentication.
Username string `bson:"username" json:"username"`
// Password is the password to be used for upstream OAuth2 password authentication.
Password string `bson:"password" json:"password"`
// TokenURL is the resource server's token endpoint
// URL. This is a constant specific to each server.
TokenURL string `bson:"token_url" json:"token_url"`
// Scopes specifies optional requested permissions.
Scopes []string `bson:"scopes" json:"scopes,omitempty"`

// TokenProvider is the OAuth2 password authentication flow token for internal use.
Token *oauth2.Token `bson:"-" json:"-"`
}

// ClientAuthData holds the client ID and secret for upstream OAuth2 authentication.
type ClientAuthData struct {
// ClientID is the application's ID.
Expand Down
37 changes: 33 additions & 4 deletions apidef/oas/schema/x-tyk-api-gateway.json
Original file line number Diff line number Diff line change
Expand Up @@ -2052,13 +2052,42 @@
"tokenUrl": {
"type": "string"
},
"scopes":{
"type": ["array", "null"]
"scopes": {
"type": [
"array",
"null"
]
}
}
},
"headerName": {
"type": "string"
"passwordAuthentication": {
"type": "object",
"properties": {
"clientId": {
"type": "string"
},
"clientSecret": {
"type": "string"
},
"tokenUrl": {
"type": "string"
},
"scopes": {
"type": [
"array",
"null"
]
},
"username": {
"type": "string"
},
"password": {
"type": "string"
}
},
"headerName": {
"type": "string"
}
}
}
}
Expand Down
61 changes: 58 additions & 3 deletions apidef/oas/upstream.go
Original file line number Diff line number Diff line change
Expand Up @@ -639,17 +639,40 @@ type UpstreamOAuth struct {
Enabled bool `bson:"enabled" json:"enabled"`
// ClientCredentials holds the configuration for OAuth2 Client Credentials flow.
ClientCredentials *ClientCredentials `bson:"clientCredentials,omitempty" json:"clientCredentials,omitempty"`
// PasswordAuthentication holds the configuration for upstream OAauth password authentication flow.
PasswordAuthentication *PasswordAuthentication `bson:"passwordAuthentication,omitempty" json:"passwordAuthentication,omitempty"`
// HeaderName is the custom header name to be used for upstream basic authentication.
// Defaults to `Authorization`.
HeaderName string `bson:"headerName" json:"headerName"`
}

// ClientCredentials holds the configuration for OAuth2 Client Credentials flow.
type ClientCredentials struct {
// PasswordAuthentication holds the configuration for upstream OAuth2 password authentication flow.
type PasswordAuthentication struct {
ClientAuthData
// Enabled activates upstream OAuth2 password authentication.
Enabled bool `bson:"enabled" json:"enabled"`
// Username is the username to be used for upstream OAuth2 password authentication.
Username string `bson:"username" json:"username"`
// Password is the password to be used for upstream OAuth2 password authentication.
Password string `bson:"password" json:"password"`
// TokenURL is the resource server's token endpoint
// URL. This is a constant specific to each server.
TokenURL string `bson:"tokenURL" json:"tokenURL"`
// Scopes specifies optional requested permissions.
Scopes []string `bson:"scopes" json:"scopes,omitempty"`
}

// ClientAuthData holds the client ID and secret for OAuth2 authentication.
type ClientAuthData struct {
// ClientID is the application's ID.
ClientID string `bson:"clientID" json:"clientID"`
ClientID string `bson:"clientId" json:"clientId"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Breaking change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that was some unintended typo , wil revert it to the one agreed in the schema

// ClientSecret is the application's secret.
ClientSecret string `bson:"clientSecret" json:"clientSecret"`
}

// ClientCredentials holds the configuration for OAuth2 Client Credentials flow.
type ClientCredentials struct {
ClientAuthData
// TokenURL is the resource server's token endpoint
// URL. This is a constant specific to each server.
TokenURL string `bson:"tokenURL" json:"tokenURL"`
Expand All @@ -664,6 +687,14 @@ func (c *ClientCredentials) Fill(api apidef.ClientCredentials) {
c.Scopes = api.Scopes
}

func (p *PasswordAuthentication) Fill(api apidef.PasswordAuthentication) {
p.Enabled = api.Enabled
p.Username = api.Username
p.Password = api.Password
p.TokenURL = api.TokenURL
p.Scopes = api.Scopes
}

func (u *UpstreamOAuth) Fill(api apidef.UpstreamOAuth) {
u.Enabled = api.Enabled
u.HeaderName = api.HeaderName
Expand All @@ -675,6 +706,14 @@ func (u *UpstreamOAuth) Fill(api apidef.UpstreamOAuth) {
if ShouldOmit(u.ClientCredentials) {
u.ClientCredentials = nil
}

if u.PasswordAuthentication == nil {
u.PasswordAuthentication = &PasswordAuthentication{}
}
u.PasswordAuthentication.Fill(api.PasswordAuthentication)
if ShouldOmit(u.PasswordAuthentication) {
u.PasswordAuthentication = nil
}
}

func (c *ClientCredentials) ExtractTo(api *apidef.ClientCredentials) {
Expand All @@ -684,6 +723,14 @@ func (c *ClientCredentials) ExtractTo(api *apidef.ClientCredentials) {
api.Scopes = c.Scopes
}

func (p *PasswordAuthentication) ExtractTo(api *apidef.PasswordAuthentication) {
api.Enabled = p.Enabled
api.Username = p.Username
api.Password = p.Password
api.TokenURL = p.TokenURL
api.Scopes = p.Scopes
}

func (u *UpstreamOAuth) ExtractTo(api *apidef.UpstreamOAuth) {
api.Enabled = u.Enabled
api.HeaderName = u.HeaderName
Expand All @@ -695,4 +742,12 @@ func (u *UpstreamOAuth) ExtractTo(api *apidef.UpstreamOAuth) {
}()
}
u.ClientCredentials.ExtractTo(&api.ClientCredentials)

if u.PasswordAuthentication == nil {
u.PasswordAuthentication = &PasswordAuthentication{}
defer func() {
u.PasswordAuthentication = nil
}()
}
u.PasswordAuthentication.ExtractTo(&api.PasswordAuthentication)
}
27 changes: 27 additions & 0 deletions apidef/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,33 @@ const Schema = `{
}
}
},
"password_authentication": {
"type": "object",
"properties": {
"client_id": {
"type": "string"
},
"client_secret": {
"type": "string"
},
"enabled": {
"type": "boolean"
},
"username": {
"type": "string"
},
"password": {
"type": "string"
},
"token_url": {
"type": "string"
},
"scopes": {
"type": ["array", "null"]
}
}
}
},
"header_name": {
"type": "string"
}
Expand Down
113 changes: 106 additions & 7 deletions gateway/mw_oauth2_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,63 @@ type ClientCredentialsOAuthProvider struct{}

type PerAPIClientCredentialsOAuthProvider struct{}

func newUpstreamOAuthClientCredentialsCache(connectionHandler *storage.ConnectionHandler) *upstreamOAuthClientCredentialsCache {
type PasswordOAuthProvider struct{}

func newUpstreamOAuthClientCredentialsCache(connectionHandler *storage.ConnectionHandler) UpstreamOAuthCache {
return &upstreamOAuthClientCredentialsCache{RedisCluster: storage.RedisCluster{KeyPrefix: "upstreamOAuthCC-", ConnectionHandler: connectionHandler}}
}

func newUpstreamOAuthPasswordCache(connectionHandler *storage.ConnectionHandler) UpstreamOAuthCache {
return &upstreamOAuthPasswordCache{RedisCluster: storage.RedisCluster{KeyPrefix: "upstreamOAuthPW-", ConnectionHandler: connectionHandler}}
}

type upstreamOAuthClientCredentialsCache struct {
storage.RedisCluster
}

type upstreamOAuthPasswordCache struct {
storage.RedisCluster
}

func (cache *upstreamOAuthPasswordCache) getToken(r *http.Request, OAuthSpec *UpstreamOAuth) (string, error) {
cacheKey := generatePasswordOAuthCacheKey(OAuthSpec.Spec.UpstreamAuth.OAuth, OAuthSpec.Spec.APIID)

tokenString, err := retryGetKeyAndLock(cacheKey, &cache.RedisCluster)
if err != nil {
return "", err
}

if tokenString != "" {
decryptedToken := decrypt(getPaddedSecret(OAuthSpec.Gw.GetConfig().Secret), tokenString)
return decryptedToken, nil
}

token, err := cache.obtainToken(r.Context(), OAuthSpec)
if err != nil {
return "", err
}

encryptedToken := encrypt(getPaddedSecret(OAuthSpec.Gw.GetConfig().Secret), token.AccessToken)

ttl := time.Until(token.Expiry)
if err := setTokenInCache(cacheKey, encryptedToken, ttl, &cache.RedisCluster); err != nil {
return "", err
}

return token.AccessToken, nil
}

func (cache *upstreamOAuthPasswordCache) obtainToken(ctx context.Context, OAuthSpec *UpstreamOAuth) (*oauth2.Token, error) {
cfg := newOAuth2PasswordConfig(OAuthSpec)

token, err := cfg.PasswordCredentialsToken(ctx, OAuthSpec.Spec.UpstreamAuth.OAuth.PasswordAuthentication.Username, OAuthSpec.Spec.UpstreamAuth.OAuth.PasswordAuthentication.Password)
if err != nil {
return &oauth2.Token{}, err
}

return token, nil
}

type UpstreamOAuthCache interface {
// getToken returns the token from cache or issues a request to obtain it from the OAuth provider.
getToken(r *http.Request, OAuthSpec *UpstreamOAuth) (string, error)
Expand Down Expand Up @@ -102,8 +151,20 @@ func (OAuthSpec *UpstreamOAuth) ProcessRequest(_ http.ResponseWriter, r *http.Re
}

func getOAuthHeaderProvider(oauthConfig apidef.UpstreamOAuth) (OAuthHeaderProvider, error) {
// to be extended when PasswordAuth is implemented
return &ClientCredentialsOAuthProvider{}, nil
if !oauthConfig.IsEnabled() {
return nil, fmt.Errorf("upstream OAuth is not enabled")
}

switch {
case oauthConfig.ClientCredentials.ClientID != "" && oauthConfig.PasswordAuthentication.ClientID != "":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enabled flag needs to be respected here

return nil, fmt.Errorf("both client credentials and password authentication are provided")
case oauthConfig.ClientCredentials.ClientID != "":
return &ClientCredentialsOAuthProvider{}, nil
case oauthConfig.PasswordAuthentication.ClientID != "":
return &PasswordOAuthProvider{}, nil
default:
return nil, fmt.Errorf("no valid OAuth configuration provided")
}
}

func (p *PerAPIClientCredentialsOAuthProvider) getOAuthHeaderValue(r *http.Request, OAuthSpec *UpstreamOAuth) (string, error) {
Expand Down Expand Up @@ -143,7 +204,34 @@ func (p *ClientCredentialsOAuthProvider) getOAuthToken(r *http.Request, OAuthSpe
return fmt.Sprintf("Bearer %s", token), nil
}

func generateCacheKey(config apidef.UpstreamOAuth, apiId string) string {
func (p *PasswordOAuthProvider) getOAuthToken(r *http.Request, OAuthSpec *UpstreamOAuth) (string, error) {
if OAuthSpec.Gw.UpstreamOAuthCache == nil {
OAuthSpec.Gw.UpstreamOAuthCache = newUpstreamOAuthPasswordCache(OAuthSpec.Gw.StorageConnectionHandler)
}

token, err := OAuthSpec.Gw.UpstreamOAuthCache.getToken(r, OAuthSpec)
if err != nil {
return handleOAuthError(r, OAuthSpec, err)
}

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

}

func generatePasswordOAuthCacheKey(config apidef.UpstreamOAuth, apiId string) string {
key := fmt.Sprintf(
"%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))
}

func generateClientCredentialsCacheKey(config apidef.UpstreamOAuth, apiId string) string {
key := fmt.Sprintf(
"%s|%s|%s|%s",
apiId,
Expand All @@ -157,15 +245,15 @@ func generateCacheKey(config apidef.UpstreamOAuth, apiId string) string {
}

func (cache *upstreamOAuthClientCredentialsCache) getToken(r *http.Request, OAuthSpec *UpstreamOAuth) (string, error) {
cacheKey := generateCacheKey(OAuthSpec.Spec.UpstreamAuth.OAuth, OAuthSpec.Spec.APIID)
cacheKey := generateClientCredentialsCacheKey(OAuthSpec.Spec.UpstreamAuth.OAuth, OAuthSpec.Spec.APIID)

tokenString, err := retryGetKeyAndLock(cacheKey, &cache.RedisCluster)
if err != nil {
return "", err
}

if tokenString != "" {
decryptedToken := decrypt(getPaddedSecret(OAuthSpec.Gw), tokenString)
decryptedToken := decrypt(getPaddedSecret(OAuthSpec.Gw.GetConfig().Secret), tokenString)
return decryptedToken, nil
}

Expand All @@ -174,7 +262,7 @@ func (cache *upstreamOAuthClientCredentialsCache) getToken(r *http.Request, OAut
return "", err
}

encryptedToken := encrypt(getPaddedSecret(OAuthSpec.Gw), token.AccessToken)
encryptedToken := encrypt(getPaddedSecret(OAuthSpec.Gw.GetConfig().Secret), token.AccessToken)

ttl := time.Until(token.Expiry)
if err := setTokenInCache(cacheKey, encryptedToken, ttl, &cache.RedisCluster); err != nil {
Expand Down Expand Up @@ -218,6 +306,17 @@ func newOAuth2ClientCredentialsConfig(OAuthSpec *UpstreamOAuth) oauth2clientcred
}
}

func newOAuth2PasswordConfig(OAuthSpec *UpstreamOAuth) 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,
}
}

func (cache *upstreamOAuthClientCredentialsCache) obtainToken(ctx context.Context, OAuthSpec *UpstreamOAuth) (*oauth2.Token, error) {
cfg := newOAuth2ClientCredentialsConfig(OAuthSpec)

Expand Down
Loading
Loading