package device import ( "errors" "fmt" "net/http" "net/url" "strings" "time" "golang.org/x/crypto/bcrypt" "edge-infra.dev/pkg/edge/iam/apperror" "edge-infra.dev/pkg/edge/iam/config" "edge-infra.dev/pkg/edge/iam/util" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt" "github.com/google/uuid" ) func (am *AuthMethod) localLogin(c *gin.Context, username, password, reason string) error { // todo: do we have separate metric for local? // grab the device account which has the subject acc, err := am.storage.GetDeviceAccount(c, username) if err != nil { return apperror.NewAbortError( fmt.Errorf("failed to get a device account: %w", err), http.StatusInternalServerError) } if acc == nil { // there is no profile for this subject in db return apperror.NewAbortError( fmt.Errorf("device account not found (devicelogin=`%v`): %w", username, err), http.StatusBadRequest) } // todo: get this 30 from config if acc.NumOfWrongAttempts >= config.GetDeviceRetryThreshold() && time.Now().Unix()-acc.LastUpdated <= int64(config.RetriesExceededTimeout()) { return apperror.NewStatusError(fmt.Errorf("many incorrect attempts, must wait for 30 sec"), http.StatusUnauthorized) } // now grab the profile by that subject userProfile, err := am.profileStorage.GetIdentityProfile(c, acc.Subject) if err != nil { return apperror.NewAbortError( fmt.Errorf("failed to get a profile for the device account: %w", err), http.StatusInternalServerError) } if userProfile == nil { // there is no profile for this subject in db return apperror.NewAbortError( fmt.Errorf("user profile not found (subject=`%v`): %w", username, err), http.StatusBadRequest) } // check the password if compareErr := bcrypt.CompareHashAndPassword([]byte(acc.Hash), []byte(password)); compareErr != nil { acc.NumOfWrongAttempts++ acc.LastUpdated = time.Now().Unix() err := am.storage.SaveDeviceAccount(c, *acc) // abort with internal server error only when online with database. if err != nil && !am.profileStorage.IsOffline() { return apperror.NewAbortError(fmt.Errorf("failed to update the device account during login failure: %w", err), http.StatusInternalServerError) } return apperror.NewStatusError(compareErr, http.StatusUnauthorized) } if err := IsRefreshTokenValid(acc.RefreshToken); err != nil { if errors.Is(err, ErrExpiredRefreshToken) { return apperror.NewStatusError(err, http.StatusUnauthorized) } return apperror.NewAbortError(err, http.StatusUnauthorized) } // todo: make sure we ave all fields we need here session, _ := am.sessionStore.Get(c.Request, "oauth2") session.Values["method"] = "device" session.Values["reason"] = reason continuation := uuid.New().String() session.Values["continuation"] = continuation am.setProfileOnSession(session, userProfile) if err = session.Save(c.Request, c.Writer); err != nil { return apperror.NewAbortError( fmt.Errorf("failed to save cookie session: %w", err), http.StatusInternalServerError) } return util.WriteJSON(c.Writer, http.StatusOK, gin.H{ "challenge": continuation, }) } func ExchangeRefreshToken(accRefreshToken string) error { client := &http.Client{} data := url.Values{} data.Add("refreshToken", accRefreshToken) payload := strings.NewReader(data.Encode()) req, err := http.NewRequest("POST", config.DeviceBaseURL()+"/refresh", payload) if err != nil { return err } req.Header.Add("nep-organization", config.OrganizationID()) req.Header.Add("nep-enterprise-unit", config.SiteID()) req.Header.Add("Content-Type", "application/x-www-form-urlencoded") res, err := client.Do(req) if err != nil { return err } if res.StatusCode != 200 { return ErrExchangingRefreshToken } return nil } func IsRefreshTokenValid(accRefreshToken string) error { // ToDo: parse verified once we have JWKS // cache the JWKS for local usage. refreshToken, _, err := new(jwt.Parser).ParseUnverified(accRefreshToken, &jwt.MapClaims{}) if err != nil { return err } refreshClaims, ok := refreshToken.Claims.(*jwt.MapClaims) if !ok { return fmt.Errorf("invalid claims in refresh token") } exp, ok := (*refreshClaims)["exp"].(float64) if !ok { return fmt.Errorf("invalid expiration claim in refresh token") } expirationTime := time.Unix(int64(exp), 0) if !time.Now().Before(expirationTime) { return ErrExpiredRefreshToken } return nil }