Ookay, last reshuffle.

 * openstack.NewClient() normalizes the identity endpoint with a trailing slash, and sets base and endpoint.
 * utils.ChooseVersion() checks suffixes first to short-circuit actual version calls.
 * gophercloud.ProviderClient distinguishes between the root of all identity services (IdentityBase)
   and the endpoint of the requested auth service (IdentityEndpoint).
diff --git a/openstack/client.go b/openstack/client.go
index d244053..7e9b628 100644
--- a/openstack/client.go
+++ b/openstack/client.go
@@ -2,6 +2,7 @@
 
 import (
 	"fmt"
+	"net/url"
 	"strings"
 
 	"github.com/rackspace/gophercloud"
@@ -22,11 +23,32 @@
 // This is useful if you wish to explicitly control the version of the identity service that's used for authentication explicitly,
 // for example.
 func NewClient(endpoint string) (*gophercloud.ProviderClient, error) {
+	u, err := url.Parse(endpoint)
+	if err != nil {
+		return nil, err
+	}
+	hadPath := u.Path != ""
+	u.Path, u.RawQuery, u.Fragment = "", "", ""
+	base := u.String()
+
 	if !strings.HasSuffix(endpoint, "/") {
 		endpoint = endpoint + "/"
 	}
+	if !strings.HasSuffix(base, "/") {
+		base = base + "/"
+	}
 
-	return &gophercloud.ProviderClient{IdentityEndpoint: endpoint}, nil
+	if hadPath {
+		return &gophercloud.ProviderClient{
+			IdentityBase:     base,
+			IdentityEndpoint: endpoint,
+		}, nil
+	}
+
+	return &gophercloud.ProviderClient{
+		IdentityBase:     base,
+		IdentityEndpoint: "",
+	}, nil
 }
 
 // AuthenticatedClient logs in to an OpenStack cloud found at the identity endpoint specified by options, acquires a token, and
@@ -49,61 +71,55 @@
 // Authenticate or re-authenticate against the most recent identity service supported at the provided endpoint.
 func Authenticate(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error {
 	versions := []*utils.Version{
-		&utils.Version{ID: v20, Priority: 20},
-		&utils.Version{ID: v30, Priority: 30},
+		&utils.Version{ID: v20, Priority: 20, Suffix: "/v2.0/"},
+		&utils.Version{ID: v30, Priority: 30, Suffix: "/v3/"},
 	}
 
-	chosen, endpoint, err := utils.ChooseVersion(client.IdentityEndpoint, versions)
+	chosen, endpoint, err := utils.ChooseVersion(client.IdentityBase, client.IdentityEndpoint, versions)
 	if err != nil {
 		return err
 	}
 
 	switch chosen.ID {
 	case v20:
-		v2Client := NewIdentityV2(client)
-		v2Client.Endpoint = endpoint
-
-		result, err := identity2.Authenticate(v2Client, options)
-		if err != nil {
-			return err
-		}
-
-		token, err := identity2.GetToken(result)
-		if err != nil {
-			return err
-		}
-
-		client.TokenID = token.ID
-		client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
-			return v2endpointLocator(result, opts)
-		}
-
-		return nil
+		return v2auth(client, endpoint, options)
 	case v30:
-		// Override the generated service endpoint with the one returned by the version endpoint.
-		v3Client := NewIdentityV3(client)
-		v3Client.Endpoint = endpoint
-
-		result, err := tokens3.Create(v3Client, options, nil)
-		if err != nil {
-			return err
-		}
-
-		client.TokenID, err = result.TokenID()
-		if err != nil {
-			return err
-		}
-		client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
-			return v3endpointLocator(v3Client, opts)
-		}
-
-		return nil
+		return v3auth(client, endpoint, options)
 	default:
 		// The switch statement must be out of date from the versions list.
 		return fmt.Errorf("Unrecognized identity version: %s", chosen.ID)
 	}
 }
 
