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(¤tTestParams, 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(¤tTestParams, 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(¤tTestParams, 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 }