Skip to content

Commit b445a1b

Browse files
authored
Merge pull request #555 from 0xJacky/feat/passkey
feat/passkey
2 parents a14f16e + 018a3f1 commit b445a1b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

74 files changed

+3802
-1591
lines changed

api/api.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import (
1212
"strings"
1313
)
1414

15-
func CurrentUser(c *gin.Context) *model.Auth {
16-
return c.MustGet("user").(*model.Auth)
15+
func CurrentUser(c *gin.Context) *model.User {
16+
return c.MustGet("user").(*model.User)
1717
}
1818

1919
func ErrHandler(c *gin.Context, err error) {

api/system/install.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ func InstallNginxUI(c *gin.Context) {
6161

6262
pwd, _ := bcrypt.GenerateFromPassword([]byte(json.Password), bcrypt.DefaultCost)
6363

64-
u := query.Auth
65-
err = u.Create(&model.Auth{
64+
u := query.User
65+
err = u.Create(&model.User{
6666
Name: json.Username,
6767
Password: string(pwd),
6868
})

api/user/2fa.go

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package user
2+
3+
import (
4+
"encoding/base64"
5+
"fmt"
6+
"github.com/0xJacky/Nginx-UI/api"
7+
"github.com/0xJacky/Nginx-UI/internal/cache"
8+
"github.com/0xJacky/Nginx-UI/internal/passkey"
9+
"github.com/0xJacky/Nginx-UI/internal/user"
10+
"github.com/0xJacky/Nginx-UI/model"
11+
"github.com/0xJacky/Nginx-UI/query"
12+
"github.com/gin-gonic/gin"
13+
"github.com/go-webauthn/webauthn/webauthn"
14+
"github.com/google/uuid"
15+
"net/http"
16+
"strings"
17+
"time"
18+
)
19+
20+
type Status2FA struct {
21+
Enabled bool `json:"enabled"`
22+
OTPStatus bool `json:"otp_status"`
23+
PasskeyStatus bool `json:"passkey_status"`
24+
}
25+
26+
func get2FAStatus(c *gin.Context) (status Status2FA) {
27+
// when accessing the node from the main cluster, there is no user in the context
28+
u, ok := c.Get("user")
29+
if ok {
30+
userPtr := u.(*model.User)
31+
status.OTPStatus = userPtr.EnabledOTP()
32+
status.PasskeyStatus = userPtr.EnabledPasskey() && passkey.Enabled()
33+
status.Enabled = status.OTPStatus || status.PasskeyStatus
34+
}
35+
return
36+
}
37+
38+
func Get2FAStatus(c *gin.Context) {
39+
c.JSON(http.StatusOK, get2FAStatus(c))
40+
}
41+
42+
func SecureSessionStatus(c *gin.Context) {
43+
status2FA := get2FAStatus(c)
44+
if !status2FA.Enabled {
45+
c.JSON(http.StatusOK, gin.H{
46+
"status": false,
47+
})
48+
return
49+
}
50+
51+
ssid := c.GetHeader("X-Secure-Session-ID")
52+
if ssid == "" {
53+
ssid = c.Query("X-Secure-Session-ID")
54+
}
55+
if ssid == "" {
56+
c.JSON(http.StatusOK, gin.H{
57+
"status": false,
58+
})
59+
return
60+
}
61+
62+
u := api.CurrentUser(c)
63+
64+
c.JSON(http.StatusOK, gin.H{
65+
"status": user.VerifySecureSessionID(ssid, u.ID),
66+
})
67+
}
68+
69+
func Start2FASecureSessionByOTP(c *gin.Context) {
70+
var json struct {
71+
OTP string `json:"otp"`
72+
RecoveryCode string `json:"recovery_code"`
73+
}
74+
if !api.BindAndValid(c, &json) {
75+
return
76+
}
77+
u := api.CurrentUser(c)
78+
if !u.EnabledOTP() {
79+
c.JSON(http.StatusBadRequest, gin.H{
80+
"message": "User has not configured OTP as 2FA",
81+
})
82+
return
83+
}
84+
85+
if json.OTP == "" && json.RecoveryCode == "" {
86+
c.JSON(http.StatusBadRequest, LoginResponse{
87+
Message: "The user has enabled OTP as 2FA",
88+
})
89+
return
90+
}
91+
92+
if err := user.VerifyOTP(u, json.OTP, json.RecoveryCode); err != nil {
93+
c.JSON(http.StatusBadRequest, LoginResponse{
94+
Message: "Invalid OTP or recovery code",
95+
})
96+
return
97+
}
98+
99+
sessionId := user.SetSecureSessionID(u.ID)
100+
101+
c.JSON(http.StatusOK, gin.H{
102+
"session_id": sessionId,
103+
})
104+
}
105+
106+
func BeginStart2FASecureSessionByPasskey(c *gin.Context) {
107+
if !passkey.Enabled() {
108+
api.ErrHandler(c, fmt.Errorf("WebAuthn settings are not configured"))
109+
return
110+
}
111+
webauthnInstance := passkey.GetInstance()
112+
u := api.CurrentUser(c)
113+
options, sessionData, err := webauthnInstance.BeginLogin(u)
114+
if err != nil {
115+
api.ErrHandler(c, err)
116+
return
117+
}
118+
passkeySessionID := uuid.NewString()
119+
cache.Set(passkeySessionID, sessionData, passkeyTimeout)
120+
c.JSON(http.StatusOK, gin.H{
121+
"session_id": passkeySessionID,
122+
"options": options,
123+
})
124+
}
125+
126+
func FinishStart2FASecureSessionByPasskey(c *gin.Context) {
127+
if !passkey.Enabled() {
128+
api.ErrHandler(c, fmt.Errorf("WebAuthn settings are not configured"))
129+
return
130+
}
131+
passkeySessionID := c.GetHeader("X-Passkey-Session-ID")
132+
sessionDataBytes, ok := cache.Get(passkeySessionID)
133+
if !ok {
134+
api.ErrHandler(c, fmt.Errorf("session not found"))
135+
return
136+
}
137+
sessionData := sessionDataBytes.(*webauthn.SessionData)
138+
webauthnInstance := passkey.GetInstance()
139+
u := api.CurrentUser(c)
140+
credential, err := webauthnInstance.FinishLogin(u, *sessionData, c.Request)
141+
if err != nil {
142+
api.ErrHandler(c, err)
143+
return
144+
}
145+
rawID := strings.TrimRight(base64.StdEncoding.EncodeToString(credential.ID), "=")
146+
p := query.Passkey
147+
_, _ = p.Where(p.RawID.Eq(rawID)).Updates(&model.Passkey{
148+
LastUsedAt: time.Now().Unix(),
149+
})
150+
151+
sessionId := user.SetSecureSessionID(u.ID)
152+
153+
c.JSON(http.StatusOK, gin.H{
154+
"session_id": sessionId,
155+
})
156+
}

api/user/auth.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/0xJacky/Nginx-UI/settings"
99
"github.com/gin-gonic/gin"
1010
"github.com/pkg/errors"
11+
"math/rand/v2"
1112
"net/http"
1213
"sync"
1314
"time"
@@ -67,7 +68,8 @@ func Login(c *gin.Context) {
6768

6869
u, err := user.Login(json.Name, json.Password)
6970
if err != nil {
70-
// time.Sleep(5 * time.Second)
71+
random := time.Duration(rand.Int() % 10)
72+
time.Sleep(random * time.Second)
7173
switch {
7274
case errors.Is(err, user.ErrPasswordIncorrect):
7375
c.JSON(http.StatusForbidden, LoginResponse{

0 commit comments

Comments
 (0)