...

Source file src/edge-infra.dev/pkg/edge/iam/device/method_local.go

Documentation: edge-infra.dev/pkg/edge/iam/device

     1  package device
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"net/http"
     7  	"net/url"
     8  	"strings"
     9  	"time"
    10  
    11  	"golang.org/x/crypto/bcrypt"
    12  
    13  	"edge-infra.dev/pkg/edge/iam/apperror"
    14  	"edge-infra.dev/pkg/edge/iam/config"
    15  	"edge-infra.dev/pkg/edge/iam/util"
    16  
    17  	"github.com/gin-gonic/gin"
    18  	"github.com/golang-jwt/jwt"
    19  	"github.com/google/uuid"
    20  )
    21  
    22  func (am *AuthMethod) localLogin(c *gin.Context, username, password, reason string) error {
    23  	// todo: do we have separate metric for local?
    24  	// grab the device account which has the subject
    25  	acc, err := am.storage.GetDeviceAccount(c, username)
    26  	if err != nil {
    27  		return apperror.NewAbortError(
    28  			fmt.Errorf("failed to get a device account: %w", err),
    29  			http.StatusInternalServerError)
    30  	}
    31  	if acc == nil {
    32  		// there is no profile for this subject in db
    33  		return apperror.NewAbortError(
    34  			fmt.Errorf("device account not found (devicelogin=`%v`): %w", username, err),
    35  			http.StatusBadRequest)
    36  	}
    37  	// todo: get this 30 from config
    38  	if acc.NumOfWrongAttempts >= config.GetDeviceRetryThreshold() && time.Now().Unix()-acc.LastUpdated <= int64(config.RetriesExceededTimeout()) {
    39  		return apperror.NewStatusError(fmt.Errorf("many incorrect attempts, must wait for 30 sec"), http.StatusUnauthorized)
    40  	}
    41  	// now grab the profile by that subject
    42  	userProfile, err := am.profileStorage.GetIdentityProfile(c, acc.Subject)
    43  	if err != nil {
    44  		return apperror.NewAbortError(
    45  			fmt.Errorf("failed to get a profile for the device account: %w", err),
    46  			http.StatusInternalServerError)
    47  	}
    48  	if userProfile == nil {
    49  		// there is no profile for this subject in db
    50  		return apperror.NewAbortError(
    51  			fmt.Errorf("user profile not found (subject=`%v`): %w", username, err),
    52  			http.StatusBadRequest)
    53  	}
    54  	// check the password
    55  	if compareErr := bcrypt.CompareHashAndPassword([]byte(acc.Hash), []byte(password)); compareErr != nil {
    56  		acc.NumOfWrongAttempts++
    57  		acc.LastUpdated = time.Now().Unix()
    58  		err := am.storage.SaveDeviceAccount(c, *acc)
    59  		// abort with internal server error only when online with database.
    60  		if err != nil && !am.profileStorage.IsOffline() {
    61  			return apperror.NewAbortError(fmt.Errorf("failed to update the device account during login failure: %w", err),
    62  				http.StatusInternalServerError)
    63  		}
    64  		return apperror.NewStatusError(compareErr, http.StatusUnauthorized)
    65  	}
    66  	if err := IsRefreshTokenValid(acc.RefreshToken); err != nil {
    67  		if errors.Is(err, ErrExpiredRefreshToken) {
    68  			return apperror.NewStatusError(err, http.StatusUnauthorized)
    69  		}
    70  		return apperror.NewAbortError(err, http.StatusUnauthorized)
    71  	}
    72  	// todo: make sure we ave all fields we need here
    73  	session, _ := am.sessionStore.Get(c.Request, "oauth2")
    74  
    75  	session.Values["method"] = "device"
    76  	session.Values["reason"] = reason
    77  	continuation := uuid.New().String()
    78  	session.Values["continuation"] = continuation
    79  
    80  	am.setProfileOnSession(session, userProfile)
    81  
    82  	if err = session.Save(c.Request, c.Writer); err != nil {
    83  		return apperror.NewAbortError(
    84  			fmt.Errorf("failed to save cookie session: %w", err),
    85  			http.StatusInternalServerError)
    86  	}
    87  
    88  	return util.WriteJSON(c.Writer, http.StatusOK, gin.H{
    89  		"challenge": continuation,
    90  	})
    91  }
    92  
    93  func ExchangeRefreshToken(accRefreshToken string) error {
    94  	client := &http.Client{}
    95  	data := url.Values{}
    96  	data.Add("refreshToken", accRefreshToken)
    97  	payload := strings.NewReader(data.Encode())
    98  	req, err := http.NewRequest("POST", config.DeviceBaseURL()+"/refresh", payload)
    99  
   100  	if err != nil {
   101  		return err
   102  	}
   103  	req.Header.Add("nep-organization", config.OrganizationID())
   104  	req.Header.Add("nep-enterprise-unit", config.SiteID())
   105  	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
   106  
   107  	res, err := client.Do(req)
   108  	if err != nil {
   109  		return err
   110  	}
   111  	if res.StatusCode != 200 {
   112  		return ErrExchangingRefreshToken
   113  	}
   114  	return nil
   115  }
   116  
   117  func IsRefreshTokenValid(accRefreshToken string) error {
   118  	// ToDo: parse verified once we have JWKS
   119  	// cache the JWKS for local usage.
   120  	refreshToken, _, err := new(jwt.Parser).ParseUnverified(accRefreshToken, &jwt.MapClaims{})
   121  	if err != nil {
   122  		return err
   123  	}
   124  	refreshClaims, ok := refreshToken.Claims.(*jwt.MapClaims)
   125  	if !ok {
   126  		return fmt.Errorf("invalid claims in refresh token")
   127  	}
   128  	exp, ok := (*refreshClaims)["exp"].(float64)
   129  	if !ok {
   130  		return fmt.Errorf("invalid expiration claim in refresh token")
   131  	}
   132  	expirationTime := time.Unix(int64(exp), 0)
   133  	if !time.Now().Before(expirationTime) {
   134  		return ErrExpiredRefreshToken
   135  	}
   136  	return nil
   137  }
   138  

View as plain text