Changes for AZURE_CLIENT_SEND_CERTIFICATE_CHAIN (#16851)
* add support for AZURE_CLIENT_SEND_CERTIFICATE_CHAIN
This commit is contained in:
Родитель
fa22cece71
Коммит
583ac9d44d
|
@ -6,11 +6,16 @@ package azidentity
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/internal/mock"
|
||||
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
|
||||
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/public"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
)
|
||||
|
||||
// constants used throughout this package
|
||||
|
@ -23,10 +28,97 @@ const (
|
|||
|
||||
// constants for this file
|
||||
const (
|
||||
envHostString = "https://mock.com/"
|
||||
customHostString = "https://custommock.com/"
|
||||
envHostString = "https://mock.com/"
|
||||
customHostString = "https://custommock.com/"
|
||||
tenantDiscoveryResponse = `{
|
||||
"token_endpoint": "https://login.microsoftonline.com/3c631bb7-a9f7-4343-a5ba-a6159135f1fc/oauth2/v2.0/token",
|
||||
"token_endpoint_auth_methods_supported": [
|
||||
"client_secret_post",
|
||||
"private_key_jwt",
|
||||
"client_secret_basic"
|
||||
],
|
||||
"jwks_uri": "https://login.microsoftonline.com/3c631bb7-a9f7-4343-a5ba-a6159135f1fc/discovery/v2.0/keys",
|
||||
"response_modes_supported": [
|
||||
"query",
|
||||
"fragment",
|
||||
"form_post"
|
||||
],
|
||||
"subject_types_supported": [
|
||||
"pairwise"
|
||||
],
|
||||
"id_token_signing_alg_values_supported": [
|
||||
"RS256"
|
||||
],
|
||||
"response_types_supported": [
|
||||
"code",
|
||||
"id_token",
|
||||
"code id_token",
|
||||
"id_token token"
|
||||
],
|
||||
"scopes_supported": [
|
||||
"openid",
|
||||
"profile",
|
||||
"email",
|
||||
"offline_access"
|
||||
],
|
||||
"issuer": "https://login.microsoftonline.com/3c631bb7-a9f7-4343-a5ba-a6159135f1fc/v2.0",
|
||||
"request_uri_parameter_supported": false,
|
||||
"userinfo_endpoint": "https://graph.microsoft.com/oidc/userinfo",
|
||||
"authorization_endpoint": "https://login.microsoftonline.com/3c631bb7-a9f7-4343-a5ba-a6159135f1fc/oauth2/v2.0/authorize",
|
||||
"device_authorization_endpoint": "https://login.microsoftonline.com/3c631bb7-a9f7-4343-a5ba-a6159135f1fc/oauth2/v2.0/devicecode",
|
||||
"http_logout_supported": true,
|
||||
"frontchannel_logout_supported": true,
|
||||
"end_session_endpoint": "https://login.microsoftonline.com/3c631bb7-a9f7-4343-a5ba-a6159135f1fc/oauth2/v2.0/logout",
|
||||
"claims_supported": [
|
||||
"sub",
|
||||
"iss",
|
||||
"cloud_instance_name",
|
||||
"cloud_instance_host_name",
|
||||
"cloud_graph_host_name",
|
||||
"msgraph_host",
|
||||
"aud",
|
||||
"exp",
|
||||
"iat",
|
||||
"auth_time",
|
||||
"acr",
|
||||
"nonce",
|
||||
"preferred_username",
|
||||
"name",
|
||||
"tid",
|
||||
"ver",
|
||||
"at_hash",
|
||||
"c_hash",
|
||||
"email"
|
||||
],
|
||||
"kerberos_endpoint": "https://login.microsoftonline.com/3c631bb7-a9f7-4343-a5ba-a6159135f1fc/kerberos",
|
||||
"tenant_region_scope": "NA",
|
||||
"cloud_instance_name": "microsoftonline.com",
|
||||
"cloud_graph_host_name": "graph.windows.net",
|
||||
"msgraph_host": "graph.microsoft.com",
|
||||
"rbac_url": "https://pas.windows.net"
|
||||
}`
|
||||
)
|
||||
|
||||
func validateJWTRequestContainsHeader(t *testing.T, headerName string) mock.ResponsePredicate {
|
||||
return func(req *http.Request) bool {
|
||||
body, err := ioutil.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
t.Fatal("Expected a request with the JWT in the body.")
|
||||
}
|
||||
bodystr := string(body)
|
||||
kvps := strings.Split(bodystr, "&")
|
||||
assertion := strings.Split(kvps[0], "=")
|
||||
token, _ := jwt.Parse(assertion[1], nil)
|
||||
if token == nil {
|
||||
t.Fatalf("Failed to parse the JWT token: %s.", assertion[1])
|
||||
}
|
||||
if _, ok := token.Header[headerName]; !ok {
|
||||
t.Fatalf("JWT did not contain the %s header", headerName)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Set environment variables for the duration of a test. Restore their prior values
|
||||
// after the test completes. Obviated by 1.17's T.Setenv
|
||||
func setEnvironmentVariables(t *testing.T, vars map[string]string) {
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/internal/mock"
|
||||
)
|
||||
|
@ -82,6 +83,29 @@ func TestClientCertificateCredential_GetTokenSuccess_withCertificateChain(t *tes
|
|||
}
|
||||
}
|
||||
|
||||
func TestClientCertificateCredential_GetTokenSuccess_withCertificateChain_mock(t *testing.T) {
|
||||
test := allCertTests[0]
|
||||
srv, close := mock.NewServer(mock.WithTransformAllRequestsToTestServerUrl())
|
||||
defer close()
|
||||
srv.AppendResponse()
|
||||
srv.AppendResponse(mock.WithBody([]byte(tenantDiscoveryResponse)))
|
||||
srv.AppendResponse(mock.WithPredicate(validateJWTRequestContainsHeader(t, "x5c")), mock.WithBody([]byte(accessTokenRespSuccess)))
|
||||
srv.AppendResponse()
|
||||
|
||||
options := ClientCertificateCredentialOptions{ClientOptions: azcore.ClientOptions{Transport: srv}, SendCertificateChain: true}
|
||||
cred, err := NewClientCertificateCredential(fakeTenantID, fakeClientID, test.certs, test.key, &options)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tk, err := cred.GetToken(context.Background(), policy.TokenRequestOptions{Scopes: []string{liveTestScope}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if tk.Token != tokenValue {
|
||||
t.Fatalf("unexpected token: %s", tk.Token)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientCertificateCredential_GetTokenCheckPrivateKeyBlocks(t *testing.T) {
|
||||
test := allCertTests[0]
|
||||
cred, err := NewClientCertificateCredential(fakeTenantID, fakeClientID, test.certs, test.key, nil)
|
||||
|
|
|
@ -8,12 +8,15 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/internal/log"
|
||||
)
|
||||
|
||||
const envVarSendCertChain = "AZURE_CLIENT_SEND_CERTIFICATE_CHAIN"
|
||||
|
||||
// EnvironmentCredentialOptions contains optional parameters for EnvironmentCredential
|
||||
type EnvironmentCredentialOptions struct {
|
||||
azcore.ClientOptions
|
||||
|
@ -81,6 +84,9 @@ func NewEnvironmentCredential(options *EnvironmentCredentialOptions) (*Environme
|
|||
return nil, fmt.Errorf(`failed to load certificate from "%s": %v`, certPath, err)
|
||||
}
|
||||
o := &ClientCertificateCredentialOptions{AuthorityHost: options.AuthorityHost, ClientOptions: options.ClientOptions}
|
||||
if v, ok := os.LookupEnv(envVarSendCertChain); ok {
|
||||
o.SendCertificateChain = v == "1" || strings.ToLower(v) == "true"
|
||||
}
|
||||
cred, err := NewClientCertificateCredential(tenantID, clientID, certs, key, o)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -9,7 +9,9 @@ import (
|
|||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/internal/mock"
|
||||
)
|
||||
|
||||
func initEnvironmentVarsForTest() error {
|
||||
|
@ -173,6 +175,35 @@ func TestEnvironmentCredential_UsernamePasswordSet(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestEnvironmentCredential_SendCertificateChain(t *testing.T) {
|
||||
resetEnvironmentVarsForTest()
|
||||
srv, close := mock.NewServer(mock.WithTransformAllRequestsToTestServerUrl())
|
||||
defer close()
|
||||
srv.AppendResponse()
|
||||
srv.AppendResponse(mock.WithBody([]byte(tenantDiscoveryResponse)))
|
||||
srv.AppendResponse(mock.WithPredicate(validateJWTRequestContainsHeader(t, "x5c")), mock.WithBody([]byte(accessTokenRespSuccess)))
|
||||
srv.AppendResponse()
|
||||
|
||||
vars := map[string]string{
|
||||
"AZURE_CLIENT_ID": liveSP.clientID,
|
||||
"AZURE_CLIENT_CERTIFICATE_PATH": liveSP.pfxPath,
|
||||
"AZURE_TENANT_ID": liveSP.tenantID,
|
||||
envVarSendCertChain: "true",
|
||||
}
|
||||
setEnvironmentVariables(t, vars)
|
||||
cred, err := NewEnvironmentCredential(&EnvironmentCredentialOptions{ClientOptions: azcore.ClientOptions{Transport: srv}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tk, err := cred.GetToken(context.Background(), policy.TokenRequestOptions{Scopes: []string{liveTestScope}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if tk.Token != tokenValue {
|
||||
t.Fatalf("unexpected token: %s", tk.Token)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvironmentCredential_ClientSecretLive(t *testing.T) {
|
||||
vars := map[string]string{
|
||||
"AZURE_CLIENT_ID": liveSP.clientID,
|
||||
|
|
|
@ -7,9 +7,12 @@ require (
|
|||
github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.3
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.2.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f // indirect
|
||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
)
|
||||
|
||||
replace github.com/Azure/azure-sdk-for-go/sdk/internal => ../internal
|
||||
|
|
|
@ -11,6 +11,8 @@ github.com/dnaeon/go-vcr v1.1.0 h1:ReYa/UBrRyQdant9B4fNHGoCNKw6qh6P0fsdGmZpR7c=
|
|||
github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
|
||||
github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c=
|
||||
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU=
|
||||
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
|
|
|
@ -9,9 +9,11 @@ package mock
|
|||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
@ -38,6 +40,9 @@ type Server struct {
|
|||
|
||||
// count tracks the number of requests that have been made.
|
||||
count int
|
||||
|
||||
// determines whether all requests will be routed to the httptest Server by changing the Host of each request
|
||||
routeAllRequestsToMockServer bool
|
||||
}
|
||||
|
||||
func newServer() *Server {
|
||||
|
@ -123,7 +128,24 @@ func (s *Server) Do(req *http.Request) (*http.Response, error) {
|
|||
resp := s.getResponse()
|
||||
return nil, resp.err
|
||||
}
|
||||
resp, err := s.srv.Client().Do(req)
|
||||
var err error
|
||||
var resp *http.Response
|
||||
if s.routeAllRequestsToMockServer {
|
||||
var srvUrl *url.URL
|
||||
originalURL := req.URL
|
||||
mockUrl := *req.URL
|
||||
srvUrl, err = url.Parse(s.srv.URL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to parse the test server URL: %v", err)
|
||||
}
|
||||
mockUrl.Host = srvUrl.Host
|
||||
mockUrl.Scheme = srvUrl.Scheme
|
||||
req.URL = &mockUrl
|
||||
resp, err = s.srv.Client().Do(req)
|
||||
req.URL = originalURL
|
||||
} else {
|
||||
resp, err = s.srv.Client().Do(req)
|
||||
}
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
@ -225,6 +247,12 @@ func (fn fnSrvOpt) apply(s *Server) {
|
|||
fn(s)
|
||||
}
|
||||
|
||||
func WithTransformAllRequestsToTestServerUrl() ServerOption {
|
||||
return fnSrvOpt(func(s *Server) {
|
||||
s.routeAllRequestsToMockServer = true
|
||||
})
|
||||
}
|
||||
|
||||
// WithTLSConfig sets the given TLS config on server.
|
||||
func WithTLSConfig(cfg *tls.Config) ServerOption {
|
||||
return fnSrvOpt(func(s *Server) {
|
||||
|
|
Загрузка…
Ссылка в новой задаче