Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds 3rd party login functionality to retrieve a V3SDK #56

Merged
merged 5 commits into from
May 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,7 @@ type ToznySDKV3 struct {
APIEndpoint string
// Tozny server defined globally unique id for this Client.
ClientID string
config e3dbClients.ClientConfig
}

// ToznySDKConfig wraps parameters needed to configure a ToznySDK
Expand Down Expand Up @@ -686,6 +687,7 @@ func NewToznySDKV3(config ToznySDKConfig) (*ToznySDKV3, error) {
AccountPassword: config.AccountPassword,
APIEndpoint: config.APIEndpoint,
ClientID: config.ClientID,
config: config.ClientConfig,
}, nil
}

Expand All @@ -696,6 +698,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,
Expand Down Expand Up @@ -730,6 +736,145 @@ func GetSDKV3(configJSONFilePath string) (*ToznySDKV3, error) {
})
}

type LoginActionData = map[string]string

type IdentitySessionIntermediateResponse = identityClient.IdentitySessionRequestResponse

// TozIDLoginRequest is used to login to a TozID account to get a ToznySDKV3 or active TozID session (future plan)
type TozIDLoginRequest struct {
Username string
Password string
RealmName string
APIBaseURL string
LoginHandler func(response *IdentitySessionIntermediateResponse) (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"
} else {
login.APIBaseURL = strings.TrimSuffix(strings.TrimSpace(login.APIBaseURL), "/")
}
username := strings.ToLower(login.Username)
anonConfig := e3dbClients.ClientConfig{
Host: login.APIBaseURL,
AuthNHost: login.APIBaseURL,
}
ctx := context.Background()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be global?

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, fmt.Errorf("GetSDKV3ForTozIDUser: failed to get realm infor with error %w", err)
}
noteName, encryptionKeys, signingKeys, err := e3dbClients.DeriveIdentityCredentials(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)
if err != nil {
return nil, err
}
request.Header.Set("Content-Type", sessionResponse.ContentType)
err = e3dbClients.MakeSignedServiceCall(ctx, &http.Client{}, request, signingKeys, "", &sessionResponse)
if err != nil {
return nil, err
} else 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
Expand Down Expand Up @@ -781,6 +926,27 @@ type ClientConfig struct {
PrivateSigningKey string `json:"private_signing_key"`
}

// StoreConfigFile stores a ToznySDKV3 config file at the specified path, returning an error if any
func (c *ToznySDKV3) StoreConfigFile(path string) error {
config := ToznySDKJSONConfig{
ConfigFile: ConfigFile{
Version: 2,
APIBaseURL: c.APIEndpoint,
APIKeyID: c.config.APIKey,
APISecret: c.config.APISecret,
ClientID: c.config.ClientID,
ClientEmail: "",
PublicKey: c.config.EncryptionKeys.Public.Material,
PrivateKey: c.config.EncryptionKeys.Private.Material,
},
PublicSigningKey: c.config.SigningKeys.Public.Material,
PrivateSigningKey: c.config.SigningKeys.Private.Material,
AccountUsername: c.AccountUsername,
AccountPassword: c.AccountPassword,
}
return saveJson(path, config)
}

