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
24
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
33 return apperror.NewAbortError(
34 fmt.Errorf("device account not found (devicelogin=`%v`): %w", username, err),
35 http.StatusBadRequest)
36 }
37
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
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
50 return apperror.NewAbortError(
51 fmt.Errorf("user profile not found (subject=`%v`): %w", username, err),
52 http.StatusBadRequest)
53 }
54
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
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
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
119
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