diff --git a/auth.go b/auth.go index ec56cc6ca..311e7ff50 100644 --- a/auth.go +++ b/auth.go @@ -10,6 +10,10 @@ import ( "time" ) +// Method signatures for Authenticator and TwoStageAuthenticator +type AuthRequestGenerator func(context.Context, *Connection) (*http.Request, error) +type AuthResponseHandler func(context.Context, *http.Response) error + // Auth defines the operations needed to authenticate with swift // // This encapsulates the different authentication schemes in use @@ -27,6 +31,16 @@ type Authenticator interface { CdnUrl() string } +// TwoStageAuthenticator is used for authentication using two requests, like with v3oidcaccesstoken +type TwoStageAuthenticator interface { + Authenticator + + // PrelimRequest returns a request if the authenticator needs to do a request before the main auth request + PrelimRequest(context.Context, *Connection) (*http.Request, error) + // PrelimResponse parses the response to a PrelimRequest + PrelimResponse(context.Context, *http.Response) error +} + // Expireser is an optional interface to read the expiration time of the token type Expireser interface { Expires() time.Time diff --git a/auth_v3.go b/auth_v3.go index 89840d7df..aca05c58f 100644 --- a/auth_v3.go +++ b/auth_v3.go @@ -14,6 +14,8 @@ const ( v3AuthMethodToken = "token" v3AuthMethodPassword = "password" v3AuthMethodApplicationCredential = "application_credential" + + v3AuthTypeOIDCAccessToken = "v3oidcaccesstoken" ) // V3 Authentication request @@ -117,9 +119,44 @@ type v3AuthResponse struct { } type v3Auth struct { - Region string - Auth *v3AuthResponse - Headers http.Header + Region string + Auth *v3AuthResponse + Headers http.Header + AuthType string + UnscopedToken string +} + +var ( + // make sure the methods correspond to their signatures + _ = AuthRequestGenerator((&v3Auth{}).Request) + _ = AuthRequestGenerator((&v3Auth{}).PrelimRequest) + _ = AuthResponseHandler((&v3Auth{}).Response) + _ = AuthResponseHandler((&v3Auth{}).PrelimResponse) +) + +func (auth *v3Auth) PrelimRequest(ctx context.Context, c *Connection) (*http.Request, error) { + if c.AuthUrl != "" && c.IdentityProvider != "" && c.AuthProtocol != "" && c.AccessToken != "" { + auth.AuthType = v3AuthTypeOIDCAccessToken + url := fmt.Sprintf("%s/OS-FEDERATION/identity_providers/%s/protocols/%s/auth", + c.AuthUrl, c.IdentityProvider, c.AuthProtocol) + req, err := http.NewRequestWithContext(ctx, "POST", url, nil) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+c.AccessToken) + return req, nil + } + return nil, nil +} + +func (auth *v3Auth) PrelimResponse(_ context.Context, resp *http.Response) error { + if auth.AuthType == v3AuthTypeOIDCAccessToken { + auth.UnscopedToken = resp.Header.Get("X-Subject-Token") + if auth.UnscopedToken == "" { + return fmt.Errorf("No unscoped token") + } + } + return nil } func (auth *v3Auth) Request(ctx context.Context, c *Connection) (*http.Request, error) { @@ -179,6 +216,9 @@ func (auth *v3Auth) Request(ctx context.Context, c *Connection) (*http.Request, Secret: c.ApplicationCredentialSecret, User: user, } + } else if auth.UnscopedToken != "" { // from TwoStageAuthenticator + v3.Auth.Identity.Methods = []string{v3AuthMethodToken} + v3.Auth.Identity.Token = &v3AuthToken{Id: auth.UnscopedToken} } else if c.UserName == "" && c.UserId == "" { v3.Auth.Identity.Methods = []string{v3AuthMethodToken} v3.Auth.Identity.Token = &v3AuthToken{Id: c.ApiKey} @@ -205,11 +245,13 @@ func (auth *v3Auth) Request(ctx context.Context, c *Connection) (*http.Request, if v3.Auth.Identity.Methods[0] != v3AuthMethodApplicationCredential { if c.TrustId != "" { v3.Auth.Scope = &v3Scope{Trust: &v3Trust{Id: c.TrustId}} - } else if c.TenantId != "" || c.Tenant != "" { + } else if c.TenantId != "" || c.Tenant != "" || c.ProjectId != "" { v3.Auth.Scope = &v3Scope{Project: &v3Project{}} - if c.TenantId != "" { + if c.ProjectId != "" { + v3.Auth.Scope.Project.Id = c.ProjectId + } else if c.TenantId != "" { v3.Auth.Scope.Project.Id = c.TenantId } else if c.Tenant != "" { v3.Auth.Scope.Project.Name = c.Tenant diff --git a/swift.go b/swift.go index c9357ee3f..e42dac1d8 100644 --- a/swift.go +++ b/swift.go @@ -118,6 +118,10 @@ type Connection struct { TenantDomain string // Name of the tenant's domain (v3 auth only), only needed if it differs from the user domain TenantDomainId string // Id of the tenant's domain (v3 auth only), only needed if it differs the from user domain TrustId string // Id of the trust (v3 auth only) + AccessToken string // Access token (v3 federated auth only) + AuthProtocol string // AuthProtocol, e.g. 'openid' (v3 federated auth only) + IdentityProvider string // Identity provider (v3 federated auth only) + ProjectId string // Id of the project (v3 federated auth only) Transport http.RoundTripper `json:"-" xml:"-"` // Optional specialised http.Transport (eg. for Google Appengine) // These are filled in after Authenticate is called as are the defaults for above StorageUrl string @@ -257,6 +261,10 @@ func (c *Connection) ApplyEnvironment() (err error) { {&c.TrustId, "OS_TRUST_ID"}, {&c.StorageUrl, "OS_STORAGE_URL"}, {&c.AuthToken, "OS_AUTH_TOKEN"}, + {&c.AccessToken, "OS_ACCESS_TOKEN"}, + {&c.AuthProtocol, "OS_PROTOCOL"}, + {&c.IdentityProvider, "OS_IDENTITY_PROVIDER"}, + {&c.ProjectId, "OS_PROJECT_ID"}, // v1 auth alternatives {&c.ApiKey, "ST_KEY"}, {&c.UserName, "ST_USER"}, @@ -471,27 +479,12 @@ func (c *Connection) Authenticate(ctx context.Context) (err error) { return c.authenticate(ctx) } -// Internal implementation of Authenticate -// -// Call with authLock held -func (c *Connection) authenticate(ctx context.Context) (err error) { - c.setDefaults() - - // Flush the keepalives connection - if we are - // re-authenticating then stuff has gone wrong - flushKeepaliveConnections(c.Transport) - - if c.Auth == nil { - c.Auth, err = newAuth(c) - if err != nil { - return - } - } - +// executeRequestResponsePair generates an auth request using reqGen and handles the response using reqHandler +func (c *Connection) executeRequestResponsePair(ctx context.Context, reqGen AuthRequestGenerator, reqHandler AuthResponseHandler) (err error) { retries := 1 again: var req *http.Request - req, err = c.Auth.Request(ctx, c) + req, err = reqGen(ctx, c) if err != nil { return } @@ -519,11 +512,41 @@ again: } return } - err = c.Auth.Response(ctx, resp) + return reqHandler(ctx, resp) + } + return +} + +// Internal implementation of Authenticate +// +// Call with authLock held +func (c *Connection) authenticate(ctx context.Context) (err error) { + c.setDefaults() + + // Flush the keepalives connection - if we are + // re-authenticating then stuff has gone wrong + flushKeepaliveConnections(c.Transport) + + if c.Auth == nil { + c.Auth, err = newAuth(c) if err != nil { return } } + + // handle optional authentication stage + if prelimAuth, needsPrelimReq := c.Auth.(TwoStageAuthenticator); needsPrelimReq { + err = c.executeRequestResponsePair(ctx, prelimAuth.PrelimRequest, prelimAuth.PrelimResponse) + if err != nil { + return + } + } + + err = c.executeRequestResponsePair(ctx, c.Auth.Request, c.Auth.Response) + if err != nil { + return + } + if customAuth, isCustom := c.Auth.(CustomEndpointAuthenticator); isCustom && c.EndpointType != "" { c.StorageUrl = customAuth.StorageUrlForEndpoint(c.EndpointType) } else {