Changes for AZURE_CLIENT_SEND_CERTIFICATE_CHAIN (#16851)

* add support for AZURE_CLIENT_SEND_CERTIFICATE_CHAIN
This commit is contained in:
Christopher Scott 2022-01-24 12:48:02 -06:00 коммит произвёл GitHub
Родитель fa22cece71
Коммит 583ac9d44d
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
7 изменённых файлов: 189 добавлений и 3 удалений

Просмотреть файл

@ -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) {