github.com/jcarley/cli@v0.0.0-20180201210820-966d90434c30/lib/auth/signin.go (about)

     1  package auth
     2  
     3  import (
     4  	"crypto"
     5  	"crypto/rand"
     6  	"crypto/sha256"
     7  	"crypto/x509"
     8  	"encoding/base64"
     9  	"encoding/json"
    10  	"encoding/pem"
    11  	"errors"
    12  	"fmt"
    13  	"io/ioutil"
    14  
    15  	"github.com/daticahealth/cli/config"
    16  	"github.com/daticahealth/cli/models"
    17  )
    18  
    19  // Signin signs in a user and returns the representative user model. If an
    20  // error occurs, nil is returned for the user and the error field is populated.
    21  func (a *SAuth) Signin() (*models.User, error) {
    22  	// if we're already signed in with a valid session, don't sign in again
    23  	if user, err := a.Verify(); err == nil {
    24  		return user, nil
    25  	}
    26  	f := a.signInWithKey
    27  	if a.Settings.PrivateKeyPath == "" {
    28  		f = a.signInWithCredentials
    29  	}
    30  	signinResp, err := f()
    31  	if err != nil {
    32  		return nil, err
    33  	}
    34  
    35  	var user *models.User
    36  
    37  	if signinResp.MFAID != "" {
    38  		user, err = a.mfaSignin(signinResp.MFAID, signinResp.MFAPreferredMode)
    39  		if err != nil {
    40  			return nil, err
    41  		}
    42  	} else {
    43  		user = signinResp.toUser()
    44  	}
    45  
    46  	a.Settings.UsersID = user.UsersID
    47  	a.Settings.Email = user.Email
    48  	a.Settings.SessionToken = user.SessionToken
    49  
    50  	config.SaveSettings(a.Settings)
    51  
    52  	return user, nil
    53  }
    54  
    55  type signinResponse struct {
    56  	ID               string `json:"id"`
    57  	Name             string `json:"name"`
    58  	Email            string `json:"email"`
    59  	SessionToken     string `json:"sessionToken"`
    60  	MFAID            string `json:"mfaID"`
    61  	MFAPreferredMode string `json:"mfaPreferredType"`
    62  }
    63  
    64  func (sr *signinResponse) toUser() *models.User {
    65  	return &models.User{
    66  		UsersID:      sr.ID,
    67  		Email:        sr.Email,
    68  		SessionToken: sr.SessionToken,
    69  	}
    70  }
    71  
    72  func (a *SAuth) signInWithCredentials() (*signinResponse, error) {
    73  	login := models.Login{
    74  		Identifier: a.Settings.Email,
    75  		Password:   a.Settings.Password,
    76  	}
    77  	if a.Settings.Email == "" || a.Settings.Password == "" {
    78  		email, password, err := a.Prompts.EmailPassword(a.Settings.Email, a.Settings.Password)
    79  		if err != nil {
    80  			return nil, err
    81  		}
    82  		login = models.Login{
    83  			Identifier: email,
    84  			Password:   password,
    85  		}
    86  	}
    87  
    88  	b, err := json.Marshal(login)
    89  	if err != nil {
    90  		return nil, err
    91  	}
    92  	headers := a.Settings.HTTPManager.GetHeaders(a.Settings.SessionToken, a.Settings.Version, a.Settings.Pod, a.Settings.UsersID)
    93  	resp, statusCode, err := a.Settings.HTTPManager.Post(b, fmt.Sprintf("%s%s/auth/signin", a.Settings.AuthHost, a.Settings.AuthHostVersion), headers)
    94  	if err != nil {
    95  		return nil, err
    96  	}
    97  	signinResp := &signinResponse{}
    98  	return signinResp, a.Settings.HTTPManager.ConvertResp(resp, statusCode, signinResp)
    99  }
   100  
   101  func (a *SAuth) signInWithKey() (*signinResponse, error) {
   102  	body := struct {
   103  		PublicKey string `json:"publicKey"`
   104  		Signature string `json:"signature"`
   105  	}{}
   106  
   107  	bytes, err := ioutil.ReadFile(a.Settings.PrivateKeyPath)
   108  	if err != nil {
   109  		return nil, err
   110  	}
   111  	block, _ := pem.Decode(bytes)
   112  	if block == nil {
   113  		return nil, errors.New("Private key is not PEM-encoded")
   114  	}
   115  	bytes = block.Bytes
   116  	if x509.IsEncryptedPEMBlock(block) {
   117  		passphrase := a.Prompts.KeyPassphrase(a.Settings.PrivateKeyPath)
   118  		bytes, err = x509.DecryptPEMBlock(block, []byte(passphrase))
   119  		if err != nil {
   120  			return nil, err
   121  		}
   122  	}
   123  	privateKey, err := x509.ParsePKCS1PrivateKey(bytes)
   124  	if err != nil {
   125  		return nil, err
   126  	}
   127  	publicKey, err := ioutil.ReadFile(a.Settings.PrivateKeyPath + ".pub")
   128  	if err != nil {
   129  		return nil, err
   130  	}
   131  	body.PublicKey = string(publicKey)
   132  
   133  	headers := a.Settings.HTTPManager.GetHeaders(a.Settings.SessionToken, a.Settings.Version, a.Settings.Pod, a.Settings.UsersID)
   134  	message := fmt.Sprintf("%s&%s", headers["X-Request-Nonce"][0], headers["X-Request-Timestamp"][0])
   135  	hashedMessage := sha256.Sum256([]byte(message))
   136  	signature, err := privateKey.Sign(rand.Reader, hashedMessage[:], crypto.SHA256)
   137  	if err != nil {
   138  		return nil, err
   139  	}
   140  	body.Signature = base64.StdEncoding.EncodeToString(signature)
   141  
   142  	b, err := json.Marshal(body)
   143  	if err != nil {
   144  		return nil, err
   145  	}
   146  	resp, statusCode, err := a.Settings.HTTPManager.Post(b, fmt.Sprintf("%s%s/auth/signin/key", a.Settings.AuthHost, a.Settings.AuthHostVersion), headers)
   147  	if err != nil {
   148  		return nil, err
   149  	}
   150  	signinResp := &signinResponse{}
   151  	return signinResp, a.Settings.HTTPManager.ConvertResp(resp, statusCode, signinResp)
   152  }
   153  
   154  func (a *SAuth) mfaSignin(mfaID string, preferredMode string) (*models.User, error) {
   155  	token := a.Prompts.OTP(preferredMode)
   156  	headers := a.Settings.HTTPManager.GetHeaders(a.Settings.SessionToken, a.Settings.Version, a.Settings.Pod, a.Settings.UsersID)
   157  	b, err := json.Marshal(struct {
   158  		OTP string `json:"otp"`
   159  	}{OTP: token})
   160  	if err != nil {
   161  		return nil, err
   162  	}
   163  	resp, statusCode, err := a.Settings.HTTPManager.Post(b, fmt.Sprintf("%s%s/auth/signin/mfa/%s", a.Settings.AuthHost, a.Settings.AuthHostVersion, mfaID), headers)
   164  	user := &models.User{}
   165  	err = a.Settings.HTTPManager.ConvertResp(resp, statusCode, user)
   166  	if err != nil {
   167  		return nil, err
   168  	}
   169  	return user, err
   170  }
   171  
   172  // Signout signs out a user by their session token.
   173  func (a *SAuth) Signout() error {
   174  	headers := a.Settings.HTTPManager.GetHeaders(a.Settings.SessionToken, a.Settings.Version, a.Settings.Pod, a.Settings.UsersID)
   175  	resp, statusCode, err := a.Settings.HTTPManager.Delete(nil, fmt.Sprintf("%s%s/auth/signout", a.Settings.AuthHost, a.Settings.AuthHostVersion), headers)
   176  	if err != nil {
   177  		return err
   178  	}
   179  	return a.Settings.HTTPManager.ConvertResp(resp, statusCode, nil)
   180  }
   181  
   182  // Verify verifies if a given session token is still valid or not. If it is
   183  // valid, the returned error will be nil.
   184  func (a *SAuth) Verify() (*models.User, error) {
   185  	headers := a.Settings.HTTPManager.GetHeaders(a.Settings.SessionToken, a.Settings.Version, a.Settings.Pod, a.Settings.UsersID)
   186  	resp, statusCode, err := a.Settings.HTTPManager.Get(nil, fmt.Sprintf("%s%s/auth/verify", a.Settings.AuthHost, a.Settings.AuthHostVersion), headers)
   187  	if err != nil {
   188  		return nil, err
   189  	}
   190  	var user models.User
   191  	err = a.Settings.HTTPManager.ConvertResp(resp, statusCode, &user)
   192  	if err != nil {
   193  		return nil, err
   194  	}
   195  	a.Settings.UsersID = user.UsersID
   196  	user.SessionToken = a.Settings.SessionToken
   197  	return &user, nil
   198  }