-
Notifications
You must be signed in to change notification settings - Fork 0
/
auth.go
295 lines (247 loc) · 7.98 KB
/
auth.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
package auth
import (
"net/http"
"os"
"time"
"github.com/jinzhu/gorm"
"github.com/si9ma/KillOJ-backend/wrap"
"github.com/markbates/goth/gothic"
"github.com/si9ma/KillOJ-common/mysql"
"github.com/si9ma/KillOJ-backend/kerror"
"gopkg.in/hlandau/passlib.v1"
"github.com/si9ma/KillOJ-common/utils"
"github.com/si9ma/KillOJ-common/log"
"go.uber.org/zap"
"github.com/opentracing/opentracing-go"
"github.com/si9ma/KillOJ-backend/gbl"
jwt "github.com/appleboy/gin-jwt"
"github.com/gin-gonic/gin"
"github.com/si9ma/KillOJ-common/constants"
"github.com/si9ma/KillOJ-common/model"
otgrom "github.com/smacker/opentracing-gorm"
)
type login struct {
Name string `json:"name" binding:"required,max=100"` // user name or email
Password string `json:"password" binding:"required,min=6,max=30"`
GithubID string `json:"github_id"` // user name or email
GithubName string `json:"github_name"`
}
const (
NoUseGinJwtError = "NoUseGinJwtError"
)
var (
AuthGroup *gin.RouterGroup // auth group
jwtMiddleware *jwt.GinJWTMiddleware // jwt middleware
)
func SetupAuth(r *gin.Engine) {
var err error
jwtSecret := os.Getenv(constants.EnvJWTSecret)
if jwtSecret == "" {
log.Bg().Fatal("Please Define environment", zap.String("env", constants.EnvJWTSecret))
return
}
// the jwt middleware
jwtMiddleware, err = jwt.New(&jwt.GinJWTMiddleware{
Realm: constants.ProjectName,
Key: []byte(jwtSecret),
Timeout: time.Hour,
MaxRefresh: time.Hour * 24 * 7, // 7 day
IdentityKey: constants.JwtIdentityKey,
PayloadFunc: func(data interface{}) jwt.MapClaims {
if v, ok := data.(model.User); ok {
return jwt.MapClaims{
constants.JwtIdentityKey: v.ID,
"role": v.Role,
}
}
return jwt.MapClaims{}
},
IdentityHandler: func(c *gin.Context) interface{} {
claims := jwt.ExtractClaims(c)
// todo There may be a bug here
userId := claims[constants.JwtIdentityKey].(float64)
role := claims["role"].(float64)
return model.User{
ID: int(userId),
Role: int(role),
}
},
Authenticator: authenticate,
Authorizator: func(data interface{}, c *gin.Context) bool {
return true
},
Unauthorized: func(c *gin.Context, code int, message string) {
ctx := c.Request.Context()
// clear goauth session
if err := gothic.Logout(c.Writer, c.Request); err != nil {
log.Bg().Error("goauth logout fail", zap.Error(err))
}
if val, ok := c.Get(NoUseGinJwtError); ok {
if is, ok := val.(bool); ok && is {
// use custom error handler
log.For(ctx).Info("don't use gin-jwt error")
return
}
}
// use gin-jwt error
c.JSON(code, gin.H{
"error": map[string]interface{}{
"code": kerror.ErrUnauthorizedGeneral.Code,
"message": message,
},
})
},
// TokenLookup is a string in the form of "<source>:<name>" that is used
// to extract token from the request.
// Optional. Default value "header:Authorization".
// Possible values:
// - "header:<name>"
// - "query:<name>"
// - "cookie:<name>"
// - "param:<name>"
TokenLookup: "header: Authorization, query: token, cookie: jwt",
// TokenLookup: "query:token",
// TokenLookup: "cookie:token",
// TokenHeadName is a string in the header. Default value is "Bearer"
TokenHeadName: "Bearer",
// TimeFunc provides the current time. You can override it to use another time value.
// This is useful for testing or if your server uses a different time zone than your tokens.
TimeFunc: time.Now,
})
if err != nil {
log.Bg().Fatal("init jwt fail", zap.Error(err))
return
}
r.POST("/login", jwtMiddleware.LoginHandler)
// auth group
AuthGroup = r.Group("")
AuthGroup.Use(jwtMiddleware.MiddlewareFunc())
AuthGroup.GET("/logout", func(c *gin.Context) {
if err := gothic.Logout(c.Writer, c.Request); err != nil {
log.Bg().Error("goauth logout fail", zap.Error(err))
}
c.JSON(http.StatusOK, gin.H{
"result": "success",
})
})
// Refresh time can be longer than token timeout
AuthGroup.GET("/auth/refresh_token", jwtMiddleware.RefreshHandler)
}
func authenticate(c *gin.Context) (interface{}, error) {
c.Set(NoUseGinJwtError, true) // use custom error handle
if c.Request.RequestURI == "/login" {
// password authenticate
return passwdAuthenticate(c)
} else {
return thirdAuthenticate(c)
}
}
func passwdAuthenticate(c *gin.Context) (interface{}, error) {
var (
loginVals login
err error
)
parrentCtx := c.Request.Context()
span, ctx := opentracing.StartSpanFromContext(parrentCtx, "authenticate")
defer span.Finish()
db := otgrom.SetSpanToGorm(ctx, gbl.DB)
// bind
if !wrap.ShouldBind(c, &loginVals, false) {
return "", jwt.ErrMissingLoginValues
}
userName := loginVals.Name
password := loginVals.Password
// query db
user := model.User{}
if utils.CheckEmail(userName) {
// username is email
err = db.Where("email = ?", loginVals.Name).First(&user).Error
} else {
// username is user name
err = db.Where("name = ?", loginVals.Name).First(&user).Error
}
if res := mysql.ErrorHandleAndLog(c, err, false,
"get user by username(email/name)", loginVals.Name); res == mysql.NotFound {
log.For(ctx).Error("user not exist", zap.String("username", loginVals.Name))
_ = c.Error(err).SetType(gin.ErrorTypePublic).
SetMeta(kerror.ErrUserNotExist.WithArgs(loginVals.Name))
return "", jwt.ErrFailedAuthentication
} else if res != mysql.Success {
return "", jwt.ErrFailedAuthentication
}
// verify password
if newVal, err := passlib.Verify(password, user.EncryptedPasswd); err != nil {
log.For(ctx).Error("verify password fail", zap.String("username", loginVals.Name))
_ = c.Error(err).SetType(gin.ErrorTypePublic).
SetMeta(kerror.ErrPasswordWrong)
return "", jwt.ErrFailedAuthentication
} else {
// The context has decided, as per its policy, that
// the hash which was used to validate the password
// should be changed. It has upgraded the hash using
// the verified password.
// refer : https://github.com/hlandau/passlib
if newVal != "" {
if err := db.Model(&user).Update("passwd", newVal).Error; err != nil {
log.For(ctx).Error("renew password fail", zap.Error(err),
zap.Int("id", user.ID))
_ = c.Error(err).SetType(gin.ErrorTypePrivate)
return "", jwt.ErrFailedAuthentication
}
log.For(ctx).Info("renew password success", zap.Int("id", user.ID))
}
// if github name and github id not empty, binding to user
// todo There may be a bug here
if loginVals.GithubName != "" && loginVals.GithubID != "" {
if err := bindUser(c, user, loginVals.GithubName, loginVals.GithubID); err != nil {
return "", jwt.ErrFailedAuthentication
}
}
}
log.For(ctx).Info("authenticate user success", zap.String("username", loginVals.Name))
return user, nil
}
func bindUser(c *gin.Context, oldUser model.User, githubName, githubID string) error {
ctx := c.Request.Context()
db := otgrom.SetSpanToGorm(ctx, gbl.DB)
newUser := oldUser
newUser.GithubName = githubName
newUser.GithubUserID = githubID
// unique check
// new value -- old value
uniqueCheckList := []map[string]mysql.ValuePair{
{
"github_user_id": mysql.ValuePair{
NewVal: newUser.GithubUserID,
OldVal: oldUser.GithubUserID,
},
},
{
"github_name": mysql.ValuePair{
NewVal: newUser.GithubName,
OldVal: oldUser.GithubName,
},
},
}
// check
for _, checkMap := range uniqueCheckList {
if !mysql.ShouldUnique(c, ctx, db, checkMap, func(db *gorm.DB) error {
return db.First(&model.User{}).Error
}) {
return kerror.EmptyError
}
}
if err := db.Model(&oldUser).Updates(model.User{GithubUserID: githubID, GithubName: githubName}).Error; err != nil {
log.For(ctx).Error("binding user to github fail", zap.Error(err),
zap.Int("id", oldUser.ID))
_ = c.Error(err).SetType(gin.ErrorTypePrivate)
return kerror.EmptyError
}
log.For(ctx).Info("binding user to github success", zap.Int("id", oldUser.ID))
return nil
}
func GetUserFromJWT(c *gin.Context) model.User {
// get user from jwt
u, _ := c.Get(constants.JwtIdentityKey)
return u.(model.User)
}