+// AuthenticateV2 explicitly authenticates against the identity v2 endpoint.
+func AuthenticateV2(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error {
+	return v2auth(client, "", options)
+}
+
+func v2auth(client *gophercloud.ProviderClient, endpoint string, options gophercloud.AuthOptions) error {
+	v2Client := NewIdentityV2(client)
+	if endpoint != "" {
+		v2Client.Endpoint = endpoint
+	}
+
+	result, err := identity2.Authenticate(v2Client, options)
+	if err != nil {
+		return err
+	}
+
+	token, err := identity2.GetToken(result)
+	if err != nil {
+		return err
+	}
+
+	client.TokenID = token.ID
+	client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
+		return v2endpointLocator(result, opts)
+	}
+
+	return nil
+}
+
 func v2endpointLocator(authResults identity2.AuthResults, opts gophercloud.EndpointOpts) (string, error) {
 	catalog, err := identity2.GetServiceCatalog(authResults)
 	if err != nil {
@@ -150,6 +166,34 @@
 	return "", gophercloud.ErrEndpointNotFound
 }
 
+// AuthenticateV3 explicitly authenticates against the identity v3 service.
+func AuthenticateV3(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error {
+	return v3auth(client, "", options)
+}
+
+func v3auth(client *gophercloud.ProviderClient, endpoint string, options gophercloud.AuthOptions) error {
+	// Override the generated service endpoint with the one returned by the version endpoint.
+	v3Client := NewIdentityV3(client)
+	if endpoint != "" {
+		v3Client.Endpoint = endpoint
+	}
+
+	result, err := tokens3.Create(v3Client, options, nil)
+	if err != nil {
+		return err
+	}
+
+	client.TokenID, err = result.TokenID()
+	if err != nil {
+		return err
+	}
+	client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
+		return v3endpointLocator(v3Client, opts)
+	}
+
+	return nil
+}
+
 func v3endpointLocator(v3Client *gophercloud.ServiceClient, opts gophercloud.EndpointOpts) (string, error) {
 	// Transform URLType into an Interface.
 	var endpointInterface = endpoints3.InterfacePublic
@@ -233,7 +277,7 @@
 
 // NewIdentityV2 creates a ServiceClient that may be used to interact with the v2 identity service.
 func NewIdentityV2(client *gophercloud.ProviderClient) *gophercloud.ServiceClient {
-	v2Endpoint := client.IdentityEndpoint + "v2.0/"
+	v2Endpoint := client.IdentityBase + "v2.0/"
 
 	return &gophercloud.ServiceClient{
 		Provider: client,
@@ -243,7 +287,7 @@
 
 // NewIdentityV3 creates a ServiceClient that may be used to access the v3 identity service.
 func NewIdentityV3(client *gophercloud.ProviderClient) *gophercloud.ServiceClient {
-	v3Endpoint := client.IdentityEndpoint + "v3/"
+	v3Endpoint := client.IdentityBase + "v3/"
 
 	return &gophercloud.ServiceClient{
 		Provider: client,
diff --git a/openstack/utils/choose_version.go b/openstack/utils/choose_version.go
index 402a870..753f8f8 100644
--- a/openstack/utils/choose_version.go
+++ b/openstack/utils/choose_version.go
@@ -2,7 +2,6 @@
 
 import (
 	"fmt"
-	"net/url"
 	"strings"
 
 	"github.com/racker/perigee"
@@ -11,6 +10,7 @@
 // Version is a supported API version, corresponding to a vN package within the appropriate service.
 type Version struct {
 	ID       string
+	Suffix   string
 	Priority int
 }
 
@@ -23,7 +23,7 @@
 // ChooseVersion queries the base endpoint of a API to choose the most recent non-experimental alternative from a service's
 // published versions.
 // It returns the highest-Priority Version among the alternatives that are provided, as well as its corresponding endpoint.
-func ChooseVersion(identityEndpoint string, recognized []*Version) (*Version, string, error) {
+func ChooseVersion(identityBase string, identityEndpoint string, recognized []*Version) (*Version, string, error) {
 	type linkResp struct {
 		Href string `json:"href"`
 		Rel  string `json:"rel"`
@@ -43,16 +43,23 @@
 		Versions versionsResp `json:"versions"`
 	}
 
-	// Normalize the identity endpoint that's provided by trimming any path, query or fragment from the URL.
-	u, err := url.Parse(identityEndpoint)
-	if err != nil {
-		return nil, "", err
+	normalize := func(endpoint string) string {
+		if !strings.HasSuffix(endpoint, "/") {
+			return endpoint + "/"
+		}
+		return endpoint
 	}
-	u.Path, u.RawQuery, u.Fragment = "", "", ""
-	normalized := u.String()
+	identityEndpoint = normalize(identityEndpoint)
+
+	// If a full endpoint is specified, check version suffixes for a match first.
+	for _, v := range recognized {
+		if strings.HasSuffix(identityEndpoint, v.Suffix) {
+			return v, identityEndpoint, nil
+		}
+	}
 
 	var resp response
-	_, err = perigee.Request("GET", normalized, perigee.Options{
+	_, err := perigee.Request("GET", identityBase, perigee.Options{
 		Results: &resp,
 		OkCodes: []int{200, 300},
 	})
@@ -69,14 +76,6 @@
 	var highest *Version
 	var endpoint string
 
-	normalize := func(endpoint string) string {
-		if !strings.HasSuffix(endpoint, "/") {
-			return endpoint + "/"
-		}
-		return endpoint
-	}
-	normalizedGiven := normalize(identityEndpoint)
-
 	for _, value := range resp.Versions.Values {
 		href := ""
 		for _, link := range value.Links {
@@ -87,9 +86,9 @@
 
 		if matching, ok := byID[value.ID]; ok {
 			// Prefer a version that exactly matches the provided endpoint.
-			if href == normalizedGiven {
+			if href == identityEndpoint {
 				if href == "" {
-					return nil, "", fmt.Errorf("Endpoint missing in version %s response from %s", value.ID, normalized)
+					return nil, "", fmt.Errorf("Endpoint missing in version %s response from %s", value.ID, identityBase)
 				}
 				return matching, href, nil
 			}
@@ -105,10 +104,10 @@
 	}
 
 	if highest == nil {
-		return nil, "", fmt.Errorf("No supported version available from endpoint %s", normalized)
+		return nil, "", fmt.Errorf("No supported version available from endpoint %s", identityBase)
 	}
 	if endpoint == "" {
-		return nil, "", fmt.Errorf("Endpoint missing in version %s response from %s", highest.ID, normalized)
+		return nil, "", fmt.Errorf("Endpoint missing in version %s response from %s", highest.ID, identityBase)
 	}
 
 	return highest, endpoint, nil
diff --git a/openstack/utils/choose_version_test.go b/openstack/utils/choose_version_test.go
index b724ee8..9552696 100644
--- a/openstack/utils/choose_version_test.go
+++ b/openstack/utils/choose_version_test.go
@@ -40,10 +40,10 @@
 	defer testhelper.TeardownHTTP()
 	setupVersionHandler()
 
-	v2 := &Version{ID: "v2.0", Priority: 2}
-	v3 := &Version{ID: "v3.0", Priority: 3}
+	v2 := &Version{ID: "v2.0", Priority: 2, Suffix: "blarg"}
+	v3 := &Version{ID: "v3.0", Priority: 3, Suffix: "hargl"}
 
-	v, endpoint, err := ChooseVersion(testhelper.Endpoint(), []*Version{v2, v3})
+	v, endpoint, err := ChooseVersion(testhelper.Endpoint(), "", []*Version{v2, v3})
 
 	if err != nil {
 		t.Fatalf("Unexpected error from ChooseVersion: %v", err)
@@ -64,10 +64,32 @@
 	defer testhelper.TeardownHTTP()
 	setupVersionHandler()
 
-	v2 := &Version{ID: "v2.0", Priority: 2}
-	v3 := &Version{ID: "v3.0", Priority: 3}
+	v2 := &Version{ID: "v2.0", Priority: 2, Suffix: "nope"}
+	v3 := &Version{ID: "v3.0", Priority: 3, Suffix: "northis"}
 
-	v, endpoint, err := ChooseVersion(testhelper.Endpoint()+"v2.0/", []*Version{v2, v3})
+	v, endpoint, err := ChooseVersion(testhelper.Endpoint(), testhelper.Endpoint()+"v2.0/", []*Version{v2, v3})
+	if err != nil {
+		t.Fatalf("Unexpected error from ChooseVersion: %v", err)
+	}
+
+	if v != v2 {
+		t.Errorf("Expected %#v to win, but %#v did instead", v2, v)
+	}
+
+	expected := testhelper.Endpoint() + "v2.0/"
+	if endpoint != expected {
+		t.Errorf("Expected endpoint [%s], but was [%s] instead", expected, endpoint)
+	}
+}
+
+func TestChooseVersionFromSuffix(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	v2 := &Version{ID: "v2.0", Priority: 2, Suffix: "/v2.0/"}
+	v3 := &Version{ID: "v3.0", Priority: 3, Suffix: "/v3.0/"}
+
+	v, endpoint, err := ChooseVersion(testhelper.Endpoint(), testhelper.Endpoint()+"v2.0/", []*Version{v2, v3})
 	if err != nil {
 		t.Fatalf("Unexpected error from ChooseVersion: %v", err)
 	}
diff --git a/openstack/utils/client.go b/openstack/utils/client.go
index 7e3aa12..f8a6c57 100644
--- a/openstack/utils/client.go
+++ b/openstack/utils/client.go
@@ -47,7 +47,7 @@
 		Options: ao,
 	}
 
-	c := &gophercloud.ServiceClient{Endpoint: ao.IdentityEndpoint}
+	c := &gophercloud.ServiceClient{Endpoint: ao.IdentityEndpoint + "/"}
 	ar, err := identity.Authenticate(c, ao)
 	if err != nil {
 		return client, err
diff --git a/provider_client.go b/provider_client.go
index 971276e..2be665e 100644
--- a/provider_client.go
+++ b/provider_client.go
@@ -6,9 +6,14 @@
 // providing whatever authentication credentials are required.
 type ProviderClient struct {
 
-	// IdentityEndpoint is the front door to an openstack provider.
+	// IdentityBase is the front door to an openstack provider.
 	// Generally this will be populated when you authenticate.
 	// It should be the *root* resource of the identity service, not of a specific identity version.
+	IdentityBase string
+
+	// IdentityEndpoint is the originally requested identity endpoint.
+	// This may be a specific version of the identity service, in which case that endpoint is used rather than querying the
+	// version-negotiation endpoint.
 	IdentityEndpoint string
 
 	// TokenID is the most recently valid token issued.