Repository: gophercloud/gophercloud

Issue:
Application credentials doesn't work when using access_rules
### What happened?

Application credentials doesn't work when using access_rules. This is due to a (poorly documented) missing header called OpenStack-Identity-Access-Rules which enables access_rules when using application credentials. To allow using access_rules + application credentials the keystone token get and validate functions need to send this header in their request.

python implementation reference: https://opendev.org/openstack/python-keystoneclient/src/branch/master/keystoneclient/v3/tokens.py#L60

### Version

v2 (current)

Discussion:
@kayrus: when application credentials are created with access rules, these rules must be honored by the end service, e.g. neutron, nova or cinder. and these services must be configured accordingly, e.g. include service catalog on token response. are you sure that the corresponding services work fine without gophercloud? have you tried to issue a token using application credentials and use this token with a `curl -H "X-Auth-Token: my-issued-token" http://my-service/etc/api/resource`?@winiciusallan: I've tried to reproduce the same error as reported in ORC, and I was able to make the request using the generated token. 

```bash
> curl -s -H "Content-Type: application/json" -H "X-Auth-Token: $TOKEN" http://devstack:9696/v2.0/networks | jq .
{
  "networks": [
    {
....
```

I configured my clouds.yaml with the application_credentials and created a token with it. I used that same token on the request.@kayrus: @eshulman2 could you please provide more debug details on with and wo the `OpenStack-Identity-Access-Rules` header?@eshulman2: Hi @kayrus sure,

