Merge pull request #451 from jrperritt/token-auth

allow token authentication
diff --git a/acceptance/rackspace/identity/v2/tokens_test.go b/acceptance/rackspace/identity/v2/tokens_test.go
new file mode 100644
index 0000000..95ee7e6
--- /dev/null
+++ b/acceptance/rackspace/identity/v2/tokens_test.go
@@ -0,0 +1,61 @@
+// +build acceptance
+
+package v2
+
+import (
+	"os"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	"github.com/rackspace/gophercloud/rackspace"
+	"github.com/rackspace/gophercloud/rackspace/identity/v2/tokens"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func rackspaceAuthOptions(t *testing.T) gophercloud.AuthOptions {
+	// Obtain credentials from the environment.
+	options, err := rackspace.AuthOptionsFromEnv()
+	th.AssertNoErr(t, err)
+	options = tools.OnlyRS(options)
+
+	if options.Username == "" {
+		t.Fatal("Please provide a Rackspace username as RS_USERNAME.")
+	}
+	if options.APIKey == "" {
+		t.Fatal("Please provide a Rackspace API key as RS_API_KEY.")
+	}
+
+	return options
+}
+
+func createClient(t *testing.T, auth bool) *gophercloud.ServiceClient {
+	ao := rackspaceAuthOptions(t)
+
+	provider, err := rackspace.NewClient(ao.IdentityEndpoint)
+	th.AssertNoErr(t, err)
+
+	if auth {
+		err = rackspace.Authenticate(provider, ao)
+		th.AssertNoErr(t, err)
+	}
+
+	return rackspace.NewIdentityV2(provider)
+}
+
+func TestTokenAuth(t *testing.T) {
+	authedClient := createClient(t, true)
+	token := authedClient.TokenID
+
+	tenantID := os.Getenv("RS_TENANT_ID")
+	if tenantID == "" {
+		t.Skip("You must set RS_TENANT_ID environment variable to run this test")
+	}
+
+	authOpts := tokens.AuthOptions{}
+	authOpts.TenantID = tenantID
+	authOpts.Token = token
+
+	_, err := tokens.Create(authedClient, authOpts).ExtractToken()
+	th.AssertNoErr(t, err)
+}
diff --git a/auth_options.go b/auth_options.go
index 9819e45..d26e16a 100644
--- a/auth_options.go
+++ b/auth_options.go
@@ -43,4 +43,8 @@
 	// false, it will not cache these settings, but re-authentication will not be
 	// possible.  This setting defaults to false.
 	AllowReauth bool
+
+	// TokenID allows users to authenticate (possibly as another user) with an
+	// authentication token ID.
+	TokenID string
 }
diff --git a/openstack/identity/v2/tokens/errors.go b/openstack/identity/v2/tokens/errors.go
index 3a9172e..3dfdc08 100644
--- a/openstack/identity/v2/tokens/errors.go
+++ b/openstack/identity/v2/tokens/errors.go
@@ -18,7 +18,7 @@
 	// ErrDomainNameProvided is returned if you attempt to authenticate with a DomainName.
 	ErrDomainNameProvided = unacceptedAttributeErr("DomainName")
 
-	// ErrUsernameRequired is returned if you attempt ot authenticate without a Username.
+	// ErrUsernameRequired is returned if you attempt to authenticate without a Username.
 	ErrUsernameRequired = errors.New("You must supply a Username in your AuthOptions.")
 
 	// ErrPasswordRequired is returned if you don't provide a password.
diff --git a/openstack/identity/v2/tokens/requests.go b/openstack/identity/v2/tokens/requests.go
index efa054f..074a89e 100644
--- a/openstack/identity/v2/tokens/requests.go
+++ b/openstack/identity/v2/tokens/requests.go
@@ -1,6 +1,10 @@
 package tokens
 
-import "github.com/rackspace/gophercloud"
+import (
+	"fmt"
+
+	"github.com/rackspace/gophercloud"
+)
 
 // AuthOptionsBuilder describes any argument that may be passed to the Create call.
 type AuthOptionsBuilder interface {
@@ -38,20 +42,24 @@
 		return nil, ErrDomainNameProvided
 	}
 
-	// Username and Password are always required.
-	if auth.Username == "" {
-		return nil, ErrUsernameRequired
-	}
-	if auth.Password == "" {
-		return nil, ErrPasswordRequired
-	}
-
 	// Populate the request map.
 	authMap := make(map[string]interface{})
 
-	authMap["passwordCredentials"] = map[string]interface{}{
-		"username": auth.Username,
-		"password": auth.Password,
+	if auth.Username != "" {
+		if auth.Password != "" {
+			authMap["passwordCredentials"] = map[string]interface{}{
+				"username": auth.Username,
+				"password": auth.Password,
+			}
+		} else {
+			return nil, ErrPasswordRequired
+		}
+	} else if auth.TokenID != "" {
+		authMap["token"] = map[string]interface{}{
+			"id": auth.TokenID,
+		}
+	} else {
+		return nil, fmt.Errorf("You must provide either username/password or tenantID/token values.")
 	}
 
 	if auth.TenantID != "" {
diff --git a/openstack/identity/v2/tokens/requests_test.go b/openstack/identity/v2/tokens/requests_test.go
index 2f02825..8b78c85 100644
--- a/openstack/identity/v2/tokens/requests_test.go
+++ b/openstack/identity/v2/tokens/requests_test.go
@@ -1,6 +1,7 @@
 package tokens
 
 import (
+	"fmt"
 	"testing"
 
 	"github.com/rackspace/gophercloud"
@@ -22,7 +23,7 @@
 	HandleTokenPost(t, "")
 
 	actualErr := Create(client.ServiceClient(), AuthOptions{options}).Err
-	th.CheckEquals(t, expectedErr, actualErr)
+	th.CheckDeepEquals(t, expectedErr, actualErr)
 }
 
 func TestCreateWithPassword(t *testing.T) {
@@ -128,7 +129,7 @@
 		Password: "thing",
 	}
 
-	tokenPostErr(t, options, ErrUsernameRequired)
+	tokenPostErr(t, options, fmt.Errorf("You must provide either username/password or tenantID/token values."))
 }
 
 func TestRequirePassword(t *testing.T) {