From bae96d35d0da8ee639ed12b8a33fe4d855fcd48a Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Tue, 18 Oct 2022 01:46:05 +0000 Subject: [PATCH 1/7] fix: allow for alternate usernames on conflict --- coderd/coderdtest/coderdtest.go | 75 ++++++++++++++ coderd/database/databasefake/databasefake.go | 6 ++ coderd/userauth.go | 30 ++++++ coderd/userauth_test.go | 102 ++++++++++--------- 4 files changed, 166 insertions(+), 47 deletions(-) diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index d9ddc0bae2f20..9a12460442261 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -29,6 +29,7 @@ import ( "time" "cloud.google.com/go/compute/metadata" + "github.com/coreos/go-oidc/v3/oidc" "github.com/fullsailor/pkcs7" "github.com/golang-jwt/jwt" "github.com/google/uuid" @@ -36,6 +37,7 @@ import ( "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "golang.org/x/oauth2" "golang.org/x/xerrors" "google.golang.org/api/idtoken" "google.golang.org/api/option" @@ -725,6 +727,79 @@ func NewAWSInstanceIdentity(t *testing.T, instanceID string) (awsidentity.Certif } } +type FakeOIDCConfig struct { + key *rsa.PrivateKey + issuer string +} + +func NewFakeOIDCConfig(t *testing.T, issuer string) *FakeOIDCConfig { + t.Helper() + + pkey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + if issuer == "" { + issuer = "https://coder.com" + } + + return &FakeOIDCConfig{ + key: pkey, + issuer: issuer, + } +} + +func (*FakeOIDCConfig) AuthCodeURL(state string, _ ...oauth2.AuthCodeOption) string { + return "/?state=" + url.QueryEscape(state) +} + +func (*FakeOIDCConfig) TokenSource(context.Context, *oauth2.Token) oauth2.TokenSource { + return nil +} + +func (*FakeOIDCConfig) Exchange(_ context.Context, code string, _ ...oauth2.AuthCodeOption) (*oauth2.Token, error) { + token, err := base64.StdEncoding.DecodeString(code) + if err != nil { + return nil, xerrors.Errorf("decode code: %w", err) + } + return (&oauth2.Token{ + AccessToken: "token", + }).WithExtra(map[string]interface{}{ + "id_token": string(token), + }), nil +} + +func (o *FakeOIDCConfig) EncodeClaims(t *testing.T, claims jwt.MapClaims) string { + t.Helper() + + if _, ok := claims["exp"]; !ok { + claims["exp"] = time.Now().Add(time.Hour).UnixMilli() + } + + if _, ok := claims["iss"]; !ok { + claims["iss"] = o.issuer + } + + if _, ok := claims["sub"]; !ok { + claims["sub"] = "testme" + } + + signed, err := jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(o.key) + require.NoError(t, err) + + return base64.StdEncoding.EncodeToString([]byte(signed)) +} + +func (o *FakeOIDCConfig) OIDCConfig() *coderd.OIDCConfig { + return &coderd.OIDCConfig{ + OAuth2Config: o, + Verifier: oidc.NewVerifier(o.issuer, &oidc.StaticKeySet{ + PublicKeys: []crypto.PublicKey{o.key.Public()}, + }, &oidc.Config{ + SkipClientIDCheck: true, + }), + } +} + // NewAzureInstanceIdentity returns a metadata client and ID token validator for faking // instance authentication for Azure. func NewAzureInstanceIdentity(t *testing.T, instanceID string) (x509.VerifyOptions, *http.Client) { diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index 757616774c6c7..37b5d288aff34 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -2221,6 +2221,12 @@ func (q *fakeQuerier) InsertUser(_ context.Context, arg database.InsertUserParam q.mutex.Lock() defer q.mutex.Unlock() + for _, user := range q.users { + if user.Username == arg.Username { + return database.User{}, errDuplicateKey + } + } + user := database.User{ ID: arg.ID, Email: arg.Email, diff --git a/coderd/userauth.go b/coderd/userauth.go index 7a18b9790df04..7f74e18960f67 100644 --- a/coderd/userauth.go +++ b/coderd/userauth.go @@ -13,6 +13,7 @@ import ( "github.com/coreos/go-oidc/v3/oidc" "github.com/google/go-github/v43/github" "github.com/google/uuid" + "github.com/moby/moby/pkg/namesgenerator" "golang.org/x/oauth2" "golang.org/x/xerrors" @@ -390,6 +391,35 @@ func (api *API) oauthLogin(r *http.Request, params oauthLoginParams) (*http.Cook organizationID = organizations[0].ID } + _, err := tx.GetUserByEmailOrUsername(ctx, database.GetUserByEmailOrUsernameParams{ + Username: params.Username, + }) + if err == nil { + var ( + original = params.Username + validUsername bool + ) + for i := 0; i < 10; i++ { + params.Username = fmt.Sprintf("%s_%s", original, namesgenerator.GetRandomName(10)) + _, err := tx.GetUserByEmailOrUsername(ctx, database.GetUserByEmailOrUsernameParams{ + Username: params.Username, + }) + if xerrors.Is(err, sql.ErrNoRows) { + validUsername = true + break + } + if err != nil { + return xerrors.Errorf("get user by email/username: %w", err) + } + } + if !validUsername { + return httpError{ + code: http.StatusConflict, + msg: fmt.Sprintf("exhausted alternate usernames for taken username %q", original), + } + } + } + user, _, err = api.CreateUser(ctx, tx, CreateUserRequest{ CreateUserRequest: codersdk.CreateUserRequest{ Email: params.Email, diff --git a/coderd/userauth_test.go b/coderd/userauth_test.go index 9643351032a88..f9c212fd0aac9 100644 --- a/coderd/userauth_test.go +++ b/coderd/userauth_test.go @@ -3,13 +3,12 @@ package coderd_test import ( "context" "crypto" - "crypto/rand" - "crypto/rsa" + "fmt" "io" "net/http" "net/url" + "strings" "testing" - "time" "github.com/coreos/go-oidc/v3/oidc" "github.com/golang-jwt/jwt" @@ -450,17 +449,19 @@ func TestUserOIDC(t *testing.T) { tc := tc t.Run(tc.Name, func(t *testing.T) { t.Parallel() - config := createOIDCConfig(t, tc.Claims) + conf := coderdtest.NewFakeOIDCConfig(t, "") + + config := conf.OIDCConfig() config.AllowSignups = tc.AllowSignups config.EmailDomain = tc.EmailDomain + client := coderdtest.New(t, &coderdtest.Options{ OIDCConfig: config, }) - resp := oidcCallback(t, client) + resp := oidcCallback(t, client, conf.EncodeClaims(t, tc.Claims)) assert.Equal(t, tc.StatusCode, resp.StatusCode) - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() + ctx, _ := testutil.Context(t) if tc.Username != "" { client.SessionToken = authCookieValue(resp.Cookies()) @@ -478,10 +479,50 @@ func TestUserOIDC(t *testing.T) { }) } + t.Run("AlternateUsername", func(t *testing.T) { + t.Parallel() + + conf := coderdtest.NewFakeOIDCConfig(t, "") + + config := conf.OIDCConfig() + config.AllowSignups = true + + client := coderdtest.New(t, &coderdtest.Options{ + OIDCConfig: config, + }) + + code := conf.EncodeClaims(t, jwt.MapClaims{ + "email": "jon@coder.com", + }) + resp := oidcCallback(t, client, code) + assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) + + ctx, _ := testutil.Context(t) + + client.SessionToken = authCookieValue(resp.Cookies()) + user, err := client.User(ctx, "me") + require.NoError(t, err) + require.Equal(t, "jon", user.Username) + + // Pass a different subject field so that we prompt creating a + // new user. + code = conf.EncodeClaims(t, jwt.MapClaims{ + "email": "jon@example2.com", + "sub": "diff", + }) + resp = oidcCallback(t, client, code) + assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) + + client.SessionToken = authCookieValue(resp.Cookies()) + user, err = client.User(ctx, "me") + require.NoError(t, err) + require.True(t, strings.HasPrefix(user.Username, "jon_"), "username %q should have prefix %q", user.Username, "jon") + }) + t.Run("Disabled", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - resp := oidcCallback(t, client) + resp := oidcCallback(t, client, "asdf") require.Equal(t, http.StatusPreconditionRequired, resp.StatusCode) }) @@ -492,7 +533,7 @@ func TestUserOIDC(t *testing.T) { OAuth2Config: &oauth2Config{}, }, }) - resp := oidcCallback(t, client) + resp := oidcCallback(t, client, "asdf") require.Equal(t, http.StatusBadRequest, resp.StatusCode) }) @@ -514,48 +555,16 @@ func TestUserOIDC(t *testing.T) { Verifier: verifier, }, }) - resp := oidcCallback(t, client) + resp := oidcCallback(t, client, "asdf") require.Equal(t, http.StatusBadRequest, resp.StatusCode) }) } -// createOIDCConfig generates a new OIDCConfig that returns a static token -// with the claims provided. -func createOIDCConfig(t *testing.T, claims jwt.MapClaims) *coderd.OIDCConfig { - t.Helper() - key, err := rsa.GenerateKey(rand.Reader, 2048) - require.NoError(t, err) - - // https://datatracker.ietf.org/doc/html/rfc7519#section-4.1 - claims["exp"] = time.Now().Add(time.Hour).UnixMilli() - claims["iss"] = "https://coder.com" - claims["sub"] = "hello" - - signed, err := jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(key) - require.NoError(t, err) - - verifier := oidc.NewVerifier("https://coder.com", &oidc.StaticKeySet{ - PublicKeys: []crypto.PublicKey{key.Public()}, - }, &oidc.Config{ - SkipClientIDCheck: true, - }) - - return &coderd.OIDCConfig{ - OAuth2Config: &oauth2Config{ - token: (&oauth2.Token{ - AccessToken: "token", - }).WithExtra(map[string]interface{}{ - "id_token": signed, - }), - }, - Verifier: verifier, - } -} - func oauth2Callback(t *testing.T, client *codersdk.Client) *http.Response { client.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse } + state := "somestate" oauthURL, err := client.URL.Parse("/api/v2/users/oauth2/github/callback?code=asd&state=" + state) require.NoError(t, err) @@ -573,19 +582,18 @@ func oauth2Callback(t *testing.T, client *codersdk.Client) *http.Response { return res } -func oidcCallback(t *testing.T, client *codersdk.Client) *http.Response { +func oidcCallback(t *testing.T, client *codersdk.Client, code string) *http.Response { t.Helper() client.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse } - state := "somestate" - oauthURL, err := client.URL.Parse("/api/v2/users/oidc/callback?code=asd&state=" + state) + oauthURL, err := client.URL.Parse(fmt.Sprintf("/api/v2/users/oidc/callback?code=%s&state=somestate", code)) require.NoError(t, err) req, err := http.NewRequestWithContext(context.Background(), "GET", oauthURL.String(), nil) require.NoError(t, err) req.AddCookie(&http.Cookie{ Name: codersdk.OAuth2StateKey, - Value: state, + Value: "somestate", }) res, err := client.HTTPClient.Do(req) require.NoError(t, err) From 6cbb63c09933ce18c72d59d0c4888f3a25e309e1 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Tue, 18 Oct 2022 01:52:47 +0000 Subject: [PATCH 2/7] update error message --- coderd/userauth.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/userauth.go b/coderd/userauth.go index 7f74e18960f67..1f3a390ae8a31 100644 --- a/coderd/userauth.go +++ b/coderd/userauth.go @@ -415,7 +415,7 @@ func (api *API) oauthLogin(r *http.Request, params oauthLoginParams) (*http.Cook if !validUsername { return httpError{ code: http.StatusConflict, - msg: fmt.Sprintf("exhausted alternate usernames for taken username %q", original), + msg: fmt.Sprintf("exhausted alternatives for taken username %q", original), } } } From cad20870a18b033da6e38b3cea943d9cc2a4968f Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Tue, 18 Oct 2022 02:01:12 +0000 Subject: [PATCH 3/7] fix dbfake --- coderd/database/databasefake/databasefake.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index 37b5d288aff34..6f809ad117226 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -2222,7 +2222,7 @@ func (q *fakeQuerier) InsertUser(_ context.Context, arg database.InsertUserParam defer q.mutex.Unlock() for _, user := range q.users { - if user.Username == arg.Username { + if user.Username == arg.Username && !user.Deleted { return database.User{}, errDuplicateKey } } From 33392731f3bfadbaf528a1c88c23a9e220606ade Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Tue, 18 Oct 2022 02:13:50 +0000 Subject: [PATCH 4/7] fix another test --- enterprise/coderd/license/license_test.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/enterprise/coderd/license/license_test.go b/enterprise/coderd/license/license_test.go index 6def291e3e24c..a47dd83c98ab1 100644 --- a/enterprise/coderd/license/license_test.go +++ b/enterprise/coderd/license/license_test.go @@ -140,8 +140,12 @@ func TestEntitlements(t *testing.T) { t.Run("TooManyUsers", func(t *testing.T) { t.Parallel() db := databasefake.New() - db.InsertUser(context.Background(), database.InsertUserParams{}) - db.InsertUser(context.Background(), database.InsertUserParams{}) + db.InsertUser(context.Background(), database.InsertUserParams{ + Username: "test1", + }) + db.InsertUser(context.Background(), database.InsertUserParams{ + Username: "test2", + }) db.InsertLicense(context.Background(), database.InsertLicenseParams{ JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{ UserLimit: 1, From 7b49a0d5d54b7324b86c01c6564b5b31eedfcbe0 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Tue, 18 Oct 2022 02:40:49 +0000 Subject: [PATCH 5/7] pr comments --- coderd/userauth.go | 5 ++++- coderd/userauth_test.go | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/coderd/userauth.go b/coderd/userauth.go index 1f3a390ae8a31..4a5c9c84f2095 100644 --- a/coderd/userauth.go +++ b/coderd/userauth.go @@ -400,7 +400,10 @@ func (api *API) oauthLogin(r *http.Request, params oauthLoginParams) (*http.Cook validUsername bool ) for i := 0; i < 10; i++ { - params.Username = fmt.Sprintf("%s_%s", original, namesgenerator.GetRandomName(10)) + alternate := fmt.Sprintf("%s_%s", original, namesgenerator.GetRandomName(1)) + + params.Username = httpapi.UsernameFrom(alternate) + _, err := tx.GetUserByEmailOrUsername(ctx, database.GetUserByEmailOrUsernameParams{ Username: params.Username, }) diff --git a/coderd/userauth_test.go b/coderd/userauth_test.go index f9c212fd0aac9..2f9da0b6948f5 100644 --- a/coderd/userauth_test.go +++ b/coderd/userauth_test.go @@ -449,7 +449,7 @@ func TestUserOIDC(t *testing.T) { tc := tc t.Run(tc.Name, func(t *testing.T) { t.Parallel() - conf := coderdtest.NewFakeOIDCConfig(t, "") + conf := coderdtest.NewOIDCConfig(t, "") config := conf.OIDCConfig() config.AllowSignups = tc.AllowSignups @@ -482,7 +482,7 @@ func TestUserOIDC(t *testing.T) { t.Run("AlternateUsername", func(t *testing.T) { t.Parallel() - conf := coderdtest.NewFakeOIDCConfig(t, "") + conf := coderdtest.NewOIDCConfig(t, "") config := conf.OIDCConfig() config.AllowSignups = true From c5975859693d923fdb4eec88b458372f5e26aee2 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Tue, 18 Oct 2022 02:43:10 +0000 Subject: [PATCH 6/7] forgot to save --- coderd/coderdtest/coderdtest.go | 16 ++++++++-------- coderd/userauth.go | 2 +- coderd/userauth_test.go | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 9a12460442261..4970da381d866 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -727,12 +727,12 @@ func NewAWSInstanceIdentity(t *testing.T, instanceID string) (awsidentity.Certif } } -type FakeOIDCConfig struct { +type OIDCConfig struct { key *rsa.PrivateKey issuer string } -func NewFakeOIDCConfig(t *testing.T, issuer string) *FakeOIDCConfig { +func NewOIDCConfig(t *testing.T, issuer string) *OIDCConfig { t.Helper() pkey, err := rsa.GenerateKey(rand.Reader, 2048) @@ -742,21 +742,21 @@ func NewFakeOIDCConfig(t *testing.T, issuer string) *FakeOIDCConfig { issuer = "https://coder.com" } - return &FakeOIDCConfig{ + return &OIDCConfig{ key: pkey, issuer: issuer, } } -func (*FakeOIDCConfig) AuthCodeURL(state string, _ ...oauth2.AuthCodeOption) string { +func (*OIDCConfig) AuthCodeURL(state string, _ ...oauth2.AuthCodeOption) string { return "/?state=" + url.QueryEscape(state) } -func (*FakeOIDCConfig) TokenSource(context.Context, *oauth2.Token) oauth2.TokenSource { +func (*OIDCConfig) TokenSource(context.Context, *oauth2.Token) oauth2.TokenSource { return nil } -func (*FakeOIDCConfig) Exchange(_ context.Context, code string, _ ...oauth2.AuthCodeOption) (*oauth2.Token, error) { +func (*OIDCConfig) Exchange(_ context.Context, code string, _ ...oauth2.AuthCodeOption) (*oauth2.Token, error) { token, err := base64.StdEncoding.DecodeString(code) if err != nil { return nil, xerrors.Errorf("decode code: %w", err) @@ -768,7 +768,7 @@ func (*FakeOIDCConfig) Exchange(_ context.Context, code string, _ ...oauth2.Auth }), nil } -func (o *FakeOIDCConfig) EncodeClaims(t *testing.T, claims jwt.MapClaims) string { +func (o *OIDCConfig) EncodeClaims(t *testing.T, claims jwt.MapClaims) string { t.Helper() if _, ok := claims["exp"]; !ok { @@ -789,7 +789,7 @@ func (o *FakeOIDCConfig) EncodeClaims(t *testing.T, claims jwt.MapClaims) string return base64.StdEncoding.EncodeToString([]byte(signed)) } -func (o *FakeOIDCConfig) OIDCConfig() *coderd.OIDCConfig { +func (o *OIDCConfig) OIDCConfig() *coderd.OIDCConfig { return &coderd.OIDCConfig{ OAuth2Config: o, Verifier: oidc.NewVerifier(o.issuer, &oidc.StaticKeySet{ diff --git a/coderd/userauth.go b/coderd/userauth.go index 4a5c9c84f2095..30c66a0c585a9 100644 --- a/coderd/userauth.go +++ b/coderd/userauth.go @@ -400,7 +400,7 @@ func (api *API) oauthLogin(r *http.Request, params oauthLoginParams) (*http.Cook validUsername bool ) for i := 0; i < 10; i++ { - alternate := fmt.Sprintf("%s_%s", original, namesgenerator.GetRandomName(1)) + alternate := fmt.Sprintf("%s-%s", original, namesgenerator.GetRandomName(1)) params.Username = httpapi.UsernameFrom(alternate) diff --git a/coderd/userauth_test.go b/coderd/userauth_test.go index 2f9da0b6948f5..ba4b437bdb8b3 100644 --- a/coderd/userauth_test.go +++ b/coderd/userauth_test.go @@ -516,7 +516,7 @@ func TestUserOIDC(t *testing.T) { client.SessionToken = authCookieValue(resp.Cookies()) user, err = client.User(ctx, "me") require.NoError(t, err) - require.True(t, strings.HasPrefix(user.Username, "jon_"), "username %q should have prefix %q", user.Username, "jon") + require.True(t, strings.HasPrefix(user.Username, "jon-"), "username %q should have prefix %q", user.Username, "jon-") }) t.Run("Disabled", func(t *testing.T) { From 2cc8936f1471531dc54194e323775a55025034e2 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Tue, 18 Oct 2022 02:57:38 +0000 Subject: [PATCH 7/7] hardcode the pkey --- coderd/coderdtest/coderdtest.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 4970da381d866..05e3d6a27d2d4 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -735,7 +735,8 @@ type OIDCConfig struct { func NewOIDCConfig(t *testing.T, issuer string) *OIDCConfig { t.Helper() - pkey, err := rsa.GenerateKey(rand.Reader, 2048) + block, _ := pem.Decode([]byte(testRSAPrivateKey)) + pkey, err := x509.ParsePKCS1PrivateKey(block.Bytes) require.NoError(t, err) if issuer == "" { @@ -880,3 +881,19 @@ func SDKError(t *testing.T, err error) *codersdk.Error { require.True(t, errors.As(err, &cerr)) return cerr } + +const testRSAPrivateKey = `-----BEGIN RSA PRIVATE KEY----- +MIICXQIBAAKBgQDLets8+7M+iAQAqN/5BVyCIjhTQ4cmXulL+gm3v0oGMWzLupUS +v8KPA+Tp7dgC/DZPfMLaNH1obBBhJ9DhS6RdS3AS3kzeFrdu8zFHLWF53DUBhS92 +5dCAEuJpDnNizdEhxTfoHrhuCmz8l2nt1pe5eUK2XWgd08Uc93h5ij098wIDAQAB +AoGAHLaZeWGLSaen6O/rqxg2laZ+jEFbMO7zvOTruiIkL/uJfrY1kw+8RLIn+1q0 +wLcWcuEIHgKKL9IP/aXAtAoYh1FBvRPLkovF1NZB0Je/+CSGka6wvc3TGdvppZJe +rKNcUvuOYLxkmLy4g9zuY5qrxFyhtIn2qZzXEtLaVOHzPQECQQDvN0mSajpU7dTB +w4jwx7IRXGSSx65c+AsHSc1Rj++9qtPC6WsFgAfFN2CEmqhMbEUVGPv/aPjdyWk9 +pyLE9xR/AkEA2cGwyIunijE5v2rlZAD7C4vRgdcMyCf3uuPcgzFtsR6ZhyQSgLZ8 +YRPuvwm4cdPJMmO3YwBfxT6XGuSc2k8MjQJBAI0+b8prvpV2+DCQa8L/pjxp+VhR +Xrq2GozrHrgR7NRokTB88hwFRJFF6U9iogy9wOx8HA7qxEbwLZuhm/4AhbECQC2a +d8h4Ht09E+f3nhTEc87mODkl7WJZpHL6V2sORfeq/eIkds+H6CJ4hy5w/bSw8tjf +sz9Di8sGIaUbLZI2rd0CQQCzlVwEtRtoNCyMJTTrkgUuNufLP19RZ5FpyXxBO5/u +QastnN77KfUwdj3SJt44U/uh1jAIv4oSLBr8HYUkbnI8 +-----END RSA PRIVATE KEY-----`