github.com/ydb-platform/ydb-go-sdk/v3@v3.89.2/internal/credentials/oauth2_test.go (about)

     1  package credentials
     2  
     3  import (
     4  	"context"
     5  	"encoding/base64"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"net/http/httptest"
    11  	"net/url"
    12  	"os"
    13  	"reflect"
    14  	"strconv"
    15  	"sync/atomic"
    16  	"testing"
    17  	"time"
    18  
    19  	"github.com/golang-jwt/jwt/v4"
    20  	"github.com/stretchr/testify/require"
    21  
    22  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/xtest"
    23  )
    24  
    25  // #nosec G101
    26  var (
    27  	testRSAPrivateKeyContent       = "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC75/JS3rMcLJxv\nFgpOzF5+2gH+Yig3RE2MTl9uwC0BZKAv6foYr7xywQyWIK+W1cBhz8R4LfFmZo2j\nM0aCvdRmNBdW0EDSTnHLxCsFhoQWLVq+bI5f5jzkcoiioUtaEpADPqwgVULVtN/n\nnPJiZ6/dU30C3jmR6+LUgEntUtWt3eq3xQIn5lG3zC1klBY/HxtfH5Hu8xBvwRQT\nJnh3UpPLj8XwSmriDgdrhR7o6umWyVuGrMKlLHmeivlfzjYtfzO1MOIMG8t2/zxG\nR+xb4Vwks73sH1KruH/0/JMXU97npwpe+Um+uXhpldPygGErEia7abyZB2gMpXqr\nWYKMo02NAgMBAAECggEAO0BpC5OYw/4XN/optu4/r91bupTGHKNHlsIR2rDzoBhU\nYLd1evpTQJY6O07EP5pYZx9mUwUdtU4KRJeDGO/1/WJYp7HUdtxwirHpZP0lQn77\nuccuX/QQaHLrPekBgz4ONk+5ZBqukAfQgM7fKYOLk41jgpeDbM2Ggb6QUSsJISEp\nzrwpI/nNT/wn+Hvx4DxrzWU6wF+P8kl77UwPYlTA7GsT+T7eKGVH8xsxmK8pt6lg\nsvlBA5XosWBWUCGLgcBkAY5e4ZWbkdd183o+oMo78id6C+PQPE66PLDtHWfpRRmN\nm6XC03x6NVhnfvfozoWnmS4+e4qj4F/emCHvn0GMywKBgQDLXlj7YPFVXxZpUvg/\nrheVcCTGbNmQJ+4cZXx87huqwqKgkmtOyeWsRc7zYInYgraDrtCuDBCfP//ZzOh0\nLxepYLTPk5eNn/GT+VVrqsy35Ccr60g7Lp/bzb1WxyhcLbo0KX7/6jl0lP+VKtdv\nmto+4mbSBXSM1Y5BVVoVgJ3T/wKBgQDsiSvPRzVi5TTj13x67PFymTMx3HCe2WzH\nJUyepCmVhTm482zW95pv6raDr5CTO6OYpHtc5sTTRhVYEZoEYFTM9Vw8faBtluWG\nBjkRh4cIpoIARMn74YZKj0C/0vdX7SHdyBOU3bgRPHg08Hwu3xReqT1kEPSI/B2V\n4pe5fVrucwKBgQCNFgUxUA3dJjyMES18MDDYUZaRug4tfiYouRdmLGIxUxozv6CG\nZnbZzwxFt+GpvPUV4f+P33rgoCvFU+yoPctyjE6j+0aW0DFucPmb2kBwCu5J/856\nkFwCx3blbwFHAco+SdN7g2kcwgmV2MTg/lMOcU7XwUUcN0Obe7UlWbckzQKBgQDQ\nnXaXHL24GGFaZe4y2JFmujmNy1dEsoye44W9ERpf9h1fwsoGmmCKPp90az5+rIXw\nFXl8CUgk8lXW08db/r4r+ma8Lyx0GzcZyplAnaB5/6j+pazjSxfO4KOBy4Y89Tb+\nTP0AOcCi6ws13bgY+sUTa/5qKA4UVw+c5zlb7nRpgwKBgGXAXhenFw1666482iiN\ncHSgwc4ZHa1oL6aNJR1XWH+aboBSwR+feKHUPeT4jHgzRGo/aCNHD2FE5I8eBv33\nof1kWYjAO0YdzeKrW0rTwfvt9gGg+CS397aWu4cy+mTI+MNfBgeDAIVBeJOJXLlX\nhL8bFAuNNVrCOp79TNnNIsh7\n-----END PRIVATE KEY-----\n"                             //nolint:lll
    28  	testRSAPrivateKeyJSONContent   = "-----BEGIN PRIVATE KEY-----\\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC75/JS3rMcLJxv\\nFgpOzF5+2gH+Yig3RE2MTl9uwC0BZKAv6foYr7xywQyWIK+W1cBhz8R4LfFmZo2j\\nM0aCvdRmNBdW0EDSTnHLxCsFhoQWLVq+bI5f5jzkcoiioUtaEpADPqwgVULVtN/n\\nnPJiZ6/dU30C3jmR6+LUgEntUtWt3eq3xQIn5lG3zC1klBY/HxtfH5Hu8xBvwRQT\\nJnh3UpPLj8XwSmriDgdrhR7o6umWyVuGrMKlLHmeivlfzjYtfzO1MOIMG8t2/zxG\\nR+xb4Vwks73sH1KruH/0/JMXU97npwpe+Um+uXhpldPygGErEia7abyZB2gMpXqr\\nWYKMo02NAgMBAAECggEAO0BpC5OYw/4XN/optu4/r91bupTGHKNHlsIR2rDzoBhU\\nYLd1evpTQJY6O07EP5pYZx9mUwUdtU4KRJeDGO/1/WJYp7HUdtxwirHpZP0lQn77\\nuccuX/QQaHLrPekBgz4ONk+5ZBqukAfQgM7fKYOLk41jgpeDbM2Ggb6QUSsJISEp\\nzrwpI/nNT/wn+Hvx4DxrzWU6wF+P8kl77UwPYlTA7GsT+T7eKGVH8xsxmK8pt6lg\\nsvlBA5XosWBWUCGLgcBkAY5e4ZWbkdd183o+oMo78id6C+PQPE66PLDtHWfpRRmN\\nm6XC03x6NVhnfvfozoWnmS4+e4qj4F/emCHvn0GMywKBgQDLXlj7YPFVXxZpUvg/\\nrheVcCTGbNmQJ+4cZXx87huqwqKgkmtOyeWsRc7zYInYgraDrtCuDBCfP//ZzOh0\\nLxepYLTPk5eNn/GT+VVrqsy35Ccr60g7Lp/bzb1WxyhcLbo0KX7/6jl0lP+VKtdv\\nmto+4mbSBXSM1Y5BVVoVgJ3T/wKBgQDsiSvPRzVi5TTj13x67PFymTMx3HCe2WzH\\nJUyepCmVhTm482zW95pv6raDr5CTO6OYpHtc5sTTRhVYEZoEYFTM9Vw8faBtluWG\\nBjkRh4cIpoIARMn74YZKj0C/0vdX7SHdyBOU3bgRPHg08Hwu3xReqT1kEPSI/B2V\\n4pe5fVrucwKBgQCNFgUxUA3dJjyMES18MDDYUZaRug4tfiYouRdmLGIxUxozv6CG\\nZnbZzwxFt+GpvPUV4f+P33rgoCvFU+yoPctyjE6j+0aW0DFucPmb2kBwCu5J/856\\nkFwCx3blbwFHAco+SdN7g2kcwgmV2MTg/lMOcU7XwUUcN0Obe7UlWbckzQKBgQDQ\\nnXaXHL24GGFaZe4y2JFmujmNy1dEsoye44W9ERpf9h1fwsoGmmCKPp90az5+rIXw\\nFXl8CUgk8lXW08db/r4r+ma8Lyx0GzcZyplAnaB5/6j+pazjSxfO4KOBy4Y89Tb+\\nTP0AOcCi6ws13bgY+sUTa/5qKA4UVw+c5zlb7nRpgwKBgGXAXhenFw1666482iiN\\ncHSgwc4ZHa1oL6aNJR1XWH+aboBSwR+feKHUPeT4jHgzRGo/aCNHD2FE5I8eBv33\\nof1kWYjAO0YdzeKrW0rTwfvt9gGg+CS397aWu4cy+mTI+MNfBgeDAIVBeJOJXLlX\\nhL8bFAuNNVrCOp79TNnNIsh7\\n-----END PRIVATE KEY-----\\n" //nolint:lll
    29  	testRSAPublicKeyContent        = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu+fyUt6zHCycbxYKTsxe\nftoB/mIoN0RNjE5fbsAtAWSgL+n6GK+8csEMliCvltXAYc/EeC3xZmaNozNGgr3U\nZjQXVtBA0k5xy8QrBYaEFi1avmyOX+Y85HKIoqFLWhKQAz6sIFVC1bTf55zyYmev\n3VN9At45kevi1IBJ7VLVrd3qt8UCJ+ZRt8wtZJQWPx8bXx+R7vMQb8EUEyZ4d1KT\ny4/F8Epq4g4Ha4Ue6OrplslbhqzCpSx5nor5X842LX8ztTDiDBvLdv88RkfsW+Fc\nJLO97B9Sq7h/9PyTF1Pe56cKXvlJvrl4aZXT8oBhKxImu2m8mQdoDKV6q1mCjKNN\njQIDAQAB\n-----END PUBLIC KEY-----\n"                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     //nolint:lll
    30  	testECPrivateKeyContent        = "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIB6fv25gf7P/7fkjW/2kcKICUhHeOygkFeUJ/ylyU3hloAoGCCqGSM49\nAwEHoUQDQgAEvkKy92hpLiT0GEpzFkYBEWWnkAGTTA6141H0oInA9X30eS0RObAa\nmVY8yD39NI7Nj03hBxEa4Z0tOhrq9cW8eg==\n-----END EC PRIVATE KEY-----\n"                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         //nolint:lll
    31  	testECPrivateKeyJSONContent    = "-----BEGIN EC PRIVATE KEY-----\\nMHcCAQEEIB6fv25gf7P/7fkjW/2kcKICUhHeOygkFeUJ/ylyU3hloAoGCCqGSM49\\nAwEHoUQDQgAEvkKy92hpLiT0GEpzFkYBEWWnkAGTTA6141H0oInA9X30eS0RObAa\\nmVY8yD39NI7Nj03hBxEa4Z0tOhrq9cW8eg==\\n-----END EC PRIVATE KEY-----\\n"                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    //nolint:lll
    32  	testECPublicKeyContent         = "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvkKy92hpLiT0GEpzFkYBEWWnkAGT\nTA6141H0oInA9X30eS0RObAamVY8yD39NI7Nj03hBxEa4Z0tOhrq9cW8eg==\n-----END PUBLIC KEY-----\n"                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           //nolint:lll
    33  	testHMACSecretKeyBase64Content = "VGhlIHdvcmxkIGhhcyBjaGFuZ2VkLgpJIHNlZSBpdCBpbiB0aGUgd2F0ZXIuCkkgZmVlbCBpdCBpbiB0aGUgRWFydGguCkkgc21lbGwgaXQgaW4gdGhlIGFpci4KTXVjaCB0aGF0IG9uY2Ugd2FzIGlzIGxvc3QsCkZvciBub25lIG5vdyBsaXZlIHdobyByZW1lbWJlciBpdC4K"                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 //nolint:lll
    34  )
    35  
    36  func WriteErr(w http.ResponseWriter, err error) {
    37  	WriteResponse(w, http.StatusInternalServerError, err.Error(), "text/html")
    38  }
    39  
    40  func WriteResponse(w http.ResponseWriter, code int, body string, bodyType string) {
    41  	w.Header().Add("Content-Type", bodyType)
    42  	w.Header().Add("Content-Length", strconv.Itoa(len(body)))
    43  	w.WriteHeader(code)
    44  	_, _ = w.Write([]byte(body))
    45  }
    46  
    47  func runTokenExchangeServer(
    48  	currentTestParams *Oauth2TokenExchangeTestParams,
    49  	firstReplyIsError bool,
    50  	serverRequests *atomic.Int64,
    51  ) *httptest.Server {
    52  	mux := http.NewServeMux()
    53  	returnedErr := !firstReplyIsError
    54  	returnedErrPtr := &returnedErr
    55  
    56  	mux.HandleFunc("/exchange", func(w http.ResponseWriter, r *http.Request) {
    57  		if serverRequests != nil {
    58  			serverRequests.Add(1)
    59  		}
    60  		body, err := io.ReadAll(r.Body)
    61  		if err != nil {
    62  			WriteErr(w, err)
    63  		}
    64  
    65  		params, err := url.ParseQuery(string(body))
    66  		if err != nil {
    67  			WriteErr(w, err)
    68  		}
    69  		expectedParams := url.Values{}
    70  		expectedParams.Set("scope", "test_scope1 test_scope2")
    71  		expectedParams.Set("audience", "test_audience")
    72  		expectedParams.Set("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange")
    73  		expectedParams.Set("requested_token_type", "urn:ietf:params:oauth:token-type:access_token")
    74  		expectedParams.Set("subject_token", "test_source_token")
    75  		expectedParams.Set("subject_token_type", "urn:ietf:params:oauth:token-type:test_jwt")
    76  
    77  		if !reflect.DeepEqual(expectedParams, params) {
    78  			WriteResponse(w, 555, fmt.Sprintf("Params are not as expected: \"%s\" != \"%s\"",
    79  				expectedParams.Encode(), body), "text/html") // error will be checked in test thread
    80  		} else {
    81  			if !*returnedErrPtr {
    82  				WriteResponse(w, http.StatusInternalServerError, "test error", "text/html")
    83  				*returnedErrPtr = true
    84  			} else {
    85  				WriteResponse(w, currentTestParams.Status, currentTestParams.Response, "application/json")
    86  			}
    87  		}
    88  	})
    89  
    90  	return httptest.NewServer(mux)
    91  }
    92  
    93  type Oauth2TokenExchangeTestParams struct {
    94  	Response          string
    95  	Status            int
    96  	ExpectedToken     string
    97  	ExpectedError     error
    98  	ExpectedErrorPart string
    99  }
   100  
   101  func TestOauth2TokenExchange(t *testing.T) {
   102  	ctx, cancel := context.WithCancel(xtest.Context(t))
   103  	defer cancel()
   104  
   105  	testsParams := []Oauth2TokenExchangeTestParams{
   106  		{
   107  			Response:      `{"access_token":"test_token","token_type":"BEARER","expires_in":42,"some_other_field":"x"}`,
   108  			Status:        http.StatusOK,
   109  			ExpectedToken: "Bearer test_token",
   110  		},
   111  		{
   112  			Response:      `aaa`,
   113  			Status:        http.StatusOK,
   114  			ExpectedToken: "",
   115  			ExpectedError: errCouldNotParseResponse,
   116  		},
   117  		{
   118  			Response:      `{}`,
   119  			Status:        http.StatusBadRequest,
   120  			ExpectedToken: "",
   121  			ExpectedError: errCouldNotExchangeToken,
   122  		},
   123  		{
   124  			Response:      `not json`,
   125  			Status:        http.StatusNotFound,
   126  			ExpectedToken: "",
   127  			ExpectedError: errCouldNotExchangeToken,
   128  		},
   129  		{
   130  			Response:          `{"error": "invalid_request"}`,
   131  			Status:            http.StatusBadRequest,
   132  			ExpectedToken:     "",
   133  			ExpectedError:     errCouldNotExchangeToken,
   134  			ExpectedErrorPart: "400 Bad Request, error: invalid_request",
   135  		},
   136  		{
   137  			Response:          `{"error":"unauthorized_client","error_description":"something went bad"}`,
   138  			Status:            http.StatusInternalServerError,
   139  			ExpectedToken:     "",
   140  			ExpectedError:     errCouldNotExchangeToken,
   141  			ExpectedErrorPart: "500 Internal Server Error, error: unauthorized_client, description: \\\\\\\"something went bad\\\\\\\"", //nolint:lll
   142  		},
   143  		{
   144  			Response:          `{"error_description":"something went bad","error_uri":"my_error_uri"}`,
   145  			Status:            http.StatusForbidden,
   146  			ExpectedToken:     "",
   147  			ExpectedError:     errCouldNotExchangeToken,
   148  			ExpectedErrorPart: "403 Forbidden, description: \\\"something went bad\\\", error_uri: my_error_uri",
   149  		},
   150  		{
   151  			Response:      `{"access_token":"test_token","token_type":"","expires_in":42,"some_other_field":"x"}`,
   152  			Status:        http.StatusOK,
   153  			ExpectedToken: "",
   154  			ExpectedError: errUnsupportedTokenType,
   155  		},
   156  		{
   157  			Response:      `{"access_token":"test_token","token_type":"basic","expires_in":42,"some_other_field":"x"}`,
   158  			Status:        http.StatusOK,
   159  			ExpectedToken: "",
   160  			ExpectedError: errUnsupportedTokenType,
   161  		},
   162  		{
   163  			Response:      `{"access_token":"test_token","token_type":"Bearer","expires_in":-42,"some_other_field":"x"}`,
   164  			Status:        http.StatusOK,
   165  			ExpectedToken: "",
   166  			ExpectedError: errIncorrectExpirationTime,
   167  		},
   168  		{
   169  			Response:          `{"access_token":"test_token","token_type":"Bearer","expires_in":42,"scope":"s"}`,
   170  			Status:            http.StatusOK,
   171  			ExpectedToken:     "",
   172  			ExpectedError:     errDifferentScope,
   173  			ExpectedErrorPart: "Expected \\\"test_scope1 test_scope2\\\", but got \\\"s\\\"",
   174  		},
   175  		{
   176  			Response:      `{"access_token":"","token_type":"Bearer","expires_in":42}`,
   177  			Status:        http.StatusOK,
   178  			ExpectedToken: "",
   179  			ExpectedError: errEmptyAccessToken,
   180  		},
   181  		{
   182  			Response:      `{"token_type":"Bearer","expires_in":42}`,
   183  			Status:        http.StatusOK,
   184  			ExpectedToken: "",
   185  			ExpectedError: errEmptyAccessToken,
   186  		},
   187  	}
   188  
   189  	for _, params := range testsParams {
   190  		t.Run("", func(t *testing.T) {
   191  			xtest.TestManyTimes(t, func(t testing.TB) {
   192  				var currentTestParams Oauth2TokenExchangeTestParams
   193  				server := runTokenExchangeServer(&currentTestParams, true, nil)
   194  				defer server.Close()
   195  
   196  				currentTestParams = params
   197  
   198  				client, err := NewOauth2TokenExchangeCredentials(
   199  					WithTokenEndpoint(server.URL+"/exchange"),
   200  					WithAudience("test_audience"),
   201  					WithScope("test_scope1", "test_scope2"),
   202  					WithSubjectToken(NewFixedTokenSource("test_source_token", "urn:ietf:params:oauth:token-type:test_jwt")),
   203  					WithSyncExchangeTimeout(time.Second*3),
   204  				)
   205  				require.NoError(t, err)
   206  
   207  				token, err := client.Token(ctx)
   208  				if params.ExpectedErrorPart == "" && params.ExpectedError == nil { //nolint:nestif
   209  					require.NoError(t, err)
   210  				} else {
   211  					if !errors.Is(err, context.DeadlineExceeded) {
   212  						if params.ExpectedErrorPart != "" {
   213  							require.ErrorContains(t, err, params.ExpectedErrorPart)
   214  						}
   215  						if params.ExpectedError != nil {
   216  							require.ErrorIs(t, err, params.ExpectedError)
   217  						}
   218  					}
   219  				}
   220  				require.Equal(t, params.ExpectedToken, token)
   221  			}, xtest.StopAfter(5+time.Second))
   222  		})
   223  	}
   224  }
   225  
   226  func TestOauth2TokenUpdate(t *testing.T) {
   227  	ctx, cancel := context.WithCancel(xtest.Context(t))
   228  	defer cancel()
   229  
   230  	xtest.TestManyTimes(t, func(t testing.TB) {
   231  		var currentTestParams Oauth2TokenExchangeTestParams
   232  		server := runTokenExchangeServer(&currentTestParams, true, nil)
   233  		defer server.Close()
   234  
   235  		// First exchange
   236  		currentTestParams = Oauth2TokenExchangeTestParams{
   237  			Response: `{"access_token":"test_token_1", "token_type":"Bearer","expires_in":2}`,
   238  			Status:   http.StatusOK,
   239  		}
   240  
   241  		client, err := NewOauth2TokenExchangeCredentials(
   242  			WithTokenEndpoint(server.URL+"/exchange"),
   243  			WithAudience("test_audience"),
   244  			WithScope("test_scope1", "test_scope2"),
   245  			WithFixedSubjectToken("test_source_token", "urn:ietf:params:oauth:token-type:test_jwt"),
   246  		)
   247  		require.NoError(t, err)
   248  
   249  		token, err := client.Token(ctx)
   250  		t1 := time.Now()
   251  		require.NoError(t, err)
   252  		require.Equal(t, "Bearer test_token_1", token)
   253  
   254  		// Second exchange
   255  		currentTestParams = Oauth2TokenExchangeTestParams{
   256  			Response: `{"access_token":"test_token_2", "token_type":"Bearer","expires_in":10000}`,
   257  			Status:   http.StatusOK,
   258  		}
   259  
   260  		token, err = client.Token(ctx)
   261  		t2 := time.Now()
   262  		require.NoError(t, err)
   263  		if t2.Sub(t1) <= time.Second { // half expire period => no attempts to update
   264  			require.Equal(t, "Bearer test_token_1", token)
   265  		}
   266  
   267  		time.Sleep(time.Second) // wait half expire period
   268  		for i := 1; i <= 100; i++ {
   269  			t3 := time.Now()
   270  			token, err = client.Token(ctx)
   271  			require.NoError(t, err)
   272  			if t3.Sub(t1) >= 2*time.Second {
   273  				require.Equal(t, "Bearer test_token_2", token) // Must update at least sync
   274  			}
   275  			if token == "Bearer test_token_2" { // already updated
   276  				break
   277  			}
   278  			require.Equal(t, "Bearer test_token_1", token)
   279  
   280  			time.Sleep(10 * time.Millisecond)
   281  		}
   282  
   283  		// Third exchange (never got, because token will be expired later)
   284  		currentTestParams = Oauth2TokenExchangeTestParams{
   285  			Response: `{}`,
   286  			Status:   http.StatusInternalServerError,
   287  		}
   288  
   289  		for i := 1; i <= 5; i++ {
   290  			token, err = client.Token(ctx)
   291  			require.NoError(t, err)
   292  			require.Equal(t, "Bearer test_token_2", token)
   293  		}
   294  	}, xtest.StopAfter(14*time.Second))
   295  }
   296  
   297  func TestReturnsOldTokenWhileUpdating(t *testing.T) {
   298  	ctx, cancel := context.WithCancel(xtest.Context(t))
   299  	defer cancel()
   300  
   301  	var currentTestParams Oauth2TokenExchangeTestParams
   302  	var serverRequests atomic.Int64
   303  	server := runTokenExchangeServer(&currentTestParams, true, &serverRequests)
   304  	defer server.Close()
   305  
   306  	// First exchange
   307  	currentTestParams = Oauth2TokenExchangeTestParams{
   308  		Response: `{"access_token":"test_token_1", "token_type":"Bearer","expires_in":6}`,
   309  		Status:   http.StatusOK,
   310  	}
   311  
   312  	client, err := NewOauth2TokenExchangeCredentials(
   313  		WithTokenEndpoint(server.URL+"/exchange"),
   314  		WithAudience("test_audience"),
   315  		WithScope("test_scope1", "test_scope2"),
   316  		WithFixedSubjectToken("test_source_token", "urn:ietf:params:oauth:token-type:test_jwt"),
   317  	)
   318  	require.NoError(t, err)
   319  
   320  	token, err := client.Token(ctx)
   321  	t1 := time.Now()
   322  	require.NoError(t, err)
   323  	require.Equal(t, "Bearer test_token_1", token)
   324  
   325  	// Second exchange
   326  	currentTestParams = Oauth2TokenExchangeTestParams{
   327  		Response: `{"error":"unauthorized_client","error_description":"something went bad"}`,
   328  		Status:   http.StatusInternalServerError,
   329  	}
   330  
   331  	token, err = client.Token(ctx)
   332  	t2 := time.Now()
   333  	require.NoError(t, err)
   334  	if t2.Sub(t1) <= time.Second*3 {
   335  		require.Equal(t, "Bearer test_token_1", token)
   336  		require.Equal(t, int64(2), serverRequests.Load())
   337  	}
   338  
   339  	time.Sleep(time.Second * 3) // wait half expire period
   340  	for i := 1; i <= 100; i++ {
   341  		token, err = client.Token(ctx)
   342  		t3 := time.Now()
   343  		if t3.Sub(t1) < 6*time.Second {
   344  			require.NoError(t, err)
   345  			require.Equal(t, "Bearer test_token_1", token)
   346  			time.Sleep(10 * time.Millisecond)
   347  		} else {
   348  			break
   349  		}
   350  	}
   351  	require.Greater(t, serverRequests.Load(), int64(3)) // at least one retry
   352  }
   353  
   354  func TestWrongParameters(t *testing.T) {
   355  	_, err := NewOauth2TokenExchangeCredentials(
   356  		// No endpoint
   357  		WithFixedActorToken("test_source_token", "urn:ietf:params:oauth:token-type:test_jwt"),
   358  		WithRequestedTokenType("access_token"),
   359  	)
   360  	require.ErrorIs(t, err, errEmptyTokenEndpointError)
   361  }
   362  
   363  type errorTokenSource struct{}
   364  
   365  var errTokenSource = errors.New("test error")
   366  
   367  func (s *errorTokenSource) Token() (Token, error) {
   368  	return Token{"", ""}, errTokenSource
   369  }
   370  
   371  func TestErrorInSourceToken(t *testing.T) {
   372  	// Create
   373  	_, err := NewOauth2TokenExchangeCredentials(
   374  		WithTokenEndpoint("http:trololo"),
   375  		WithJWTSubjectToken(
   376  			WithRSAPrivateKeyPEMContent([]byte("invalid")),
   377  			WithKeyID("key_id"),
   378  			WithSigningMethod(jwt.SigningMethodRS256),
   379  			WithIssuer("test_issuer"),
   380  			WithAudience("test_audience"),
   381  		),
   382  	)
   383  	require.ErrorIs(t, err, errCouldNotCreateTokenSource)
   384  
   385  	_, err = NewOauth2TokenExchangeCredentials(
   386  		WithTokenEndpoint("http:trololo"),
   387  		WithJWTSubjectToken(
   388  			WithECPrivateKeyPEMContent([]byte("invalid")),
   389  			WithKeyID("key_id"),
   390  			WithSigningMethod(jwt.SigningMethodES512),
   391  			WithIssuer("test_issuer"),
   392  			WithAudience("test_audience"),
   393  		),
   394  	)
   395  	require.ErrorIs(t, err, errCouldNotCreateTokenSource)
   396  
   397  	_, err = NewOauth2TokenExchangeCredentials(
   398  		WithTokenEndpoint("http:trololo"),
   399  		WithJWTSubjectToken(
   400  			WithHMACSecretKeyBase64Content("<not base64>"),
   401  			WithKeyID("key_id"),
   402  			WithSigningMethod(jwt.SigningMethodHS384),
   403  			WithIssuer("test_issuer"),
   404  			WithAudience("test_audience"),
   405  		),
   406  	)
   407  	require.ErrorIs(t, err, errCouldNotCreateTokenSource)
   408  
   409  	_, err = NewOauth2TokenExchangeCredentials(
   410  		WithTokenEndpoint("http:trololo"),
   411  		WithJWTSubjectToken(
   412  			WithHMACSecretKeyBase64Content(testHMACSecretKeyBase64Content),
   413  			WithKeyID("key_id"),
   414  			WithSigningMethodName("unknown"),
   415  			WithIssuer("test_issuer"),
   416  			WithAudience("test_audience"),
   417  		),
   418  	)
   419  	require.ErrorIs(t, err, errUnsupportedSigningMethod)
   420  
   421  	// Use
   422  	client, err := NewOauth2TokenExchangeCredentials(
   423  		WithTokenEndpoint("http:trololo"),
   424  		WithGrantType("grant_type"),
   425  		WithRequestTimeout(time.Second),
   426  		WithResource("res", "res2"),
   427  		WithFixedSubjectToken("t", "tt"),
   428  		WithActorToken(&errorTokenSource{}),
   429  		WithSourceInfo("TestErrorInSourceToken"),
   430  	)
   431  	require.NoError(t, err)
   432  
   433  	// Check that token prints well
   434  	formatted := fmt.Sprint(client)
   435  	require.Equal(t, `OAuth2TokenExchange{Endpoint:"http:trololo",GrantType:grant_type,Resource:[res res2],Audience:[],Scope:[],RequestedTokenType:urn:ietf:params:oauth:token-type:access_token,SubjectToken:FixedTokenSource{Token:"****(CRC-32c: 856A5AA8)",Type:tt},ActorToken:&{},From:"TestErrorInSourceToken"}`, formatted) //nolint:lll
   436  
   437  	token, err := client.Token(context.Background())
   438  	require.ErrorIs(t, err, errTokenSource)
   439  	require.Equal(t, "", token)
   440  
   441  	client, err = NewOauth2TokenExchangeCredentials(
   442  		WithTokenEndpoint("http:trololo"),
   443  		WithGrantType("grant_type"),
   444  		WithRequestTimeout(time.Second),
   445  		WithResource("res", "res2"),
   446  		WithSubjectToken(&errorTokenSource{}),
   447  	)
   448  	require.NoError(t, err)
   449  
   450  	token, err = client.Token(context.Background())
   451  	require.ErrorIs(t, err, errTokenSource)
   452  	require.Equal(t, "", token)
   453  }
   454  
   455  func TestErrorInHTTPRequest(t *testing.T) {
   456  	xtest.TestManyTimes(t, func(t testing.TB) {
   457  		client, err := NewOauth2TokenExchangeCredentials(
   458  			WithTokenEndpoint("http://invalid_host:42/exchange"),
   459  			WithJWTSubjectToken(
   460  				WithRSAPrivateKeyPEMContent([]byte(testRSAPrivateKeyContent)),
   461  				WithKeyID("key_id"),
   462  				WithSigningMethod(jwt.SigningMethodRS256),
   463  				WithIssuer("test_issuer"),
   464  				WithAudience("test_audience"),
   465  			),
   466  			WithJWTActorToken(
   467  				WithRSAPrivateKeyPEMContent([]byte(testRSAPrivateKeyContent)),
   468  				WithKeyID("key_id"),
   469  				WithSigningMethod(jwt.SigningMethodRS256),
   470  				WithIssuer("test_issuer"),
   471  			),
   472  			WithScope("1", "2", "3"),
   473  			WithSourceInfo("TestErrorInHTTPRequest"),
   474  			WithSyncExchangeTimeout(time.Second*3),
   475  		)
   476  		require.NoError(t, err)
   477  
   478  		token, err := client.Token(context.Background())
   479  		if !errors.Is(err, context.DeadlineExceeded) {
   480  			require.ErrorIs(t, err, errCouldNotExchangeToken)
   481  		}
   482  		require.Equal(t, "", token)
   483  
   484  		// check format:
   485  		formatted := fmt.Sprint(client)
   486  		require.Equal(t, `OAuth2TokenExchange{Endpoint:"http://invalid_host:42/exchange",GrantType:urn:ietf:params:oauth:grant-type:token-exchange,Resource:[],Audience:[],Scope:[1 2 3],RequestedTokenType:urn:ietf:params:oauth:token-type:access_token,SubjectToken:JWTTokenSource{Method:RS256,KeyID:key_id,Issuer:"test_issuer",Subject:"",Audience:[test_audience],ID:,TokenTTL:1h0m0s},ActorToken:JWTTokenSource{Method:RS256,KeyID:key_id,Issuer:"test_issuer",Subject:"",Audience:[],ID:,TokenTTL:1h0m0s},From:"TestErrorInHTTPRequest"}`, formatted) //nolint:lll
   487  	}, xtest.StopAfter(15*time.Second))
   488  }
   489  
   490  func TestJWTTokenSource(t *testing.T) {
   491  	methods := []string{
   492  		"RS384",
   493  		"ES256",
   494  		"HS512",
   495  		"PS512",
   496  	}
   497  	binaryOpts := []bool{
   498  		false,
   499  		true,
   500  	}
   501  
   502  	for _, method := range methods {
   503  		for _, binary := range binaryOpts {
   504  			var publicKey interface{}
   505  			var src TokenSource
   506  			var err error
   507  
   508  			//nolint:nestif
   509  			if method[0:2] == "HS" {
   510  				publicKey, err = base64.StdEncoding.DecodeString(testHMACSecretKeyBase64Content)
   511  				require.NoError(t, err)
   512  
   513  				if binary {
   514  					src, err = NewJWTTokenSource(
   515  						WithHMACSecretKey(publicKey.([]byte)),
   516  						WithKeyID("key_id"),
   517  						WithSigningMethodName(method),
   518  						WithIssuer("test_issuer"),
   519  						WithAudience("test_audience"),
   520  					)
   521  					require.NoError(t, err)
   522  				} else {
   523  					src, err = NewJWTTokenSource(
   524  						WithHMACSecretKeyBase64Content(testHMACSecretKeyBase64Content),
   525  						WithKeyID("key_id"),
   526  						WithSigningMethodName(method),
   527  						WithIssuer("test_issuer"),
   528  						WithAudience("test_audience"),
   529  					)
   530  					require.NoError(t, err)
   531  				}
   532  			} else if method[0:2] == "ES" {
   533  				if binary {
   534  					continue
   535  				}
   536  
   537  				publicKey, err = jwt.ParseECPublicKeyFromPEM([]byte(testECPublicKeyContent))
   538  				require.NoError(t, err)
   539  
   540  				src, err = NewJWTTokenSource(
   541  					WithECPrivateKeyPEMContent([]byte(testECPrivateKeyContent)),
   542  					WithKeyID("key_id"),
   543  					WithSigningMethodName(method),
   544  					WithIssuer("test_issuer"),
   545  					WithAudience("test_audience"),
   546  				)
   547  				require.NoError(t, err)
   548  			} else {
   549  				if binary {
   550  					continue
   551  				}
   552  
   553  				publicKey, err = jwt.ParseRSAPublicKeyFromPEM([]byte(testRSAPublicKeyContent))
   554  				require.NoError(t, err)
   555  
   556  				src, err = NewJWTTokenSource(
   557  					WithRSAPrivateKeyPEMContent([]byte(testRSAPrivateKeyContent)),
   558  					WithKeyID("key_id"),
   559  					WithSigningMethodName(method),
   560  					WithIssuer("test_issuer"),
   561  					WithAudience("test_audience"),
   562  				)
   563  				require.NoError(t, err)
   564  			}
   565  
   566  			getPublicKey := func(*jwt.Token) (interface{}, error) {
   567  				return publicKey, nil
   568  			}
   569  
   570  			token, err := src.Token()
   571  			require.NoError(t, err)
   572  			require.Equal(t, "urn:ietf:params:oauth:token-type:jwt", token.TokenType)
   573  
   574  			claims := jwt.RegisteredClaims{}
   575  			parsedToken, err := jwt.ParseWithClaims(token.Token, &claims, getPublicKey)
   576  			require.NoError(t, err)
   577  
   578  			require.True(t, parsedToken.Valid)
   579  			require.NoError(t, parsedToken.Claims.Valid())
   580  			require.Equal(t, "test_issuer", claims.Issuer)
   581  			require.Equal(t, "test_audience", claims.Audience[0])
   582  			require.Equal(t, "key_id", parsedToken.Header["kid"].(string))
   583  			require.Equal(t, method, parsedToken.Header["alg"].(string))
   584  		}
   585  	}
   586  }
   587  
   588  func TestJWTTokenBadParams(t *testing.T) {
   589  	_, err := NewJWTTokenSource(
   590  		// no private key
   591  		WithKeyID("key_id"),
   592  		WithSigningMethod(jwt.SigningMethodRS256),
   593  		WithIssuer("test_issuer"),
   594  		WithAudience("test_audience"),
   595  		WithID("id"),
   596  	)
   597  	require.ErrorIs(t, err, errNoPrivateKeyError)
   598  
   599  	_, err = NewJWTTokenSource(
   600  		WithPrivateKey([]byte{1, 2, 3}),
   601  		WithKeyID("key_id"),
   602  		// no signing method
   603  		WithSubject("s"),
   604  		WithTokenTTL(time.Minute),
   605  		WithAudience("test_audience"),
   606  	)
   607  	require.ErrorIs(t, err, errNoSigningMethodError)
   608  }
   609  
   610  func TestJWTTokenSourceReadPrivateKeyFromFile(t *testing.T) {
   611  	methods := []string{
   612  		"ES256",
   613  		"PS512",
   614  		"RS384",
   615  		"HS256",
   616  	}
   617  	binaryOpts := []bool{
   618  		false,
   619  		true,
   620  	}
   621  
   622  	for _, method := range methods {
   623  		for _, binary := range binaryOpts {
   624  			f, err := os.CreateTemp("", "tmpfile-")
   625  			require.NoError(t, err)
   626  			defer os.Remove(f.Name())
   627  
   628  			var publicKey interface{}
   629  			var src TokenSource
   630  
   631  			//nolint:nestif
   632  			if method[0:2] == "HS" {
   633  				publicKey, err = base64.StdEncoding.DecodeString(testHMACSecretKeyBase64Content)
   634  				require.NoError(t, err)
   635  
   636  				if binary {
   637  					_, err = f.Write(publicKey.([]byte))
   638  					require.NoError(t, err)
   639  					f.Close()
   640  
   641  					_, err = NewJWTTokenSource(
   642  						WithHMACSecretKeyFile("~/unknown_file"),
   643  						WithKeyID("key_id"),
   644  						WithSigningMethodName(method),
   645  						WithIssuer("test_issuer"),
   646  						WithAudience("test_audience"),
   647  					)
   648  					require.ErrorIs(t, err, errCouldNotReadPrivateKeyFile)
   649  
   650  					src, err = NewJWTTokenSource(
   651  						WithHMACSecretKeyFile(f.Name()),
   652  						WithKeyID("key_id"),
   653  						WithSigningMethodName(method),
   654  						WithIssuer("test_issuer"),
   655  						WithAudience("test_audience"),
   656  					)
   657  					require.NoError(t, err)
   658  				} else {
   659  					_, err = f.WriteString(testHMACSecretKeyBase64Content)
   660  					require.NoError(t, err)
   661  					f.Close()
   662  
   663  					_, err = NewJWTTokenSource(
   664  						WithHMACSecretKeyBase64File("~/unknown_file"),
   665  						WithKeyID("key_id"),
   666  						WithSigningMethodName(method),
   667  						WithIssuer("test_issuer"),
   668  						WithAudience("test_audience"),
   669  					)
   670  					require.ErrorIs(t, err, errCouldNotReadPrivateKeyFile)
   671  
   672  					src, err = NewJWTTokenSource(
   673  						WithHMACSecretKeyBase64File(f.Name()),
   674  						WithKeyID("key_id"),
   675  						WithSigningMethodName(method),
   676  						WithIssuer("test_issuer"),
   677  						WithAudience("test_audience"),
   678  					)
   679  					require.NoError(t, err)
   680  				}
   681  			} else if method[0:2] == "ES" {
   682  				if binary {
   683  					continue
   684  				}
   685  
   686  				publicKey, err = jwt.ParseECPublicKeyFromPEM([]byte(testECPublicKeyContent))
   687  				require.NoError(t, err)
   688  
   689  				_, err = f.WriteString(testECPrivateKeyContent)
   690  				require.NoError(t, err)
   691  				f.Close()
   692  
   693  				_, err = NewJWTTokenSource(
   694  					WithECPrivateKeyPEMFile("~/unknown_file"),
   695  					WithKeyID("key_id"),
   696  					WithSigningMethodName(method),
   697  					WithIssuer("test_issuer"),
   698  					WithAudience("test_audience"),
   699  				)
   700  				require.ErrorIs(t, err, errCouldNotReadPrivateKeyFile)
   701  
   702  				src, err = NewJWTTokenSource(
   703  					WithECPrivateKeyPEMFile(f.Name()),
   704  					WithKeyID("key_id"),
   705  					WithSigningMethodName(method),
   706  					WithIssuer("test_issuer"),
   707  					WithAudience("test_audience"),
   708  				)
   709  				require.NoError(t, err)
   710  			} else {
   711  				if binary {
   712  					continue
   713  				}
   714  
   715  				publicKey, err = jwt.ParseRSAPublicKeyFromPEM([]byte(testRSAPublicKeyContent))
   716  				require.NoError(t, err)
   717  
   718  				_, err = f.WriteString(testRSAPrivateKeyContent)
   719  				require.NoError(t, err)
   720  				f.Close()
   721  
   722  				_, err = NewJWTTokenSource(
   723  					WithRSAPrivateKeyPEMFile("~/unknown_file"),
   724  					WithKeyID("key_id"),
   725  					WithSigningMethodName(method),
   726  					WithIssuer("test_issuer"),
   727  					WithAudience("test_audience"),
   728  				)
   729  				require.ErrorIs(t, err, errCouldNotReadPrivateKeyFile)
   730  
   731  				src, err = NewJWTTokenSource(
   732  					WithRSAPrivateKeyPEMFile(f.Name()),
   733  					WithKeyID("key_id"),
   734  					WithSigningMethodName(method),
   735  					WithIssuer("test_issuer"),
   736  					WithAudience("test_audience"),
   737  				)
   738  				require.NoError(t, err)
   739  			}
   740  
   741  			token, err := src.Token()
   742  			require.NoError(t, err)
   743  
   744  			// parse token
   745  			getPublicKey := func(*jwt.Token) (interface{}, error) {
   746  				return publicKey, nil
   747  			}
   748  
   749  			claims := jwt.RegisteredClaims{}
   750  			parsedToken, err := jwt.ParseWithClaims(token.Token, &claims, getPublicKey)
   751  			require.NoError(t, err)
   752  
   753  			require.True(t, parsedToken.Valid)
   754  			require.NoError(t, parsedToken.Claims.Valid())
   755  			require.Equal(t, "test_issuer", claims.Issuer)
   756  			require.Equal(t, "test_audience", claims.Audience[0])
   757  			require.Equal(t, "key_id", parsedToken.Header["kid"].(string))
   758  			require.Equal(t, method, parsedToken.Header["alg"].(string))
   759  		}
   760  	}
   761  }
   762  
   763  type parseSettingsFromFileTestParams struct {
   764  	Cfg                          string
   765  	CfgFile                      string
   766  	ExpectedError                error
   767  	ExpectedFormattedCredentials string
   768  }
   769  
   770  func TestParseSettingsFromFile(t *testing.T) {
   771  	testsParams := []parseSettingsFromFileTestParams{
   772  		{
   773  			Cfg: `{
   774  				"token-endpoint": "http://localhost:123",
   775  				"res": "tEst",
   776  				"grant-type": "grant",
   777  				"subject-credentials": {
   778  					"type": "fixed",
   779  					"token": "test-token",
   780  					"token-type": "test-token-type"
   781  				}
   782  			}`,
   783  			ExpectedFormattedCredentials: `OAuth2TokenExchange{Endpoint:"http://localhost:123",GrantType:grant,Resource:[tEst],Audience:[],Scope:[],RequestedTokenType:urn:ietf:params:oauth:token-type:access_token,SubjectToken:FixedTokenSource{Token:"****(CRC-32c: 1203ABFA)",Type:test-token-type},From:"TestParseSettingsFromFile"}`, //nolint:lll
   784  		},
   785  		{
   786  			Cfg: `{
   787  				"token-endpoint": "http://localhost:123",
   788  				"aud": "test-aud",
   789  				"res": [
   790  					"r1",
   791  					"r2"
   792  				],
   793  				"scope": [
   794  					"s1",
   795  					"s2"
   796  				],
   797  				"unknown-field": [123],
   798  				"actor-credentials": {
   799  					"type": "fixed",
   800  					"token": "test-token",
   801  					"token-type": "test-token-type"
   802  				}
   803  			}`,
   804  			ExpectedFormattedCredentials: `OAuth2TokenExchange{Endpoint:"http://localhost:123",GrantType:urn:ietf:params:oauth:grant-type:token-exchange,Resource:[r1 r2],Audience:[test-aud],Scope:[s1 s2],RequestedTokenType:urn:ietf:params:oauth:token-type:access_token,ActorToken:FixedTokenSource{Token:"****(CRC-32c: 1203ABFA)",Type:test-token-type},From:"TestParseSettingsFromFile"}`, //nolint:lll
   805  		},
   806  		{
   807  			Cfg: `{
   808  				"token-endpoint": "http://localhost:123",
   809  				"requested-token-type": "access_token",
   810  				"subject-credentials": {
   811  					"type": "JWT",
   812  					"alg": "ps256",
   813  					"private-key": "` + testRSAPrivateKeyJSONContent + `",
   814  					"aud": ["a1", "a2"],
   815  					"jti": "123",
   816  					"sub": "test_subject",
   817  					"iss": "test_issuer",
   818  					"kid": "test_key_id",
   819  					"ttl": "24h",
   820  					"unknown_field": [123]
   821  				}
   822  			}`,
   823  			ExpectedFormattedCredentials: `OAuth2TokenExchange{Endpoint:"http://localhost:123",GrantType:urn:ietf:params:oauth:grant-type:token-exchange,Resource:[],Audience:[],Scope:[],RequestedTokenType:access_token,SubjectToken:JWTTokenSource{Method:PS256,KeyID:test_key_id,Issuer:"test_issuer",Subject:"test_subject",Audience:[a1 a2],ID:123,TokenTTL:24h0m0s},From:"TestParseSettingsFromFile"}`, //nolint:lll
   824  		},
   825  		{
   826  			Cfg: `{
   827  				"token-endpoint": "http://localhost:123",
   828  				"subject-credentials": {
   829  					"type": "JWT",
   830  					"alg": "es256",
   831  					"private-key": "` + testECPrivateKeyJSONContent + `",
   832  					"ttl": "3m"
   833  				}
   834  			}`,
   835  			ExpectedFormattedCredentials: `OAuth2TokenExchange{Endpoint:"http://localhost:123",GrantType:urn:ietf:params:oauth:grant-type:token-exchange,Resource:[],Audience:[],Scope:[],RequestedTokenType:urn:ietf:params:oauth:token-type:access_token,SubjectToken:JWTTokenSource{Method:ES256,KeyID:,Issuer:"",Subject:"",Audience:[],ID:,TokenTTL:3m0s},From:"TestParseSettingsFromFile"}`, //nolint:lll
   836  		},
   837  		{
   838  			Cfg: `{
   839  				"token-endpoint": "http://localhost:123",
   840  				"subject-credentials": {
   841  					"type": "JWT",
   842  					"alg": "hs512",
   843  					"private-key": "` + testHMACSecretKeyBase64Content + `"
   844  				}
   845  			}`,
   846  			ExpectedFormattedCredentials: `OAuth2TokenExchange{Endpoint:"http://localhost:123",GrantType:urn:ietf:params:oauth:grant-type:token-exchange,Resource:[],Audience:[],Scope:[],RequestedTokenType:urn:ietf:params:oauth:token-type:access_token,SubjectToken:JWTTokenSource{Method:HS512,KeyID:,Issuer:"",Subject:"",Audience:[],ID:,TokenTTL:1h0m0s},From:"TestParseSettingsFromFile"}`, //nolint:lll
   847  		},
   848  		{
   849  			Cfg: `{
   850  				"token-endpoint": "http://localhost:123",
   851  				"subject-credentials": {
   852  					"type": "JWT",
   853  					"alg": "rs512",
   854  					"private-key": "` + testHMACSecretKeyBase64Content + `"
   855  				}
   856  			}`,
   857  			ExpectedError: errCouldNotparsePrivateKey, // wrong private key format
   858  		},
   859  		{
   860  			Cfg: `{
   861  				"token-endpoint": "http://localhost:123",
   862  				"subject-credentials": {
   863  					"type": "JWT",
   864  					"alg": "es512",
   865  					"private-key": "` + testHMACSecretKeyBase64Content + `"
   866  				}
   867  			}`,
   868  			ExpectedError: errCouldNotparsePrivateKey, // wrong private key format
   869  		},
   870  		{
   871  			Cfg: `{
   872  				"token-endpoint": "http://localhost:123",
   873  				"subject-credentials": {
   874  					"type": "JWT",
   875  					"alg": "es512",
   876  					"private-key": "` + testRSAPrivateKeyJSONContent + `"
   877  				}
   878  			}`,
   879  			ExpectedError: errCouldNotparsePrivateKey, // wrong private key format
   880  		},
   881  		{
   882  			Cfg: `{
   883  				"token-endpoint": "http://localhost:123",
   884  				"subject-credentials": {
   885  					"type": "JWT",
   886  					"alg": "hs512",
   887  					"private-key": "<not base64>"
   888  				}
   889  			}`,
   890  			ExpectedError: errCouldNotParseBase64Secret, // wrong private key format
   891  		},
   892  		{
   893  			CfgFile:       "~/unknown-file.cfg",
   894  			ExpectedError: errCouldNotReadConfigFile,
   895  		},
   896  		{
   897  			Cfg:           "{not json",
   898  			ExpectedError: errCouldNotUnmarshalJSON,
   899  		},
   900  		{
   901  			Cfg: `{
   902  				"actor-credentials": ""
   903  			}`,
   904  			ExpectedError: errCouldNotUnmarshalJSON,
   905  		},
   906  		{
   907  			Cfg: `{
   908  				"subject-credentials": {
   909  					"type": "JWT",
   910  					"ttl": 123
   911  				}
   912  			}`,
   913  			ExpectedError: errCouldNotUnmarshalJSON,
   914  		},
   915  		{
   916  			Cfg: `{
   917  				"subject-credentials": {
   918  					"type": "JWT",
   919  					"ttl": "123"
   920  				}
   921  			}`,
   922  			ExpectedError: errCouldNotUnmarshalJSON,
   923  		},
   924  		{
   925  			Cfg: `{
   926  				"subject-credentials": {
   927  					"type": "JWT",
   928  					"ttl": "-3h"
   929  				}
   930  			}`,
   931  			ExpectedError: errTTLMustBePositive,
   932  		},
   933  		{
   934  			Cfg: `{
   935  				"actor-credentials": {
   936  					"type": "JWT",
   937  					"alg": "HS384"
   938  				}
   939  			}`,
   940  			ExpectedError: errAlgAndKeyRequired,
   941  		},
   942  		{
   943  			Cfg: `{
   944  				"actor-credentials": {
   945  					"type": "JWT",
   946  					"private-key": "1234"
   947  				}
   948  			}`,
   949  			ExpectedError: errAlgAndKeyRequired,
   950  		},
   951  		{
   952  			Cfg: `{
   953  				"actor-credentials": {
   954  					"type": "JWT",
   955  					"alg": "unknown",
   956  					"private-key": "1234"
   957  				}
   958  			}`,
   959  			ExpectedError: errUnsupportedSigningMethod,
   960  		},
   961  		{
   962  			Cfg: `{
   963  				"actor-credentials": {
   964  					"type": "JWT",
   965  					"ttl": "3h"
   966  				}
   967  			}`,
   968  			ExpectedError: errAlgAndKeyRequired,
   969  		},
   970  		{
   971  			Cfg: `{
   972  				"aud": {
   973  					"value": "wrong_format of aud: not string and not list"
   974  				},
   975  				"actor-credentials": {
   976  					"type": "fixed",
   977  					"token": "test-token",
   978  					"token-type": "test-token-type"
   979  				}
   980  			}`,
   981  			ExpectedError: errCouldNotUnmarshalJSON,
   982  		},
   983  		{
   984  			Cfg: `{
   985  				"actor-credentials": {
   986  					"type": "unknown"
   987  				}
   988  			}`,
   989  			ExpectedError: errUnknownTokenSourceType,
   990  		},
   991  		{
   992  			Cfg: `{
   993  				"subject-credentials": {
   994  					"token": "123"
   995  				}
   996  			}`,
   997  			ExpectedError: errUnknownTokenSourceType,
   998  		},
   999  		{
  1000  			Cfg: `{
  1001  				"subject-credentials": {
  1002  					"type": "FIXED",
  1003  					"token": "123"
  1004  				}
  1005  			}`,
  1006  			ExpectedError: errTokenAndTokenTypeRequired,
  1007  		},
  1008  		{
  1009  			Cfg: `{
  1010  				"actor-credentials": {
  1011  					"type": "Fixed",
  1012  					"token-type": "t"
  1013  				}
  1014  			}`,
  1015  			ExpectedError: errTokenAndTokenTypeRequired,
  1016  		},
  1017  	}
  1018  	for _, params := range testsParams {
  1019  		var fileName string
  1020  		if params.Cfg != "" {
  1021  			f, err := os.CreateTemp("", "cfg-")
  1022  			require.NoError(t, err)
  1023  			defer os.Remove(f.Name())
  1024  			_, err = f.WriteString(params.Cfg)
  1025  			require.NoError(t, err)
  1026  			f.Close()
  1027  			fileName = f.Name()
  1028  		} else {
  1029  			fileName = params.CfgFile
  1030  		}
  1031  
  1032  		client, err := NewOauth2TokenExchangeCredentialsFile(
  1033  			fileName,
  1034  			WithSourceInfo("TestParseSettingsFromFile"),
  1035  		)
  1036  		t.Logf("Cfg:\n%s\n", params.Cfg)
  1037  		if params.ExpectedError != nil {
  1038  			require.ErrorIs(t, err, params.ExpectedError)
  1039  		} else {
  1040  			require.NoError(t, err)
  1041  			formatted := fmt.Sprint(client)
  1042  			require.Equal(t, params.ExpectedFormattedCredentials, formatted)
  1043  		}
  1044  	}
  1045  }