forked from remote/oauth2
148 lines
4.7 KiB
Go
148 lines
4.7 KiB
Go
package externalaccount
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"golang.org/x/oauth2"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
// The configuration for fetching tokens with external credentials.
|
|
type Config struct {
|
|
Audience string
|
|
SubjectTokenType string
|
|
TokenURL string
|
|
TokenInfoURL string
|
|
ServiceAccountImpersonationURL string
|
|
ClientSecret string
|
|
ClientID string
|
|
CredentialSource CredentialSource
|
|
QuotaProjectID string
|
|
|
|
Scopes []string
|
|
}
|
|
|
|
// Returns an external account TokenSource. This is to be called by package google to construct a google.Credentials.
|
|
func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource {
|
|
ts := tokenSource{
|
|
ctx: ctx,
|
|
conf: c,
|
|
}
|
|
return oauth2.ReuseTokenSource(nil, ts)
|
|
}
|
|
|
|
//Subject token file types
|
|
const (
|
|
fileTypeText = "text"
|
|
fileTypeJSON = "json"
|
|
)
|
|
|
|
type format struct {
|
|
// Either "text" or "json". When not provided "text" type is assumed.
|
|
Type string `json:"type"`
|
|
// Only required for JSON.
|
|
// This would be "access_token" for azure.
|
|
SubjectTokenFieldName string `json:"subject_token_field_name"`
|
|
}
|
|
|
|
type CredentialSource struct {
|
|
File string `json:"file"`
|
|
|
|
URL string `json:"url"`
|
|
Headers map[string]string `json:"headers"`
|
|
|
|
EnvironmentID string `json:"environment_id"`
|
|
RegionURL string `json:"region_url"`
|
|
RegionalCredVerificationURL string `json:"regional_cred_verification_url"`
|
|
CredVerificationURL string `json:"cred_verification_url"`
|
|
Format format `json:"format"`
|
|
}
|
|
|
|
func (cs CredentialSource) instance() baseCredentialSource {
|
|
if cs.EnvironmentID == "awsX" {
|
|
return nil
|
|
//return awsCredentialSource{EnvironmentID:cs.EnvironmentID, RegionURL:cs.RegionURL, RegionalCredVerificationURL: cs.RegionalCredVerificationURL, CredVerificationURL:cs.CredVerificationURL}
|
|
} else if cs.File == "internalTestingFile" {
|
|
return testCredentialSource{}
|
|
} else if cs.File != "" {
|
|
return fileCredentialSource{File: cs.File}
|
|
} else if cs.URL != "" {
|
|
//return urlCredentialSource{URL:cs.URL, Headers:cs.Headers}
|
|
return nil
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
type baseCredentialSource interface {
|
|
retrieveSubjectToken(c *Config) (string, error)
|
|
}
|
|
|
|
// tokenSource is the source that handles 3PI credentials.
|
|
type tokenSource struct {
|
|
ctx context.Context
|
|
conf *Config
|
|
}
|
|
|
|
// This method is implemented so that tokenSource conforms to oauth2.TokenSource.
|
|
func (ts tokenSource) Token() (*oauth2.Token, error) {
|
|
conf := ts.conf
|
|
|
|
subjectToken, err := conf.CredentialSource.instance().retrieveSubjectToken(conf)
|
|
if err != nil {
|
|
return &oauth2.Token{}, err
|
|
}
|
|
stsRequest := STSTokenExchangeRequest{
|
|
GrantType: "urn:ietf:params:oauth:grant-type:token-exchange",
|
|
Audience: conf.Audience,
|
|
Scope: conf.Scopes,
|
|
RequestedTokenType: "urn:ietf:params:oauth:token-type:access_token",
|
|
SubjectToken: subjectToken,
|
|
SubjectTokenType: conf.SubjectTokenType,
|
|
}
|
|
header := make(http.Header)
|
|
header.Add("Content-Type", "application/x-www-form-urlencoded")
|
|
clientAuth := ClientAuthentication{
|
|
AuthStyle: oauth2.AuthStyleInHeader,
|
|
ClientID: conf.ClientID,
|
|
ClientSecret: conf.ClientSecret,
|
|
}
|
|
stsResp, err := ExchangeToken(ts.ctx, conf.TokenURL, &stsRequest, clientAuth, header, nil)
|
|
if err != nil {
|
|
fmt.Errorf("oauth2/google: %s", err.Error())
|
|
return &oauth2.Token{}, err
|
|
}
|
|
|
|
accessToken := &oauth2.Token{
|
|
AccessToken: stsResp.AccessToken,
|
|
TokenType: stsResp.TokenType,
|
|
}
|
|
if stsResp.ExpiresIn < 0 {
|
|
fmt.Errorf("google/oauth2: got invalid expiry from security token service")
|
|
// REVIEWERS: Should I return the Token that I actually got back here so that people could inspect the result even with a improper ExpiresIn response?
|
|
// Or is it more appropriate to still return an empty token: &oauth2.Token{} so that anybody who checks for an empty token as a sign of failure doesn't get confused.
|
|
return accessToken, nil
|
|
} else if stsResp.ExpiresIn > 0 {
|
|
accessToken.Expiry = time.Now().Add(time.Duration(stsResp.ExpiresIn) * time.Second)
|
|
}
|
|
|
|
if stsResp.RefreshToken != "" {
|
|
accessToken.RefreshToken = stsResp.RefreshToken
|
|
}
|
|
|
|
return accessToken, nil
|
|
}
|
|
|
|
// NOTE: this method doesn't exist yet. It is being investigated to add this method to oauth2.TokenSource.
|
|
//func (ts tokenSource) TokenInfo() (*oauth2.TokenInfo, error)
|
|
|
|
// testCredentialSource is only used for testing, but must be defined here in order to avoid undefined errors when testing.
|
|
type testCredentialSource struct {
|
|
File string
|
|
}
|
|
|
|
func (cs testCredentialSource) retrieveSubjectToken(c *Config) (string, error) {
|
|
return "Sample.Subject.Token", nil
|
|
}
|