add versioning to identity; add generic 'client' function
diff --git a/openstack/identity/v2/client.go b/openstack/identity/v2/client.go
new file mode 100644
index 0000000..c736833
--- /dev/null
+++ b/openstack/identity/v2/client.go
@@ -0,0 +1,80 @@
+package identity
+
+import (
+	"os"
+)
+
+type Client struct {
+	Endpoint  string
+	Authority AuthResults
+	Options   AuthOptions
+}
+
+type ClientOpts struct {
+	Type    string
+	Name    string
+	Region  string
+	URLType string
+}
+
+func (ao AuthOptions) NewClient(opts ClientOpts) (Client, error) {
+	client := Client{
+		Options: ao,
+	}
+
+	ar, err := Authenticate(ao)
+	if err != nil {
+		return client, err
+	}
+
+	client.Authority = ar
+
+	sc, err := GetServiceCatalog(ar)
+	if err != nil {
+		return client, err
+	}
+
+	ces, err := sc.CatalogEntries()
+	if err != nil {
+		return client, err
+	}
+
+	var eps []Endpoint
+
+	if opts.Name != "" {
+		for _, ce := range ces {
+			if ce.Type == opts.Type && ce.Name == opts.Name {
+				eps = ce.Endpoints
+			}
+		}
+	} else {
+		for _, ce := range ces {
+			if ce.Type == opts.Type {
+				eps = ce.Endpoints
+			}
+		}
+	}
+
+	region := os.Getenv("OS_REGION_NAME")
+	if opts.Region != "" {
+		region = opts.Region
+	}
+
+	var rep string
+	for _, ep := range eps {
+		if ep.Region == region {
+			switch opts.URLType {
+			case "public":
+				rep = ep.PublicURL
+			case "private":
+				rep = ep.InternalURL
+			default:
+				rep = ep.PublicURL
+			}
+		}
+	}
+
+	client.Endpoint = rep
+
+	return client, nil
+}
diff --git a/openstack/identity/v2/common_test.go b/openstack/identity/v2/common_test.go
new file mode 100644
index 0000000..18c5340
--- /dev/null
+++ b/openstack/identity/v2/common_test.go
@@ -0,0 +1,224 @@
+package identity
+
+// Taken from: http://docs.openstack.org/api/openstack-identity-service/2.0/content/POST_authenticate_v2.0_tokens_.html
+const authResultsOK = `{
+    "access":{
+        "token":{
+            "id": "ab48a9efdfedb23ty3494",
+            "expires": "2010-11-01T03:32:15-05:00",
+            "tenant":{
+                "id": "t1000",
+                "name": "My Project"
+            }
+        },
+        "user":{
+            "id": "u123",
+            "name": "jqsmith",
+            "roles":[{
+                    "id": "100",
+                    "name": "compute:admin"
+                },
+                {
+                    "id": "101",
+                    "name": "object-store:admin",
+                    "tenantId": "t1000"
+                }
+            ],
+            "roles_links":[]
+        },
+        "serviceCatalog":[{
+                "name": "Cloud Servers",
+                "type": "compute",
+                "endpoints":[{
+                        "tenantId": "t1000",
+                        "publicURL": "https://compute.north.host.com/v1/t1000",
+                        "internalURL": "https://compute.north.internal/v1/t1000",
+                        "region": "North",
+                        "versionId": "1",
+                        "versionInfo": "https://compute.north.host.com/v1/",
+                        "versionList": "https://compute.north.host.com/"
+                    },
+                    {
+                        "tenantId": "t1000",
+                        "publicURL": "https://compute.north.host.com/v1.1/t1000",
+                        "internalURL": "https://compute.north.internal/v1.1/t1000",
+                        "region": "North",
+                        "versionId": "1.1",
+                        "versionInfo": "https://compute.north.host.com/v1.1/",
+                        "versionList": "https://compute.north.host.com/"
+                    }
+                ],
+                "endpoints_links":[]
+            },
+            {
+                "name": "Cloud Files",
+                "type": "object-store",
+                "endpoints":[{
+                        "tenantId": "t1000",
+                        "publicURL": "https://storage.north.host.com/v1/t1000",
+                        "internalURL": "https://storage.north.internal/v1/t1000",
+                        "region": "North",
+                        "versionId": "1",
+                        "versionInfo": "https://storage.north.host.com/v1/",
+                        "versionList": "https://storage.north.host.com/"
+                    },
+                    {
+                        "tenantId": "t1000",
+                        "publicURL": "https://storage.south.host.com/v1/t1000",
+                        "internalURL": "https://storage.south.internal/v1/t1000",
+                        "region": "South",
+                        "versionId": "1",
+                        "versionInfo": "https://storage.south.host.com/v1/",
+                        "versionList": "https://storage.south.host.com/"
+                    }
+                ]
+            },
+            {
+                "name": "DNS-as-a-Service",
+                "type": "dnsextension:dns",
+                "endpoints":[{
+                        "tenantId": "t1000",
+                        "publicURL": "https://dns.host.com/v2.0/t1000",
+                        "versionId": "2.0",
+                        "versionInfo": "https://dns.host.com/v2.0/",
+                        "versionList": "https://dns.host.com/"
+                    }
+                ]
+            }
+        ]
+    }
+}`
+
+// Taken from: http://developer.openstack.org/api-ref-identity-v2.html
+const queryResults = `{
+	"extensions": {
+		"values": [
+			{
+				"updated": "2013-07-07T12:00:0-00:00",
+				"name": "OpenStack S3 API",
+				"links": [
+					{
+						"href": "https://github.com/openstack/identity-api",
+						"type": "text/html",
+						"rel": "describedby"
+					}
+				],
+				"namespace": "http://docs.openstack.org/identity/api/ext/s3tokens/v1.0",
+				"alias": "s3tokens",
+				"description": "OpenStack S3 API."
+			},
+			{
+				"updated": "2013-07-23T12:00:0-00:00",
+				"name": "OpenStack Keystone Endpoint Filter API",
+				"links": [
+					{
+						"href": "https://github.com/openstack/identity-api/blob/master/openstack-identity-api/v3/src/markdown/identity-api-v3-os-ep-filter-ext.md",
+						"type": "text/html",
+						"rel": "describedby"
+					}
+				],
+				"namespace": "http://docs.openstack.org/identity/api/ext/OS-EP-FILTER/v1.0",
+				"alias": "OS-EP-FILTER",
+				"description": "OpenStack Keystone Endpoint Filter API."
+			},
+			{
+				"updated": "2013-12-17T12:00:0-00:00",
+				"name": "OpenStack Federation APIs",
+				"links": [
+					{
+						"href": "https://github.com/openstack/identity-api",
+						"type": "text/html",
+						"rel": "describedby"
+					}
+				],
+				"namespace": "http://docs.openstack.org/identity/api/ext/OS-FEDERATION/v1.0",
+				"alias": "OS-FEDERATION",
+				"description": "OpenStack Identity Providers Mechanism."
+			},
+			{
+				"updated": "2013-07-11T17:14:00-00:00",
+				"name": "OpenStack Keystone Admin",
+				"links": [
+					{
+						"href": "https://github.com/openstack/identity-api",
+						"type": "text/html",
+						"rel": "describedby"
+					}
+				],
+				"namespace": "http://docs.openstack.org/identity/api/ext/OS-KSADM/v1.0",
+				"alias": "OS-KSADM",
+				"description": "OpenStack extensions to Keystone v2.0 API enabling Administrative Operations."
+			},
+			{
+				"updated": "2014-01-20T12:00:0-00:00",
+				"name": "OpenStack Simple Certificate API",
+				"links": [
+					{
+						"href": "https://github.com/openstack/identity-api",
+						"type": "text/html",
+						"rel": "describedby"
+					}
+				],
+				"namespace": "http://docs.openstack.org/identity/api/ext/OS-SIMPLE-CERT/v1.0",
+				"alias": "OS-SIMPLE-CERT",															
+				"description": "OpenStack simple certificate retrieval extension"	
+			},																
+			{
+				"updated": "2013-07-07T12:00:0-00:00",	
+				"name": "OpenStack EC2 API",
+				"links": [
+					{
+						"href": "https://github.com/openstack/identity-api",	
+						"type": "text/html",				
+						"rel": "describedby"				
+					}	
+				],
+				"namespace": "http://docs.openstack.org/identity/api/ext/OS-EC2/v1.0",	
+				"alias": "OS-EC2",
+				"description": "OpenStack EC2 Credentials backend."
+			}
+		]
+	}
+}`
+
+// Extensions query with a bogus JSON envelop.
+const bogusExtensionsResults = `{
+    "explosions":[{
+            "name": "Reset Password Extension",
+            "namespace": "http://docs.rackspacecloud.com/identity/api/ext/rpe/v2.0",
+            "alias": "RS-RPE",
+            "updated": "2011-01-22T13:25:27-06:00",
+            "description": "Adds the capability to reset a user's password. The user is emailed when the password has been reset.",
+            "links":[{
+                    "rel": "describedby",
+                    "type": "application/pdf",
+                    "href": "http://docs.rackspacecloud.com/identity/api/ext/identity-rpe-20111111.pdf"
+                },
+                {
+                    "rel": "describedby",
+                    "type": "application/vnd.sun.wadl+xml",
+                    "href": "http://docs.rackspacecloud.com/identity/api/ext/identity-rpe.wadl"
+                }
+            ]
+        },
+        {
+            "name": "User Metadata Extension",
+            "namespace": "http://docs.rackspacecloud.com/identity/api/ext/meta/v2.0",
+            "alias": "RS-META",
+            "updated": "2011-01-12T11:22:33-06:00",
+            "description": "Allows associating arbritrary metadata with a user.",
+            "links":[{
+                    "rel": "describedby",
+                    "type": "application/pdf",
+                    "href": "http://docs.rackspacecloud.com/identity/api/ext/identity-meta-20111201.pdf"
+                },
+                {
+                    "rel": "describedby",
+                    "type": "application/vnd.sun.wadl+xml",
+                    "href": "http://docs.rackspacecloud.com/identity/api/ext/identity-meta.wadl"
+                }
+            ]
+        }
+    ],
+    "extensions_links":[]
+}`
diff --git a/openstack/identity/v2/doc.go b/openstack/identity/v2/doc.go
new file mode 100644
index 0000000..081950d
--- /dev/null
+++ b/openstack/identity/v2/doc.go
@@ -0,0 +1,120 @@
+/*
+The Identity package provides convenient OpenStack Identity V2 API client access.
+This package currently doesn't support the administrative access endpoints, but may appear in the future based on demand.
+
+Authentication
+
+Established convention in the OpenStack community suggests the use of environment variables to hold authentication parameters.
+For example, the following settings would be sufficient to authenticate against Rackspace:
+
+	# assumes Bash shell on a POSIX environment; use SET command for Windows.
+	export OS_AUTH_URL=https://identity.api.rackspacecloud.com/v2.0
+	export OS_USERNAME=xxxx
+	export OS_PASSWORD=yyyy
+
+while you'd need these additional settings to authenticate against, e.g., Nebula One:
+
+	export OS_TENANT_ID=zzzz
+	export OS_TENANT_NAME=wwww
+
+Be sure to consult with your provider to see which settings you'll need to authenticate with.
+
+A skeletal client gets started with Gophercloud by authenticating against his/her provider, like so:
+
+	package main
+
+	import (
+		"fmt"
+		"github.com/rackspace/gophercloud/openstack/identity"
+		"github.com/rackspace/gophercloud/openstack/utils"
+	)
+
+	func main() {
+		// Create an initialized set of authentication options based on available OS_*
+		// environment variables.
+		ao, err := utils.AuthOptions()
+		if err != nil {
+			panic(err)
+		}
+
+		// Attempt to authenticate with them.
+		r, err := identity.Authenticate(ao)
+		if err != nil {
+			panic(err)
+		}
+
+		// With each authentication, you receive a master directory of all the services
+		// your account can access.  This "service catalog", as OpenStack calls it,
+		// provides you the means to exploit other OpenStack services.
+		sc, err := identity.GetServiceCatalog(r)
+		if err != nil {
+			panic(err)
+		}
+
+		// Find the desired service(s) for our application.
+		computeService, err := findService(sc, "compute", ...)
+		if err != nil {
+			panic(err)
+		}
+
+		blockStorage, err := findService(sc, "block-storage", ...)
+		if err != nil {
+			panic(err)
+		}
+
+		// ... etc ...
+	}
+
+NOTE!
+Unlike versions 0.1.x of the Gophercloud API,
+0.2.0 and later will not provide a service look-up mechanism as a built-in feature of the Identity SDK binding.
+The 0.1.x behavior potentially opened its non-US users to legal liability by potentially selecting endpoints in undesirable regions
+in a non-obvious manner if a specific region was not explicitly specified.
+Starting with 0.2.0 and beyond, you'll need to use either your own service catalog query function or one in a separate package.
+This makes it plainly visible to a code auditor that if you indeed desired automatic selection of an arbitrary region,
+you made the conscious choice to use that feature.
+
+Extensions
+
+Some OpenStack deployments may support features that other deployments do not.
+Anything beyond the scope of standard OpenStack must be scoped by an "extension," a named, yet well-known, change to the API.
+Users may invoke IsExtensionAvailable() after grabbing a list of extensions from the server with GetExtensions().
+This of course assumes you know the name of the extension ahead of time.
+
+Here's a simple example of listing all the aliases for supported extensions.
+Once you have an alias to an extension, everything else about it may be queried through accessors.
+
+	package main
+
+	import (
+		"fmt"
+		"github.com/rackspace/gophercloud/openstack/identity"
+		"github.com/rackspace/gophercloud/openstack/utils"
+	)
+
+	func main() {
+		// Create an initialized set of authentication options based on available OS_*
+		// environment variables.
+		ao, err := utils.AuthOptions()
+		if err != nil {
+			panic(err)
+		}
+
+		// Attempt to query extensions.
+		exts, err := identity.GetExtensions(ao)
+		if err != nil {
+			panic(err)
+		}
+
+		// Print out a summary of supported extensions
+		aliases, err := exts.Aliases()
+		if err != nil {
+			panic(err)
+		}
+		fmt.Println("Extension Aliases:")
+		for _, alias := range aliases {
+			fmt.Printf("  %s\n", alias)
+		}
+	}
+*/
+package identity
diff --git a/openstack/identity/v2/errors.go b/openstack/identity/v2/errors.go
new file mode 100644
index 0000000..efa7c85
--- /dev/null
+++ b/openstack/identity/v2/errors.go
@@ -0,0 +1,17 @@
+package identity
+
+import "fmt"
+
+// ErrNotImplemented errors may occur in two contexts:
+// (1) development versions of this package may return this error for endpoints which are defined but not yet completed, and,
+// (2) production versions of this package may return this error when a provider fails to offer the requested Identity extension.
+//
+// ErrEndpoint errors occur when the authentication URL provided to Authenticate() either isn't valid
+// or the endpoint provided doesn't respond like an Identity V2 API endpoint should.
+//
+// ErrCredentials errors occur when authentication fails due to the caller possessing insufficient access privileges.
+var (
+	ErrNotImplemented = fmt.Errorf("Identity feature not yet implemented")
+	ErrEndpoint       = fmt.Errorf("Improper or missing Identity endpoint")
+	ErrCredentials    = fmt.Errorf("Improper or missing Identity credentials")
+)
diff --git a/openstack/identity/v2/extensions.go b/openstack/identity/v2/extensions.go
new file mode 100644
index 0000000..9cf9c78
--- /dev/null
+++ b/openstack/identity/v2/extensions.go
@@ -0,0 +1,96 @@
+package identity
+
+import (
+	"github.com/mitchellh/mapstructure"
+)
+
+// ExtensionDetails provides the details offered by the OpenStack Identity V2 extensions API
+// for a named extension.
+//
+// Name provides the name, presumably the same as that used to query the API with.
+//
+// Updated provides, in a sense, the version of the extension supported.  It gives the timestamp
+// of the most recent extension deployment.
+//
+// Description provides a more customer-oriented description of the extension.
+type ExtensionDetails struct {
+	Name        string
+	Namespace   string
+	Updated     string
+	Description string
+}
+
+// ExtensionsResult encapsulates the raw data returned by a call to
+// GetExtensions().  As OpenStack extensions may freely alter the response
+// bodies of structures returned to the client, you may only safely access the
+// data provided through separate, type-safe accessors or methods.
+type ExtensionsResult map[string]interface{}
+
+// IsExtensionAvailable returns true if and only if the provider supports the named extension.
+func (er ExtensionsResult) IsExtensionAvailable(alias string) bool {
+	e, err := extensions(er)
+	if err != nil {
+		return false
+	}
+	_, err = extensionIndexByAlias(e, alias)
+	return err == nil
+}
+
+// ExtensionDetailsByAlias returns more detail than the mere presence of an extension by the provider.
+// See the ExtensionDetails structure.
+func (er ExtensionsResult) ExtensionDetailsByAlias(alias string) (*ExtensionDetails, error) {
+	e, err := extensions(er)
+	if err != nil {
+		return nil, err
+	}
+	i, err := extensionIndexByAlias(e, alias)
+	if err != nil {
+		return nil, err
+	}
+	ed := &ExtensionDetails{}
+	err = mapstructure.Decode(e[i], ed)
+	return ed, err
+}
+
+func extensionIndexByAlias(records []interface{}, alias string) (int, error) {
+	for i, er := range records {
+		extensionRecord := er.(map[string]interface{})
+		if extensionRecord["alias"] == alias {
+			return i, nil
+		}
+	}
+	return 0, ErrNotImplemented
+}
+
+func extensions(er ExtensionsResult) ([]interface{}, error) {
+	ei, ok := er["extensions"]
+	if !ok {
+		return nil, ErrNotImplemented
+	}
+	e := ei.(map[string]interface{})
+	vi, ok := e["values"]
+	if !ok {
+		return nil, ErrNotImplemented
+	}
+	v := vi.([]interface{})
+	return v, nil
+}
+
+// Aliases returns the set of extension handles, or "aliases" as OpenStack calls them.
+// These are not the names of the extensions, but rather opaque, symbolic monikers for their corresponding extension.
+// Use the ExtensionDetailsByAlias() method to query more information for an extension if desired.
+func (er ExtensionsResult) Aliases() ([]string, error) {
+	e, err := extensions(er)
+	if err != nil {
+		return nil, err
+	}
+	aliases := make([]string, len(e))
+	for i, ex := range e {
+		ext := ex.(map[string]interface{})
+		extn, ok := ext["alias"]
+		if ok {
+			aliases[i] = extn.(string)
+		}
+	}
+	return aliases, nil
+}
diff --git a/openstack/identity/v2/extensions_test.go b/openstack/identity/v2/extensions_test.go
new file mode 100644
index 0000000..3000fc0
--- /dev/null
+++ b/openstack/identity/v2/extensions_test.go
@@ -0,0 +1,112 @@
+package identity
+
+import (
+	"encoding/json"
+	"testing"
+)
+
+func TestIsExtensionAvailable(t *testing.T) {
+	// Make a response as we'd expect from the IdentityService.GetExtensions() call.
+	getExtensionsResults := make(map[string]interface{})
+	err := json.Unmarshal([]byte(queryResults), &getExtensionsResults)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	e := ExtensionsResult(getExtensionsResults)
+	for _, alias := range []string{"OS-KSADM", "OS-FEDERATION"} {
+		if !e.IsExtensionAvailable(alias) {
+			t.Errorf("Expected extension %s present.", alias)
+			return
+		}
+	}
+	if e.IsExtensionAvailable("blort") {
+		t.Errorf("Input JSON doesn't list blort as an extension")
+		return
+	}
+}
+
+func TestGetExtensionDetails(t *testing.T) {
+	// Make a response as we'd expect from the IdentityService.GetExtensions() call.
+	getExtensionsResults := make(map[string]interface{})
+	err := json.Unmarshal([]byte(queryResults), &getExtensionsResults)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	e := ExtensionsResult(getExtensionsResults)
+	ed, err := e.ExtensionDetailsByAlias("OS-KSADM")
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	actuals := map[string]string{
+		"name":        ed.Name,
+		"namespace":   ed.Namespace,
+		"updated":     ed.Updated,
+		"description": ed.Description,
+	}
+
+	expecteds := map[string]string{
+		"name":        "OpenStack Keystone Admin",
+		"namespace":   "http://docs.openstack.org/identity/api/ext/OS-KSADM/v1.0",
+		"updated":     "2013-07-11T17:14:00-00:00",
+		"description": "OpenStack extensions to Keystone v2.0 API enabling Administrative Operations.",
+	}
+
+	for k, v := range expecteds {
+		if actuals[k] != v {
+			t.Errorf("Expected %s \"%s\", got \"%s\" instead", k, v, actuals[k])
+			return
+		}
+	}
+}
+
+func TestMalformedResponses(t *testing.T) {
+	getExtensionsResults := make(map[string]interface{})
+	err := json.Unmarshal([]byte(bogusExtensionsResults), &getExtensionsResults)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	e := ExtensionsResult(getExtensionsResults)
+
+	_, err = e.ExtensionDetailsByAlias("OS-KSADM")
+	if err == nil {
+		t.Error("Expected ErrNotImplemented at least")
+		return
+	}
+	if err != ErrNotImplemented {
+		t.Error("Expected ErrNotImplemented")
+		return
+	}
+
+	if e.IsExtensionAvailable("anything at all") {
+		t.Error("No extensions are available with a bogus result.")
+		return
+	}
+}
+
+func TestAliases(t *testing.T) {
+	getExtensionsResults := make(map[string]interface{})
+	err := json.Unmarshal([]byte(queryResults), &getExtensionsResults)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	e := ExtensionsResult(getExtensionsResults)
+	aliases, err := e.Aliases()
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	extensions := (((e["extensions"]).(map[string]interface{}))["values"]).([]interface{})
+	if len(aliases) != len(extensions) {
+		t.Error("Expected one alias name per extension")
+		return
+	}
+}
diff --git a/openstack/identity/v2/requests.go b/openstack/identity/v2/requests.go
new file mode 100644
index 0000000..dbd367e
--- /dev/null
+++ b/openstack/identity/v2/requests.go
@@ -0,0 +1,120 @@
+package identity
+
+import (
+	"github.com/racker/perigee"
+)
+
+// AuthResults encapsulates the raw results from an authentication request.
+// As OpenStack allows extensions to influence the structure returned in
+// ways that Gophercloud cannot predict at compile-time, you should use
+// type-safe accessors to work with the data represented by this type,
+// such as ServiceCatalog() and Token().
+type AuthResults map[string]interface{}
+
+// AuthOptions lets anyone calling Authenticate() supply the required access
+// credentials.  At present, only Identity V2 API support exists; therefore,
+// only Username, Password, and optionally, TenantId are provided.  If future
+// Identity API versions become available, alternative fields unique to those
+// versions may appear here.
+//
+// Endpoint specifies the HTTP endpoint offering the Identity V2 API.
+// Required.
+//
+// Username is required if using Identity V2 API.  Consult with your provider's
+// control panel to discover your account's username.
+//
+// At most one of Password or ApiKey is required if using Identity V2 API.
+// Consult with your provider's control panel to discover your account's
+// preferred method of authentication.
+//
+// The TenantId and TenantName fields are optional for the Identity V2 API.
+// Some providers allow you to specify a TenantName instead of the TenantId.
+// Some require both.  Your provider's authentication policies will determine
+// how these fields influence authentication.
+//
+// AllowReauth should be set to true if you grant permission for Gophercloud to
+// cache your credentials in memory, and to allow Gophercloud to attempt to
+// re-authenticate automatically if/when your token expires.  If you set it to
+// false, it will not cache these settings, but re-authentication will not be
+// possible.  This setting defaults to false.
+type AuthOptions struct {
+	Endpoint         string
+	Username         string
+	Password, ApiKey string
+	TenantId         string
+	TenantName       string
+	AllowReauth      bool
+}
+
+// Authenticate passes the supplied credentials to the OpenStack provider for authentication.
+// If successful, the caller may use Token() to retrieve the authentication token,
+// and ServiceCatalog() to retrieve the set of services available to the API user.
+func Authenticate(options AuthOptions) (AuthResults, error) {
+	type AuthContainer struct {
+		Auth auth `json:"auth"`
+	}
+
+	var ar AuthResults
+
+	if options.Endpoint == "" {
+		return nil, ErrEndpoint
+	}
+
+	if (options.Username == "") || (options.Password == "" && options.ApiKey == "") {
+		return nil, ErrCredentials
+	}
+
+	url := options.Endpoint + "/tokens"
+	err := perigee.Post(url, perigee.Options{
+		ReqBody: &AuthContainer{
+			Auth: getAuthCredentials(options),
+		},
+		Results: &ar,
+	})
+	return ar, err
+}
+
+func getAuthCredentials(options AuthOptions) auth {
+	if options.ApiKey == "" {
+		return auth{
+			PasswordCredentials: &struct {
+				Username string `json:"username"`
+				Password string `json:"password"`
+			}{
+				Username: options.Username,
+				Password: options.Password,
+			},
+			TenantId:   options.TenantId,
+			TenantName: options.TenantName,
+		}
+	} else {
+		return auth{
+			ApiKeyCredentials: &struct {
+				Username string `json:"username"`
+				ApiKey   string `json:"apiKey"`
+			}{
+				Username: options.Username,
+				ApiKey:   options.ApiKey,
+			},
+			TenantId:   options.TenantId,
+			TenantName: options.TenantName,
+		}
+	}
+}
+
+type auth struct {
+	PasswordCredentials interface{} `json:"passwordCredentials,omitempty"`
+	ApiKeyCredentials   interface{} `json:"RAX-KSKEY:apiKeyCredentials,omitempty"`
+	TenantId            string      `json:"tenantId,omitempty"`
+	TenantName          string      `json:"tenantName,omitempty"`
+}
+
+func GetExtensions(options AuthOptions) (ExtensionsResult, error) {
+	var exts ExtensionsResult
+
+	url := options.Endpoint + "/extensions"
+	err := perigee.Get(url, perigee.Options{
+		Results: &exts,
+	})
+	return exts, err
+}
diff --git a/openstack/identity/v2/service_catalog.go b/openstack/identity/v2/service_catalog.go
new file mode 100644
index 0000000..035f671
--- /dev/null
+++ b/openstack/identity/v2/service_catalog.go
@@ -0,0 +1,102 @@
+package identity
+
+import "github.com/mitchellh/mapstructure"
+
+// ServiceCatalog provides a view into the service catalog from a previous, successful authentication.
+// OpenStack extensions may alter the structure of the service catalog in ways unpredictable to Go at compile-time,
+// so this structure serves as a convenient anchor for type-safe accessors and methods.
+type ServiceCatalog struct {
+	serviceDescriptions []interface{}
+}
+
+// CatalogEntry provides a type-safe interface to an Identity API V2 service
+// catalog listing.  Each class of service, such as cloud DNS or block storage
+// services, will have a single CatalogEntry representing it.
+//
+// Name will contain the provider-specified name for the service.
+//
+// If OpenStack defines a type for the service, this field will contain that
+// type string.  Otherwise, for provider-specific services, the provider may
+// assign their own type strings.
+//
+// Endpoints will let the caller iterate over all the different endpoints that
+// may exist for the service.
+//
+// Note: when looking for the desired service, try, whenever possible, to key
+// off the type field.  Otherwise, you'll tie the representation of the service
+// to a specific provider.
+type CatalogEntry struct {
+	Name      string
+	Type      string
+	Endpoints []Endpoint
+}
+
+// Endpoint represents a single API endpoint offered by a service.
+// It provides the public and internal URLs, if supported, along with a region specifier, again if provided.
+// The significance of the Region field will depend upon your provider.
+//
+// In addition, the interface offered by the service will have version information associated with it
+// through the VersionId, VersionInfo, and VersionList fields, if provided or supported.
+//
+// In all cases, fields which aren't supported by the provider and service combined will assume a zero-value ("").
+type Endpoint struct {
+	TenantId    string
+	PublicURL   string
+	InternalURL string
+	Region      string
+	VersionId   string
+	VersionInfo string
+	VersionList string
+}
+
+// GetServiceCatalog acquires the service catalog from a successful authentication's results.
+func GetServiceCatalog(ar AuthResults) (*ServiceCatalog, error) {
+	access := ar["access"].(map[string]interface{})
+	sds := access["serviceCatalog"].([]interface{})
+	sc := &ServiceCatalog{
+		serviceDescriptions: sds,
+	}
+	return sc, nil
+}
+
+// NumberOfServices yields the number of services the caller may use.  Note
+// that this does not necessarily equal the number of endpoints available for
+// use.
+func (sc *ServiceCatalog) NumberOfServices() int {
+	return len(sc.serviceDescriptions)
+}
+
+// CatalogEntries returns a slice of service catalog entries.
+// Each entry corresponds to a specific class of service offered by the API provider.
+// See the CatalogEntry structure for more details.
+func (sc *ServiceCatalog) CatalogEntries() ([]CatalogEntry, error) {
+	var err error
+	ces := make([]CatalogEntry, sc.NumberOfServices())
+	for i, sd := range sc.serviceDescriptions {
+		d := sd.(map[string]interface{})
+		eps, err := parseEndpoints(d["endpoints"].([]interface{}))
+		if err != nil {
+			return ces, err
+		}
+		ces[i] = CatalogEntry{
+			Name:      d["name"].(string),
+			Type:      d["type"].(string),
+			Endpoints: eps,
+		}
+	}
+	return ces, err
+}
+
+func parseEndpoints(eps []interface{}) ([]Endpoint, error) {
+	var err error
+	result := make([]Endpoint, len(eps))
+	for i, ep := range eps {
+		e := Endpoint{}
+		err = mapstructure.Decode(ep, &e)
+		if err != nil {
+			return result, err
+		}
+		result[i] = e
+	}
+	return result, err
+}
diff --git a/openstack/identity/v2/service_catalog_test.go b/openstack/identity/v2/service_catalog_test.go
new file mode 100644
index 0000000..f810609
--- /dev/null
+++ b/openstack/identity/v2/service_catalog_test.go
@@ -0,0 +1,91 @@
+package identity
+
+import (
+	"encoding/json"
+	"testing"
+)
+
+func TestServiceCatalog(t *testing.T) {
+	authResults := make(map[string]interface{})
+	err := json.Unmarshal([]byte(authResultsOK), &authResults)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	sc, err := GetServiceCatalog(authResults)
+	if err != nil {
+		panic(err)
+	}
+
+	if sc.NumberOfServices() != 3 {
+		t.Errorf("Expected 3 services; got %d", sc.NumberOfServices())
+	}
+
+	ces, err := sc.CatalogEntries()
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	for _, ce := range ces {
+		if strNotInStrList(ce.Name, "Cloud Servers", "Cloud Files", "DNS-as-a-Service") {
+			t.Errorf("Expected \"%s\" to be one of Cloud Servers, Cloud Files, or DNS-as-a-Service", ce.Name)
+			return
+		}
+
+		if strNotInStrList(ce.Type, "dnsextension:dns", "object-store", "compute") {
+			t.Errorf("Expected \"%s\" to be one of dnsextension:dns, object-store, or compute")
+			return
+		}
+	}
+
+	eps := endpointsFor(ces, "compute")
+	if len(eps) != 2 {
+		t.Errorf("Expected 2 endpoints for compute service")
+		return
+	}
+	for _, ep := range eps {
+		if strNotInStrList(ep.VersionId, "1", "1.1", "1.1") {
+			t.Errorf("Expected versionId field of compute resource to be one of 1 or 1.1")
+			return
+		}
+	}
+
+	eps = endpointsFor(ces, "object-store")
+	if len(eps) != 2 {
+		t.Errorf("Expected 2 endpoints for object-store service")
+		return
+	}
+	for _, ep := range eps {
+		if ep.VersionId != "1" {
+			t.Errorf("Expected only version 1 object store API version")
+			return
+		}
+	}
+
+	eps = endpointsFor(ces, "dnsextension:dns")
+	if len(eps) != 1 {
+		t.Errorf("Expected 1 endpoint for DNS-as-a-Service service")
+		return
+	}
+	if eps[0].VersionId != "2.0" {
+		t.Errorf("Expected version 2.0 of DNS-as-a-Service service")
+		return
+	}
+}
+
+func endpointsFor(ces []CatalogEntry, t string) []Endpoint {
+	for _, ce := range ces {
+		if ce.Type == t {
+			return ce.Endpoints
+		}
+	}
+	panic("Precondition violated")
+}
+
+func strNotInStrList(needle, haystack1, haystack2, haystack3 string) bool {
+	if (needle != haystack1) && (needle != haystack2) && (needle != haystack3) {
+		return true
+	}
+	return false
+}
diff --git a/openstack/identity/v2/token.go b/openstack/identity/v2/token.go
new file mode 100644
index 0000000..d50cce0
--- /dev/null
+++ b/openstack/identity/v2/token.go
@@ -0,0 +1,72 @@
+package identity
+
+import (
+	"github.com/mitchellh/mapstructure"
+)
+
+// Token provides only the most basic information related to an authentication token.
+//
+// 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.
+//
+// Expires 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.
+//
+// TenantId provides the canonical means of identifying a tenant.
+// As with Id, this field is defined to be opaque, so do not depend on its content.
+// It is safe, however, to compare for equality.
+//
+// TenantName provides a human-readable tenant name corresponding to the TenantId.
+type Token struct {
+	Id, Expires          string
+	TenantId, TenantName string
+}
+
+// GetToken, if successful, yields an unpacked collection of fields related to the user's access credentials, called a "token."
+// See the Token structure for more details.
+func GetToken(m AuthResults) (*Token, error) {
+	type (
+		Tenant struct {
+			Id   string
+			Name string
+		}
+
+		TokenDesc struct {
+			Id      string `mapstructure:"id"`
+			Expires string `mapstructure:"expires"`
+			Tenant
+		}
+	)
+
+	accessMap, err := getSubmap(m, "access")
+	if err != nil {
+		return nil, err
+	}
+	tokenMap, err := getSubmap(accessMap, "token")
+	if err != nil {
+		return nil, err
+	}
+	t := &TokenDesc{}
+	err = mapstructure.Decode(tokenMap, t)
+	if err != nil {
+		return nil, err
+	}
+	td := &Token{
+		Id:         t.Id,
+		Expires:    t.Expires,
+		TenantId:   t.Tenant.Id,
+		TenantName: t.Tenant.Name,
+	}
+	return td, nil
+}
+
+func getSubmap(m map[string]interface{}, name string) (map[string]interface{}, error) {
+	entry, ok := m[name]
+	if !ok {
+		return nil, ErrNotImplemented
+	}
+	return entry.(map[string]interface{}), nil
+}
diff --git a/openstack/identity/v2/token_test.go b/openstack/identity/v2/token_test.go
new file mode 100644
index 0000000..5e96496
--- /dev/null
+++ b/openstack/identity/v2/token_test.go
@@ -0,0 +1,25 @@
+package identity
+
+import (
+	"encoding/json"
+	"testing"
+)
+
+func TestAccessToken(t *testing.T) {
+	authResults := make(map[string]interface{})
+	err := json.Unmarshal([]byte(authResultsOK), &authResults)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	tok, err := GetToken(authResults)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	if tok.Id != "ab48a9efdfedb23ty3494" {
+		t.Errorf("Expected token \"ab48a9efdfedb23ty3494\"; got \"%s\" instead", tok.Id)
+		return
+	}
+}