From 1f939cdbe26eda53652d7f8eb7cbb3e24716ff22 Mon Sep 17 00:00:00 2001 From: Eli Fabens Date: Wed, 14 Apr 2021 11:26:05 -0700 Subject: [PATCH] WIP: Adds 3rd party login functionality to retrieve a V3SDK - Supports arbitrary action handler functions - Example mfa function in identity_test - More tests to come --- client.go | 140 +++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 3 +- go.sum | 7 +-- identity.go | 6 +- identity_test.go | 31 +++++++++++ 5 files changed, 179 insertions(+), 8 deletions(-) create mode 100644 identity_test.go diff --git a/client.go b/client.go index 3286b92..e2d0117 100644 --- a/client.go +++ b/client.go @@ -696,6 +696,10 @@ func GetSDKV3(configJSONFilePath string) (*ToznySDKV3, error) { if err != nil { return nil, err } + return sdkV3FromConfig(config) +} + +func sdkV3FromConfig(config ToznySDKJSONConfig) (*ToznySDKV3, error) { return NewToznySDKV3(ToznySDKConfig{ ClientConfig: e3dbClients.ClientConfig{ ClientID: config.ClientID, @@ -730,6 +734,142 @@ func GetSDKV3(configJSONFilePath string) (*ToznySDKV3, error) { }) } +type LoginActionData = map[string]string + +type TozIDLoginRequest struct { + Username string + Password string + RealmName string + + APIBaseURL string + LoginHandler func(response *identityClient.IdentitySessionRequestResponse) (LoginActionData, error) +} + +//GetSDKV3ForTozIDUser logs in a TozID user and returns the storage client of that user as a ToznySDKV3 +func GetSDKV3ForTozIDUser(login TozIDLoginRequest) (*ToznySDKV3, error) { + if login.APIBaseURL == "" { + login.APIBaseURL = "https://api.e3db.com" + } + username := strings.ToLower(login.Username) + anonConfig := e3dbClients.ClientConfig{ + Host: login.APIBaseURL, + AuthNHost: login.APIBaseURL, + } + ctx := context.Background() + anonymousClient := identityClient.New(anonConfig) + realmInfo, err := anonymousClient.RealmInfo(ctx, login.RealmName) + if err != nil { + // TODO: better error message for failure to get realmInfo + return nil, err + } + noteName, encryptionKeys, signingKeys, err := deriveCredentials(username, login.Password, realmInfo.Name, "") + if err != nil { + return nil, err + } + clientConfig := e3dbClients.ClientConfig{ + Host: login.APIBaseURL, + AuthNHost: login.APIBaseURL, + SigningKeys: signingKeys, + EncryptionKeys: encryptionKeys, + } + idClient := identityClient.New(clientConfig) + loginResponse, err := idClient.InitiateIdentityLogin(ctx, identityClient.IdentityLoginRequest{ + Username: username, + RealmName: login.RealmName, + AppName: "account", + LoginStyle: "api", + }) + if err != nil { + return nil, err + } + sessionResponse, err := idClient.IdentitySessionRequest(ctx, realmInfo.Name, *loginResponse) + if err != nil { + return nil, err + } + // TODO: rework this to support brokered logins. See JS SDK for examples + for { + if sessionResponse.LoginActionType == "fetch" { + break + } + switch sessionResponse.LoginActionType { + case "continue": + data := url.Values{} + request, err := http.NewRequest("POST", sessionResponse.ActionURL, strings.NewReader(data.Encode())) + if err != nil { + return nil, err + } + err = e3dbClients.MakeSignedServiceCall(ctx, &http.Client{}, request, signingKeys, "", &sessionResponse) + if err != nil { + return nil, err + } + default: + if login.LoginHandler == nil { + return nil, fmt.Errorf("A Login handler must be provided for action type %s", sessionResponse.LoginActionType) + } + data, err := login.LoginHandler(sessionResponse) + if err != nil { + return nil, err + } + var reader io.Reader + if sessionResponse.ContentType == "application/x-www-form-urlencoded" { + values := url.Values{} + for key, value := range data { + values.Set(key, value) + } + reader = strings.NewReader(values.Encode()) + } else { + var buf bytes.Buffer + err := json.NewEncoder(&buf).Encode(&data) + if err != nil { + return nil, err + } + reader = &buf + } + request, err := http.NewRequest("POST", sessionResponse.ActionURL, reader) + request.Header.Set("Content-Type", sessionResponse.ContentType) + if err != nil { + return nil, err + } + err = e3dbClients.MakeSignedServiceCall(ctx, &http.Client{}, request, signingKeys, "", &sessionResponse) + if err != nil { + return nil, err + } + if sessionResponse.Message.IsError { + return nil, fmt.Errorf(sessionResponse.Message.Summary) + } + } + } + redirectRequest := identityClient.IdentityLoginRedirectRequest{ + RealmName: realmInfo.Domain, + // The following map values if not present will be set to the empty string and identity service will handle appropriately + SessionCode: sessionResponse.Context["session_code"], + Execution: sessionResponse.Context["execution"], + TabID: sessionResponse.Context["tab_id"], + ClientID: sessionResponse.Context["client_id"], + AuthSessionId: sessionResponse.Context["auth_session_id"], + } + redirect, err := idClient.IdentityLoginRedirect(ctx, redirectRequest) + if err != nil { + return nil, err + } + storage := storageClient.New(clientConfig) + note, err := storage.ReadNoteByName(ctx, noteName, map[string]string{storageClient.TozIDLoginTokenHeader: redirect.AccessToken}) + if err != nil { + return nil, err + } + err = storage.DecryptNote(note) + if err != nil { + return nil, err + } + var config ToznySDKJSONConfig + err = json.Unmarshal([]byte(note.Data["storage"]), &config) + if err != nil { + return nil, err + } + return sdkV3FromConfig(config) + +} + // CreateResponse wraps the value return from the account creation method type RegisterAccountResponse struct { PaperKey string diff --git a/go.mod b/go.mod index 1d596a5..a780447 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,7 @@ require ( github.com/jawher/mow.cli v1.0.4 github.com/mitchellh/go-homedir v1.0.0 github.com/stretchr/testify v1.6.1 // indirect - github.com/tozny/e3db-clients-go v0.0.132 + github.com/tozny/e3db-clients-go v0.0.142-0.20210414182137-dca883891b01 golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 - google.golang.org/appengine v1.6.6 // indirect ) diff --git a/go.sum b/go.sum index d6cfc52..ff921de 100644 --- a/go.sum +++ b/go.sum @@ -38,13 +38,12 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/tozny/e3db-clients-go v0.0.132 h1:0jddtG7GNC5vtVRGMsRROg0a0yjPNK3dAfl3J/PZm8A= -github.com/tozny/e3db-clients-go v0.0.132/go.mod h1:AF1aBlYWYm0c8gUTd74FXbVOiH6HLF/TKUAwXSOlgTg= +github.com/tozny/e3db-clients-go v0.0.142-0.20210414182137-dca883891b01 h1:+/XdmMfT0anY2lbkjmJwCKJYMQNS2xoqm9dCpuqNmeQ= +github.com/tozny/e3db-clients-go v0.0.142-0.20210414182137-dca883891b01/go.mod h1:fmlw8jD3qHE3l/gcUSDvytj+Kr0nlADLIrPxOdi7EoE= github.com/tozny/utils-go v0.0.35 h1:gPvhlQ8QCoLBUjIx1COfYy6o4dfSM8Lrh+2FV9Ask+g= github.com/tozny/utils-go v0.0.35/go.mod h1:SHi9wnpPEEzAxbwcBhRd+jW32r+gY6S+AcWweuGytRw= golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -68,9 +67,7 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/identity.go b/identity.go index 148ebde..b611a75 100644 --- a/identity.go +++ b/identity.go @@ -159,7 +159,11 @@ func (r *Realm) Register(username, password, registrationToken, email, firstName } func (i *Identity) deriveCredentails(password string, nameSalt string) (string, e3dbClients.EncryptionKeys, e3dbClients.SigningKeys, error) { - nameSeed := fmt.Sprintf("%s@realm:%s", i.Username, i.Realm.Name) + return deriveCredentials(i.Username, password, i.Realm.Name, nameSalt) +} + +func deriveCredentials(username string, password string, realmName string, nameSalt string) (string, e3dbClients.EncryptionKeys, e3dbClients.SigningKeys, error) { + nameSeed := fmt.Sprintf("%s@realm:%s", username, realmName) if nameSalt != "" { nameSeed = fmt.Sprintf("%s:%s", nameSalt, nameSeed) } diff --git a/identity_test.go b/identity_test.go new file mode 100644 index 0000000..861b82d --- /dev/null +++ b/identity_test.go @@ -0,0 +1,31 @@ +package e3db + +import ( + "fmt" + "github.com/tozny/e3db-clients-go/identityClient" + "testing" +) + +func TestToznySDKV3_Login(t *testing.T) { + request := TozIDLoginRequest{ + Username: "", + Password: "", + RealmName: "", + APIBaseURL: "https://api.e3db.com", + LoginHandler: mfaHandler, + } + sdk, err := GetSDKV3ForTozIDUser(request) + if err != nil { + t.Fatal("Abort", err) + } + fmt.Printf("%v", sdk) +} + +func mfaHandler(sessionResponse *identityClient.IdentitySessionRequestResponse) (LoginActionData, error) { + if sessionResponse.LoginActionType == "login-totp" { + totpValue := make(map[string]string) + totpValue["otp"] = "" + return totpValue, nil + } + return nil, fmt.Errorf("mfaHandler cannot support \"%s\" action types", sessionResponse.LoginActionType) +} \ No newline at end of file