This is related to this ORC [bug](https://github.com/k-orc/openstack-resource-controller/issues/596) in the [feature spec](https://specs.openstack.org/openstack/keystone-specs/specs/keystone/train/capabilities-app-creds.html) it is mentioned that:

> 1. When requesting token validation, keystonemiddleware (or any 3rd party application that supports access rule enforcement) sets an Openstack-Identity-Access-Rules header with a version string as its value. Token validation for an application credential with a access rule list will only succeed if this header is present. The version string will allow us to safely extend this feature by invalidating tokens using the extended version in situations where keystonemiddleware only supports an older version of this feature.
> 
> 2. If there is no Openstack-Identity-Access-Rules header in the token validation request, token validation fails.

here is an example usage with and without this flag:
without:
```bash
$ TOKEN=$(openstack token issue -c id -f value)
$ curl -g -i -X GET -H "Accept: application/json" -H "X-Auth-Token: ${TOKEN}" -H "X-Subject-Token: ${TOKEN}" "https://mycloud.com:13000/v3/auth/tokens"
HTTP/1.1 404 NOT FOUND
date: Wed, 10 Dec 2025 16:49:55 GMT
server: Apache
content-length: 80
vary: X-Auth-Token
x-openstack-request-id: req-77b5fd64-8267-4038-88b7-23c77ea92c44
content-type: application/json

{"error":{"code":404,"message":"Failed to validate token","title":"Not Found"}}
```
with:
```bash
$ TOKEN=$(openstack token issue -c id -f value)
$ curl -H "OpenStack-Identity-Access-Rules: 1" -H "Accept: application/json" -H "X-Auth-Token: ${TOKEN}" -H "X-Subject-Token: ${TOKEN}" "https://mycloud.com:13000/v3/auth/tokens" 2>/dev/null | jq .token.expires_at
"2025-12-11T10:35:25.000000Z"
```

When identifying to other services that are not keystone and are using keystone middleware you won't hit this issue as it is strictly set for all requests:
https://opendev.org/openstack/keystonemiddleware/src/branch/master/keystonemiddleware/auth_token/_identity.py#L24
https://opendev.org/openstack/keystonemiddleware/src/branch/master/keystonemiddleware/auth_token/_identity.py#L51

This means that only for direct keystone request you would need to set it as it is already set for all middleware using services.@eshulman2: I don't like this draft but I didn't have any better idea on implementing it without breaking the API https://github.com/gophercloud/gophercloud/pull/3576

Solution patch:
diff --git a/openstack/client.go b/openstack/client.go
index 2c81b27f..234f23eb 100644
--- a/openstack/client.go
+++ b/openstack/client.go
@@ -206,7 +206,7 @@ func v3auth(ctx context.Context, client *gophercloud.ProviderClient, endpoint st
 		}
 
 		v3Client.SetToken(tokenID)
-		result := tokens3.Get(ctx, v3Client, tokenID)
+		result := tokens3.Get(ctx, v3Client, tokenID, nil)
 		if result.Err != nil {
 			return result.Err
 		}
diff --git a/openstack/identity/v3/tokens/doc.go b/openstack/identity/v3/tokens/doc.go
index c711d0c2..597d781d 100644
--- a/openstack/identity/v3/tokens/doc.go
+++ b/openstack/identity/v3/tokens/doc.go
@@ -103,5 +103,53 @@ Example to Create a Token from a Username and Password with Project Name Scope
 	if err != nil {
 		panic(err)
 	}
+
+Example to Get a Token
+
+	token, err := tokens.Get(context.TODO(), identityClient, "token_id", nil).ExtractToken()
+	if err != nil {
+		panic(err)
+	}
+
+# Example to Get a Token Created with Application Credentials Access Rules
+
+When validating or retrieving tokens that were created using application
+credentials with access rules, the OpenStack-Identity-Access-Rules header
+must be sent. Without this header, Keystone will return a 404 Not Found error.
+
+See https://docs.openstack.org/keystone/latest/user/application_credentials.html
+
+	getOpts := tokens.GetOpts{
+		AccessRulesVersion: "1.0",
+	}
+	token, err := tokens.Get(context.TODO(), identityClient, "token_id", getOpts).ExtractToken()
+	if err != nil {
+		panic(err)
+	}
+
+Example to Validate a Token
+
+	ok, err := tokens.Validate(context.TODO(), identityClient, "token_id", nil)
+	if err != nil {
+		panic(err)
+	}
+
+	if ok {
+		fmt.Println("Token is valid")
+	}
+
+Example to Validate a Token Created with Application Credentials Access Rules
+
+	validateOpts := tokens.ValidateOpts{
+		AccessRulesVersion: "1.0",
+	}
+	ok, err := tokens.Validate(context.TODO(), identityClient, "token_id", validateOpts)
+	if err != nil {
+		panic(err)
+	}
+
+	if ok {
+		fmt.Println("Token is valid")
+	}
 */
 package tokens
diff --git a/openstack/identity/v3/tokens/requests.go b/openstack/identity/v3/tokens/requests.go
index fa8b925d..40b41e44 100644
--- a/openstack/identity/v3/tokens/requests.go
+++ b/openstack/identity/v3/tokens/requests.go
@@ -2,10 +2,13 @@ package tokens
 
 import (
 	"context"
+	"maps"
 
 	"github.com/gophercloud/gophercloud/v2"
 )
 
+const xSubjectTokenHeader = "X-Subject-Token"
+
 // Scope allows a created token to be limited to a specific domain or project.
 type Scope struct {
 	ProjectID   string
@@ -118,12 +121,6 @@ func (opts *AuthOptions) ToTokenV3HeadersMap(map[string]any) (map[string]string,
 	return nil, nil
 }
 
-func subjectTokenHeaders(subjectToken string) map[string]string {
-	return map[string]string{
-		"X-Subject-Token": subjectToken,
-	}
-}
-
 // Create authenticates and either generates a new token, or changes the Scope
 // of an existing token.
 func Create(ctx context.Context, c *gophercloud.ServiceClient, opts AuthOptionsBuilder) (r CreateResult) {
@@ -146,20 +143,79 @@ func Create(ctx context.Context, c *gophercloud.ServiceClient, opts AuthOptionsB
 	return
 }
 
+// GetOptsBuilder allows extensions to add additional parameters to
+// the Get request.
+type GetOptsBuilder interface {
+	ToTokenGetParams() (map[string]string, error)
+}
+
+// GetOpts provides options for the Get request.
+type GetOpts struct {
+	// AccessRulesVersion specifies the OpenStack-Identity-Access-Rules header
+	// version. This is required when getting tokens that were created using
+	// application credentials with access rules. Versions less than 1 will cause
+	// Keystone to ignore access rules validation.
+	AccessRulesVersion string `h:"OpenStack-Identity-Access-Rules"`
+}
+
+// ToTokenGetParams formats GetOpts into request headers.
+func (opts GetOpts) ToTokenGetParams() (map[string]string, error) {
+	return gophercloud.BuildHeaders(opts)
+}
+
 // Get validates and retrieves information about another token.
-func Get(ctx context.Context, c *gophercloud.ServiceClient, token string) (r GetResult) {
+func Get(ctx context.Context, c *gophercloud.ServiceClient, token string, opts GetOptsBuilder) (r GetResult) {
+	h := map[string]string{xSubjectTokenHeader: token}
+	if opts != nil {
+		b, err := opts.ToTokenGetParams()
+		if err != nil {
+			r.Err = err
+			return
+		}
+		maps.Copy(h, b)
+	}
+
 	resp, err := c.Get(ctx, tokenURL(c), &r.Body, &gophercloud.RequestOpts{
-		MoreHeaders: subjectTokenHeaders(token),
+		MoreHeaders: h,
 		OkCodes:     []int{200, 203},
 	})
 	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
 	return
 }
 
+// ValidateOptsBuilder allows extensions to add additional parameters to
+// the Validate request.
+type ValidateOptsBuilder interface {
+	ToTokenValidateParams() (map[string]string, error)
+}
+
+// ValidateOpts provides options for the Validate request.
+type ValidateOpts struct {
+	// AccessRulesVersion specifies the OpenStack-Identity-Access-Rules header
+	// version. This is required when validating tokens that were created using
+	// application credentials with access rules. Versions less than 1 will cause
+	// Keystone to ignore access rules validation.
+	AccessRulesVersion string `h:"OpenStack-Identity-Access-Rules"`
+}
+
+// ToTokenValidateParams formats ValidateOpts into request headers.
+func (opts ValidateOpts) ToTokenValidateParams() (map[string]string, error) {
+	return gophercloud.BuildHeaders(opts)
+}
+
 // Validate determines if a specified token is valid or not.
-func Validate(ctx context.Context, c *gophercloud.ServiceClient, token string) (bool, error) {
+func Validate(ctx context.Context, c *gophercloud.ServiceClient, token string, opts ValidateOptsBuilder) (bool, error) {
+	h := map[string]string{xSubjectTokenHeader: token}
+	if opts != nil {
+		b, err := opts.ToTokenValidateParams()
+		if err != nil {
+			return false, err
+		}
+		maps.Copy(h, b)
+	}
+
 	resp, err := c.Head(ctx, tokenURL(c), &gophercloud.RequestOpts{
-		MoreHeaders: subjectTokenHeaders(token),
+		MoreHeaders: h,
 		OkCodes:     []int{200, 204, 404},
 	})
 	if err != nil {
@@ -172,7 +228,7 @@ func Validate(ctx context.Context, c *gophercloud.ServiceClient, token string) (
 // Revoke immediately makes specified token invalid.
 func Revoke(ctx context.Context, c *gophercloud.ServiceClient, token string) (r RevokeResult) {
 	resp, err := c.Delete(ctx, tokenURL(c), &gophercloud.RequestOpts{
-		MoreHeaders: subjectTokenHeaders(token),
+		MoreHeaders: map[string]string{xSubjectTokenHeader: token},
 	})
 	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
 	return
diff --git a/openstack/identity/v3/tokens/results.go b/openstack/identity/v3/tokens/results.go
index bd61a9a6..1ec1ce40 100644
--- a/openstack/identity/v3/tokens/results.go
+++ b/openstack/identity/v3/tokens/results.go
@@ -88,6 +88,33 @@ type Trust struct {
 	TrustorUserID TrustUser `json:"trustor_user"`
 }
 
+// AccessRule represents an access rule for an application credential.
+type AccessRule struct {
+	// The ID of the access rule.
+	ID string `json:"id"`
+	// The API path that the application credential is permitted to access.
+	Path string `json:"path"`
+	// The request method that the application credential is permitted to use
+	// for a given API endpoint.
+	Method string `json:"method"`
+	// The service type identifier for the service that the application
+	// credential is permitted to access.
+	Service string `json:"service"`
+}
+
+// ApplicationCredential represents the application credential information
+// included in a token when the token was created using an application credential.
+type ApplicationCredential struct {
+	// The ID of the application credential.
+	ID string `json:"id"`
+	// The name of the application credential.
+	Name string `json:"name"`
+	// A flag indicating whether the application credential is restricted.
+	Restricted bool `json:"restricted"`
+	// A list of access rules for the application credential.
+	AccessRules []AccessRule `json:"access_rules"`
+}
+
 // commonResult is the response from a request. A commonResult has various
 // methods which can be used to extract different details about the result.
 type commonResult struct {
@@ -181,6 +208,17 @@ func (r commonResult) ExtractTrust() (*Trust, error) {
 	return s.Trust, err
 }
 
+// ExtractApplicationCredential returns the ApplicationCredential that was used
+// to create the token. This is only present when the token was created using
+// an application credential.
+func (r commonResult) ExtractApplicationCredential() (*ApplicationCredential, error) {
+	var s struct {
+		ApplicationCredential *ApplicationCredential `json:"application_credential"`
+	}
+	err := r.ExtractInto(&s)
+	return s.ApplicationCredential, err
+}
+
 // CreateResult is the response from a Create request. Use ExtractToken()
 // to interpret it as a Token, or ExtractServiceCatalog() to interpret it
 // as a service catalog.



Test patch:
diff --git a/internal/acceptance/openstack/client_test.go b/internal/acceptance/openstack/client_test.go
index 3a732d42..acb0399b 100644
--- a/internal/acceptance/openstack/client_test.go
+++ b/internal/acceptance/openstack/client_test.go
@@ -71,11 +71,11 @@ func TestEC2AuthMethod(t *testing.T) {
 	th.AssertNoErr(t, err)
 	tools.PrintResource(t, token)
 
-	user, err := tokens.Get(context.TODO(), client, token.ID).ExtractUser()
+	user, err := tokens.Get(context.TODO(), client, token.ID, nil).ExtractUser()
 	th.AssertNoErr(t, err)
 	tools.PrintResource(t, user)
 
-	project, err := tokens.Get(context.TODO(), client, token.ID).ExtractProject()
+	project, err := tokens.Get(context.TODO(), client, token.ID, nil).ExtractProject()
 	th.AssertNoErr(t, err)
 	tools.PrintResource(t, project)
 
diff --git a/internal/acceptance/openstack/identity/v3/applicationcredentials_test.go b/internal/acceptance/openstack/identity/v3/applicationcredentials_test.go
index 7a4ad01e..f8bb2ae1 100644
--- a/internal/acceptance/openstack/identity/v3/applicationcredentials_test.go
+++ b/internal/acceptance/openstack/identity/v3/applicationcredentials_test.go
@@ -50,15 +50,15 @@ func TestApplicationCredentialsCRD(t *testing.T) {
 	th.AssertNoErr(t, err)
 	tools.PrintResource(t, token)
 
-	user, err := tokens.Get(context.TODO(), client, token.ID).ExtractUser()
+	user, err := tokens.Get(context.TODO(), client, token.ID, nil).ExtractUser()
 	th.AssertNoErr(t, err)
 	tools.PrintResource(t, user)
 
-	roles, err := tokens.Get(context.TODO(), client, token.ID).ExtractRoles()
+	roles, err := tokens.Get(context.TODO(), client, token.ID, nil).ExtractRoles()
 	th.AssertNoErr(t, err)
 	tools.PrintResource(t, roles)
 
-	project, err := tokens.Get(context.TODO(), client, token.ID).ExtractProject()
+	project, err := tokens.Get(context.TODO(), client, token.ID, nil).ExtractProject()
 	th.AssertNoErr(t, err)
 	tools.PrintResource(t, project)
 
@@ -194,7 +194,7 @@ func TestApplicationCredentialsAccessRules(t *testing.T) {
 	th.AssertNoErr(t, err)
 	tools.PrintResource(t, token)
 
-	user, err := tokens.Get(context.TODO(), client, token.ID).ExtractUser()
+	user, err := tokens.Get(context.TODO(), client, token.ID, nil).ExtractUser()
 	th.AssertNoErr(t, err)
 	tools.PrintResource(t, user)
 
diff --git a/internal/acceptance/openstack/identity/v3/credentials_test.go b/internal/acceptance/openstack/identity/v3/credentials_test.go
index ce0362ab..9590f41b 100644
--- a/internal/acceptance/openstack/identity/v3/credentials_test.go
+++ b/internal/acceptance/openstack/identity/v3/credentials_test.go
@@ -39,11 +39,11 @@ func TestCredentialsCRUD(t *testing.T) {
 	th.AssertNoErr(t, err)
 	tools.PrintResource(t, token)
 
-	user, err := tokens.Get(context.TODO(), client, token.ID).ExtractUser()
+	user, err := tokens.Get(context.TODO(), client, token.ID, nil).ExtractUser()
 	th.AssertNoErr(t, err)
 	tools.PrintResource(t, user)
 
-	project, err := tokens.Get(context.TODO(), client, token.ID).ExtractProject()
+	project, err := tokens.Get(context.TODO(), client, token.ID, nil).ExtractProject()
 	th.AssertNoErr(t, err)
 	tools.PrintResource(t, project)
 
@@ -116,11 +116,11 @@ func TestCredentialsValidateS3(t *testing.T) {
 	th.AssertNoErr(t, err)
 	tools.PrintResource(t, token)
 
-	user, err := tokens.Get(context.TODO(), client, token.ID).ExtractUser()
+	user, err := tokens.Get(context.TODO(), client, token.ID, nil).ExtractUser()
 	th.AssertNoErr(t, err)
 	tools.PrintResource(t, user)
 
-	project, err := tokens.Get(context.TODO(), client, token.ID).ExtractProject()
+	project, err := tokens.Get(context.TODO(), client, token.ID, nil).ExtractProject()
 	th.AssertNoErr(t, err)
 	tools.PrintResource(t, project)
 
diff --git a/internal/acceptance/openstack/identity/v3/oauth1_test.go b/internal/acceptance/openstack/identity/v3/oauth1_test.go
index 2e5b9b25..7a2ef800 100644
--- a/internal/acceptance/openstack/identity/v3/oauth1_test.go
+++ b/internal/acceptance/openstack/identity/v3/oauth1_test.go
@@ -215,7 +215,7 @@ func oauth1MethodTest(t *testing.T, client *gophercloud.ServiceClient, consumer
 		tokens.Token
 		oauth1.TokenExt
 	}
-	tokenRes := tokens.Get(context.TODO(), newClient, newClient.TokenID)
+	tokenRes := tokens.Get(context.TODO(), newClient, newClient.TokenID, nil)
 	err = tokenRes.ExtractInto(&token)
 	th.AssertNoErr(t, err)
 	oauth1Roles, err := tokenRes.ExtractRoles()
diff --git a/internal/acceptance/openstack/identity/v3/token_test.go b/internal/acceptance/openstack/identity/v3/token_test.go
index 1bc436ca..290bce94 100644
--- a/internal/acceptance/openstack/identity/v3/token_test.go
+++ b/internal/acceptance/openstack/identity/v3/token_test.go
@@ -40,19 +40,19 @@ func TestTokensGet(t *testing.T) {
 	th.AssertNoErr(t, err)
 	tools.PrintResource(t, token)
 
-	catalog, err := tokens.Get(context.TODO(), client, token.ID).ExtractServiceCatalog()
+	catalog, err := tokens.Get(context.TODO(), client, token.ID, nil).ExtractServiceCatalog()
 	th.AssertNoErr(t, err)
 	tools.PrintResource(t, catalog)
 
-	user, err := tokens.Get(context.TODO(), client, token.ID).ExtractUser()
+	user, err := tokens.Get(context.TODO(), client, token.ID, nil).ExtractUser()
 	th.AssertNoErr(t, err)
 	tools.PrintResource(t, user)
 
-	roles, err := tokens.Get(context.TODO(), client, token.ID).ExtractRoles()
+	roles, err := tokens.Get(context.TODO(), client, token.ID, nil).ExtractRoles()
 	th.AssertNoErr(t, err)
 	tools.PrintResource(t, roles)
 
-	project, err := tokens.Get(context.TODO(), client, token.ID).ExtractProject()
+	project, err := tokens.Get(context.TODO(), client, token.ID, nil).ExtractProject()
 	th.AssertNoErr(t, err)
 	tools.PrintResource(t, project)
 }
diff --git a/internal/acceptance/openstack/identity/v3/trusts_test.go b/internal/acceptance/openstack/identity/v3/trusts_test.go
index 2eb659d5..225e09d4 100644
--- a/internal/acceptance/openstack/identity/v3/trusts_test.go
+++ b/internal/acceptance/openstack/identity/v3/trusts_test.go
@@ -43,7 +43,7 @@ func TestTrustCRUD(t *testing.T) {
 
 	token, err := tokens.Create(context.TODO(), client, &authOptions).Extract()
 	th.AssertNoErr(t, err)
-	adminUser, err := tokens.Get(context.TODO(), client, token.ID).ExtractUser()
+	adminUser, err := tokens.Get(context.TODO(), client, token.ID, nil).ExtractUser()
 	th.AssertNoErr(t, err)
 
 	// Get the admin and member role IDs.
diff --git a/openstack/identity/v3/tokens/testing/fixtures.go b/openstack/identity/v3/tokens/testing/fixtures.go
index 00797867..f0580521 100644
--- a/openstack/identity/v3/tokens/testing/fixtures.go
+++ b/openstack/identity/v3/tokens/testing/fixtures.go
@@ -323,3 +323,73 @@ func getGetDomainResult(t *testing.T) tokens.GetResult {
 	th.AssertNoErr(t, err)
 	return result
 }
+
+// ApplicationCredentialTokenOutput is a sample response to a Token call
+// using application credentials with access rules.
+const ApplicationCredentialTokenOutput = `
+{
+   "token":{
+      "methods":["application_credential"],
+      "expires_at":"2017-06-03T02:19:49.000000Z",
+      "user":{
+         "domain":{
+            "id":"default",
+            "name":"Default"
+         },
+         "id":"0fe36e73809d46aeae6705c39077b1b3",
+         "name":"admin"
+      },
+      "application_credential":{
+         "id":"d832f05beee743c696672ef65ee073ff",
+         "name":"test",
+         "restricted":true,
+         "access_rules":[
+            {
+               "id":"4641f614301d48bfae1ad323e3e1c44c",
+               "service":"identity",
+               "path":"/v3/auth/tokens",
+               "method":"GET"
+            },
+            {
+               "id":"968c404cb9b44672a574e5ee9d3db987",
+               "service":"identity",
+               "path":"/v3/**",
+               "method":"HEAD"
+            }
+         ]
+      },
+      "issued_at":"2017-06-03T01:19:49.000000Z"
+   }
+}`
+
+// ExpectedApplicationCredential contains expected application credential
+// extracted from token response.
+var ExpectedApplicationCredential = tokens.ApplicationCredential{
+	ID:         "d832f05beee743c696672ef65ee073ff",
+	Name:       "test",
+	Restricted: true,
+	AccessRules: []tokens.AccessRule{
+		{
+			ID:      "4641f614301d48bfae1ad323e3e1c44c",
+			Service: "identity",
+			Path:    "/v3/auth/tokens",
+			Method:  "GET",
+		},
+		{
+			ID:      "968c404cb9b44672a574e5ee9d3db987",
+			Service: "identity",
+			Path:    "/v3/**",
+			Method:  "HEAD",
+		},
+	},
+}
+
+func getGetApplicationCredentialResult(t *testing.T) tokens.GetResult {
+	result := tokens.GetResult{}
+	result.Header = http.Header{
+		"X-Subject-Token": []string{testTokenID},
+	}
+	err := json.Unmarshal([]byte(ApplicationCredentialTokenOutput), &result.Body)
+	th.AssertNoErr(t, err)
+	return result
+}
diff --git a/openstack/identity/v3/tokens/testing/requests_test.go b/openstack/identity/v3/tokens/testing/requests_test.go
index 8157de3c..c8c5d498 100644
--- a/openstack/identity/v3/tokens/testing/requests_test.go
+++ b/openstack/identity/v3/tokens/testing/requests_test.go
@@ -675,7 +675,7 @@ func TestGetRequest(t *testing.T) {
 		`)
 	})
 
-	token, err := tokens.Get(context.TODO(), &client, "abcdef12345").Extract()
+	token, err := tokens.Get(context.TODO(), &client, "abcdef12345", nil).Extract()
 	if err != nil {
 		t.Errorf("Info returned an error: %v", err)
 	}
@@ -712,7 +712,7 @@ func TestValidateRequestSuccessful(t *testing.T) {
 	defer fakeServer.Teardown()
 	client := prepareAuthTokenHandler(t, fakeServer, "HEAD", http.StatusNoContent)
 
-	ok, err := tokens.Validate(context.TODO(), &client, "abcdef12345")
+	ok, err := tokens.Validate(context.TODO(), &client, "abcdef12345", nil)
 	if err != nil {
 		t.Errorf("Unexpected error from Validate: %v", err)
 	}
@@ -727,7 +727,7 @@ func TestValidateRequestFailure(t *testing.T) {
 	defer fakeServer.Teardown()
 	client := prepareAuthTokenHandler(t, fakeServer, "HEAD", http.StatusNotFound)
 
-	ok, err := tokens.Validate(context.TODO(), &client, "abcdef12345")
+	ok, err := tokens.Validate(context.TODO(), &client, "abcdef12345", nil)
 	if err != nil {
 		t.Errorf("Unexpected error from Validate: %v", err)
 	}
@@ -742,7 +742,7 @@ func TestValidateRequestError(t *testing.T) {
 	defer fakeServer.Teardown()
 	client := prepareAuthTokenHandler(t, fakeServer, "HEAD", http.StatusMethodNotAllowed)
 
-	_, err := tokens.Validate(context.TODO(), &client, "abcdef12345")
+	_, err := tokens.Validate(context.TODO(), &client, "abcdef12345", nil)
 	if err == nil {
 		t.Errorf("Missing expected error from Validate")
 	}
@@ -768,6 +768,76 @@ func TestRevokeRequestError(t *testing.T) {
 	}
 }
 
+func TestGetRequestWithAccessRules(t *testing.T) {
+	fakeServer := th.SetupHTTP()
+	defer fakeServer.Teardown()
+
+	client := gophercloud.ServiceClient{
+		ProviderClient: &gophercloud.ProviderClient{
+			TokenID: "12345abcdef",
+		},
+		Endpoint: fakeServer.Endpoint(),
+	}
+
+	fakeServer.Mux.HandleFunc("/auth/tokens", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", "12345abcdef")
+		th.TestHeader(t, r, "X-Subject-Token", "abcdef12345")
+		th.TestHeader(t, r, "OpenStack-Identity-Access-Rules", "1")
+
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprint(w, `
+			{ "token": { "expires_at": "2014-08-29T13:10:01.000000Z" } }
+		`)
+	})
+
+	getOpts := tokens.GetOpts{
+		AccessRulesVersion: "1",
+	}
+	token, err := tokens.Get(context.TODO(), &client, "abcdef12345", getOpts).Extract()
+	if err != nil {
+		t.Errorf("Get returned an error: %v", err)
+	}
+
+	expected, _ := time.Parse(time.UnixDate, "Fri Aug 29 13:10:01 UTC 2014")
+	if token.ExpiresAt != expected {
+		t.Errorf("Expected expiration time %s, but was %s", expected.Format(time.UnixDate), time.Time(token.ExpiresAt).Format(time.UnixDate))
+	}
+}
+
+func TestValidateRequestWithAccessRules(t *testing.T) {
+	fakeServer := th.SetupHTTP()
+	defer fakeServer.Teardown()
+
+	client := gophercloud.ServiceClient{
+		ProviderClient: &gophercloud.ProviderClient{
+			TokenID: "12345abcdef",
+		},
+		Endpoint: fakeServer.Endpoint(),
+	}
+
+	fakeServer.Mux.HandleFunc("/auth/tokens", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "HEAD")
+		th.TestHeader(t, r, "X-Auth-Token", "12345abcdef")
+		th.TestHeader(t, r, "X-Subject-Token", "abcdef12345")
+		th.TestHeader(t, r, "OpenStack-Identity-Access-Rules", "1")
+
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	validateOpts := tokens.ValidateOpts{
+		AccessRulesVersion: "1",
+	}
+	ok, err := tokens.Validate(context.TODO(), &client, "abcdef12345", validateOpts)
+	if err != nil {
+		t.Errorf("Unexpected error from Validate: %v", err)
+	}
+
+	if !ok {
+		t.Errorf("Validate returned false for a valid token")
+	}
+}
+
 func TestNoTokenInResponse(t *testing.T) {
 	fakeServer := th.SetupHTTP()
 	defer fakeServer.Teardown()
diff --git a/openstack/identity/v3/tokens/testing/results_test.go b/openstack/identity/v3/tokens/testing/results_test.go
index c45581ff..f5e4029c 100644
--- a/openstack/identity/v3/tokens/testing/results_test.go
+++ b/openstack/identity/v3/tokens/testing/results_test.go
@@ -59,3 +59,23 @@ func TestExtractDomain(t *testing.T) {
 
 	th.CheckDeepEquals(t, &ExpectedDomain, domain)
 }
+
+func TestExtractApplicationCredential(t *testing.T) {
+	result := getGetApplicationCredentialResult(t)
+
+	appCred, err := result.ExtractApplicationCredential()
+	th.AssertNoErr(t, err)
+
+	th.CheckDeepEquals(t, &ExpectedApplicationCredential, appCred)
+}
+
+func TestExtractApplicationCredentialNotPresent(t *testing.T) {
+	result := getGetResult(t)
+
+	appCred, err := result.ExtractApplicationCredential()
+	th.AssertNoErr(t, err)
+
+	if appCred != nil {
+		t.Errorf("Expected nil application credential, got %v", appCred)
+	}
+}