Completely untested code for tokens and tenants.
diff --git a/openstack/identity/v2/tokens/doc.go b/openstack/identity/v2/tokens/doc.go
new file mode 100644
index 0000000..d26f642
--- /dev/null
+++ b/openstack/identity/v2/tokens/doc.go
@@ -0,0 +1,6 @@
+/*
+Package tokens contains functions that issue and manipulate identity tokens.
+
+Reference: http://developer.openstack.org/api-ref-identity-v2.html#identity-auth-v2
+*/
+package tokens
diff --git a/openstack/identity/v2/tokens/errors.go b/openstack/identity/v2/tokens/errors.go
new file mode 100644
index 0000000..244db1b
--- /dev/null
+++ b/openstack/identity/v2/tokens/errors.go
@@ -0,0 +1,27 @@
+package tokens
+
+import (
+ "errors"
+ "fmt"
+)
+
+var (
+ // ErrUserIDProvided is returned if you attempt to authenticate with a UserID.
+ ErrUserIDProvided = unacceptedAttributeErr("UserID")
+
+ // ErrDomainIDProvided is returned if you attempt to authenticate with a DomainID.
+ ErrDomainIDProvided = unacceptedAttributeErr("DomainID")
+
+ // 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 = errors.New("You must supply a Username in your AuthOptions.")
+
+ // ErrPasswordOrAPIKey is returned if you provide both a password and an API key.
+ ErrPasswordOrAPIKey = errors.New("Please supply exactly one of Password or APIKey in your AuthOptions.")
+)
+
+func unacceptedAttributeErr(attribute string) error {
+ return fmt.Errorf("The base Identity V2 API does not accept authentication by %s", attribute)
+}
diff --git a/openstack/identity/v2/tokens/requests.go b/openstack/identity/v2/tokens/requests.go
new file mode 100644
index 0000000..c0441ab
--- /dev/null
+++ b/openstack/identity/v2/tokens/requests.go
@@ -0,0 +1,72 @@
+package tokens
+
+import (
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+)
+
+// Create authenticates to the identity service and attempts to acquire a Token.
+// If successful, the CreateResult
+// Generally, rather than interact with this call directly, end users should call openstack.AuthenticatedClient(),
+// which abstracts all of the gory details about navigating service catalogs and such.
+func Create(client *gophercloud.ServiceClient, auth gophercloud.AuthOptions) CreateResult {
+ var request struct {
+ Auth struct {
+ PasswordCredentials struct {
+ Username string `json:"username"`
+ Password string `json:"password"`
+ } `json:"passwordCredentials,omitempty"`
+ APIKeyCredentials struct {
+ Username string `json:"username"`
+ APIKey string `json:"apiKey"`
+ } `json:"RAX-KSKEY:apiKeyCredentials,omitempty"`
+ TenantID string `json:"tenantId,omitempty"`
+ TenantName string `json:"tenantName,omitempty"`
+ } `json:"auth"`
+ }
+
+ // Error out if an unsupported auth option is present.
+ if auth.UserID != "" {
+ return createErr(ErrUserIDProvided)
+ }
+ if auth.DomainID != "" {
+ return createErr(ErrDomainIDProvided)
+ }
+ if auth.DomainName != "" {
+ return createErr(ErrDomainNameProvided)
+ }
+
+ // Username is always required.
+ if auth.Username == "" {
+ return createErr(ErrUsernameRequired)
+ }
+
+ // Populate either PasswordCredentials or APIKeyCredentials
+ if auth.Password != "" {
+ if auth.APIKey != "" {
+ return createErr(ErrPasswordOrAPIKey)
+ }
+
+ // Username + Password
+ request.Auth.PasswordCredentials.Username = auth.Username
+ request.Auth.PasswordCredentials.Password = auth.Password
+ } else if auth.APIKey != "" {
+ // API key authentication.
+ request.Auth.APIKeyCredentials.Username = auth.Username
+ request.Auth.APIKeyCredentials.APIKey = auth.APIKey
+ } else {
+ return createErr(ErrPasswordOrAPIKey)
+ }
+
+ // Populate the TenantName or TenantID, if provided.
+ request.Auth.TenantID = auth.TenantID
+ request.Auth.TenantName = auth.TenantName
+
+ var result CreateResult
+ _, result.Err = perigee.Request("POST", listURL(client), perigee.Options{
+ ReqBody: &request,
+ Results: &result.Resp,
+ OkCodes: []int{200, 203},
+ })
+ return result
+}
diff --git a/openstack/identity/v2/tokens/results.go b/openstack/identity/v2/tokens/results.go
new file mode 100644
index 0000000..7a245ab
--- /dev/null
+++ b/openstack/identity/v2/tokens/results.go
@@ -0,0 +1,66 @@
+package tokens
+
+import (
+ "time"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
+)
+
+// Token provides only the most basic information related to an authentication token.
+type Token struct {
+ // ID provides the primary means of identifying a user to the OpenStack API.
+ // OpenStack defines this field as an opaque value, so do not depend on its content.
+ // It is safe, however, to compare for equality.
+ ID string
+
+ // ExpiresAt provides a timestamp in ISO 8601 format, indicating when the authentication token becomes invalid.
+ // After this point in time, future API requests made using this authentication token will respond with errors.
+ // Either the caller will need to reauthenticate manually, or more preferably, the caller should exploit automatic re-authentication.
+ // See the AuthOptions structure for more details.
+ ExpiresAt time.Time
+
+ // Tenant provides information about the tenant to which this token grants access.
+ Tenant tenants.Tenant
+}
+
+// CreateResult defers the interpretation of a created token.
+// Use ExtractToken() to interpret it as a Token, or ExtractServiceCatalog() to interpret it as a service catalog.
+type CreateResult struct {
+ gophercloud.CommonResult
+}
+
+// ExtractToken returns the just-created Token from a CreateResult.
+func (result CreateResult) ExtractToken() (*Token, error) {
+ var response struct {
+ Access struct {
+ Token struct {
+ Expires string `mapstructure:"expires"`
+ ID string `mapstructure:"id"`
+ Tenant tenants.Tenant `mapstructure:"tenant"`
+ } `mapstructure:"token"`
+ } `mapstructure:"access"`
+ }
+
+ err := mapstructure.Decode(result.Resp, &response)
+ if err != nil {
+ return nil, err
+ }
+
+ expiresTs, err := time.Parse(gophercloud.RFC3339Milli, response.Access.Token.Expires)
+ if err != nil {
+ return nil, err
+ }
+
+ return &Token{
+ ID: response.Access.Token.ID,
+ ExpiresAt: expiresTs,
+ Tenant: response.Access.Token.Tenant,
+ }, nil
+}
+
+// createErr quickly packs an error in a CreateResult.
+func createErr(err error) CreateResult {
+ return CreateResult{gophercloud.CommonResult{Err: err}}
+}
diff --git a/openstack/identity/v2/tokens/urls.go b/openstack/identity/v2/tokens/urls.go
new file mode 100644
index 0000000..86d19f2
--- /dev/null
+++ b/openstack/identity/v2/tokens/urls.go
@@ -0,0 +1,7 @@
+package tokens
+
+import "github.com/rackspace/gophercloud"
+
+func listURL(client *gophercloud.ServiceClient) string {
+ return client.ServiceURL("tokens")
+}