// Register attempts to create a valid TozStore account returning the root client config for the created account and error (if any).
func (c *ToznySDKV3) Register(ctx context.Context, name string, email string, password string, apiURL string) (RegisterAccountResponse, error) {
if apiURL == "" {
Expand Down
20 changes: 14 additions & 6 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ func loadConfig(configPath string) (*ClientOpts, error) {
}, nil
}

func saveConfig(configPath string, opts *ClientOpts) error {
func saveJson(configPath string, obj interface{}) error {
configFullPath, err := homedir.Expand(configPath)
if err != nil {
return err
Expand All @@ -126,6 +126,14 @@ func saveConfig(configPath string, opts *ClientOpts) error {
}
defer configFd.Close()

if err = json.NewEncoder(configFd).Encode(&obj); err != nil {
return err
}

return nil
}

func saveConfig(configPath string, opts *ClientOpts) error {
configObj := configFile{
Version: 1,
ClientID: opts.ClientID,
Expand All @@ -137,11 +145,7 @@ func saveConfig(configPath string, opts *ClientOpts) error {
PrivateKey: encodePrivateKey(opts.PrivateKey),
}

if err = json.NewEncoder(configFd).Encode(&configObj); err != nil {
return err
}

return nil
return saveJson(configPath, configObj)
}

func fileExists(name string) (bool, error) {
Expand Down Expand Up @@ -214,3 +218,7 @@ func LoadConfigFile(configPath string) (ToznySDKJSONConfig, error) {
}
return config, nil
}

func StoreConfigFile(configPath string, config ToznySDKJSONConfig) error {
return saveJson(configPath, config)
}
108 changes: 54 additions & 54 deletions example_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,77 +4,77 @@ package e3db_test
// this code, it will share text with Tozny in an end-to-end encrypted manner.

import (
"context"
"fmt"
"github.com/tozny/e3db-go/v2"
"log"
"context"
"fmt"
"github.com/tozny/e3db-go/v2"
"log"
)

func chk(err error) {
if err != nil {
log.Fatal(err)
}
if err != nil {
log.Fatal(err)
}
}

func printRecords(recordType string) {
client, err := e3db.GetDefaultClient()
chk(err)
client, err := e3db.GetDefaultClient()
chk(err)

// Query for all records and print them out
query := e3db.Q{} // queries all records
if recordType != "" {
query = e3db.Q{ContentTypes: []string{recordType}}
}
cursor := client.Query(context.Background(), query)
for {
record, err := cursor.Next()
if err == e3db.Done {
break
} else if err != nil {
chk(err)
}
fmt.Println("\t" + record.Meta.RecordID + " " + record.Meta.Type)
}
// Query for all records and print them out
query := e3db.Q{} // queries all records
if recordType != "" {
query = e3db.Q{ContentTypes: []string{recordType}}
}
cursor := client.Query(context.Background(), query)
for {
record, err := cursor.Next()
if err == e3db.Done {
break
} else if err != nil {
chk(err)
}
fmt.Println("\t" + record.Meta.RecordID + " " + record.Meta.Type)
}
}

func Example() {

// Accessing the default profile.
// You must run e3db RegisterClient before this will work:
client, err := e3db.GetDefaultClient()
chk(err)
// Accessing the default profile.
// You must run e3db RegisterClient before this will work:
client, err := e3db.GetDefaultClient()
chk(err)

fmt.Println("Current list of records:")
printRecords("")
fmt.Println("Current list of records:")
printRecords("")

// Create a new "feedback" record; this is the type the CLI uses
feedbackData := make(map[string]string)
feedbackData["comment"] = "This is some example feedback!"
feedbackData["interface"] = "Go Example Code"
record, err := client.Write(context.Background(), "feedback", feedbackData, nil)
chk(err)
// Create a new "feedback" record; this is the type the CLI uses
feedbackData := make(map[string]string)
feedbackData["comment"] = "This is some example feedback!"
feedbackData["interface"] = "Go Example Code"
record, err := client.Write(context.Background(), "feedback", feedbackData, nil)
chk(err)

// Read back the feedback we just put into the database
newFeedbackRecord, err := client.Read(context.Background(), record.Meta.RecordID)
chk(err)
fmt.Println("Read record id " + record.Meta.RecordID + ": " + newFeedbackRecord.Data["comment"])
// Read back the feedback we just put into the database
newFeedbackRecord, err := client.Read(context.Background(), record.Meta.RecordID)
chk(err)
fmt.Println("Read record id " + record.Meta.RecordID + ": " + newFeedbackRecord.Data["comment"])

// Fetch the Tozny feedback email address public key and client ID
feedbackClient, err := client.GetClientInfo(context.Background(), "db1744b9-3fb6-4458-a291-0bc677dba08b")
chk(err)
// Fetch the Tozny feedback email address public key and client ID
feedbackClient, err := client.GetClientInfo(context.Background(), "db1744b9-3fb6-4458-a291-0bc677dba08b")
chk(err)

// Share all "feedback" records with that user ID.
err = client.Share(context.Background(), "feedback", feedbackClient.ClientID)
chk(err)
// Share all "feedback" records with that user ID.
err = client.Share(context.Background(), "feedback", feedbackClient.ClientID)
chk(err)

fmt.Println("Current list of records after adding:")
printRecords("feedback")
fmt.Println("Current list of records after adding:")
printRecords("feedback")

// Delete the record we just created to keep things tidy.
// Comment out this line if you want to keep it
err = client.Delete(context.Background(), record.Meta.RecordID, record.Meta.Version)
chk(err)
// Delete the record we just created to keep things tidy.
// Comment out this line if you want to keep it
err = client.Delete(context.Background(), record.Meta.RecordID, record.Meta.Version)
chk(err)

fmt.Println("Current list of records after deleting:")
printRecords("feedback")
fmt.Println("Current list of records after deleting:")
printRecords("feedback")
}
7 changes: 3 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
module github.com/tozny/e3db-go/v2

go 1.13
go 1.15

require (
github.com/google/uuid v1.1.0
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
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4
github.com/tozny/e3db-clients-go v0.0.144
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
google.golang.org/appengine v1.6.6 // indirect
)
Loading