Skip to content

Implement federated (v3oidcaccesstoken) auth #176

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

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 14 additions & 0 deletions auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
52 changes: 47 additions & 5 deletions auth_v3.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ const (
v3AuthMethodToken = "token"
v3AuthMethodPassword = "password"
v3AuthMethodApplicationCredential = "application_credential"

v3AuthTypeOIDCAccessToken = "v3oidcaccesstoken"
)

// V3 Authentication request
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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}
Expand All @@ -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
Expand Down
61 changes: 42 additions & 19 deletions swift.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"},
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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 {
Expand Down