mirror of
https://github.com/pocket-id/pocket-id.git
synced 2025-12-13 16:52:58 +03:00
256 lines
7.6 KiB
Go
256 lines
7.6 KiB
Go
package handler
|
|
|
|
import (
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/go-webauthn/webauthn/protocol"
|
|
"github.com/go-webauthn/webauthn/webauthn"
|
|
"golang-rest-api-template/internal/common"
|
|
"golang-rest-api-template/internal/common/middleware"
|
|
"golang-rest-api-template/internal/model"
|
|
"golang-rest-api-template/internal/utils"
|
|
"golang.org/x/time/rate"
|
|
"gorm.io/gorm"
|
|
"log"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func RegisterRoutes(group *gin.RouterGroup) {
|
|
group.GET("/webauthn/register/start", middleware.JWTAuth(false), beginRegistrationHandler)
|
|
group.POST("/webauthn/register/finish", middleware.JWTAuth(false), verifyRegistrationHandler)
|
|
|
|
group.GET("/webauthn/login/start", beginLoginHandler)
|
|
group.POST("/webauthn/login/finish", middleware.RateLimiter(rate.Every(10*time.Second), 5), verifyLoginHandler)
|
|
|
|
group.POST("/webauthn/logout", middleware.JWTAuth(false), logoutHandler)
|
|
|
|
group.GET("/webauthn/credentials", middleware.JWTAuth(false), listCredentialsHandler)
|
|
group.PATCH("/webauthn/credentials/:id", middleware.JWTAuth(false), updateCredentialHandler)
|
|
group.DELETE("/webauthn/credentials/:id", middleware.JWTAuth(false), deleteCredentialHandler)
|
|
}
|
|
|
|
func beginRegistrationHandler(c *gin.Context) {
|
|
var user model.User
|
|
err := common.DB.Preload("Credentials").Find(&user, "id = ?", c.GetString("userID")).Error
|
|
if err != nil {
|
|
utils.UnknownHandlerError(c, err)
|
|
log.Println(err)
|
|
return
|
|
}
|
|
|
|
options, session, err := common.WebAuthn.BeginRegistration(&user, webauthn.WithResidentKeyRequirement(protocol.ResidentKeyRequirementRequired), webauthn.WithExclusions(user.WebAuthnCredentialDescriptors()))
|
|
if err != nil {
|
|
utils.UnknownHandlerError(c, err)
|
|
return
|
|
}
|
|
|
|
// Save the webauthn session so we can retrieve it in the verifyRegistrationHandler
|
|
sessionToStore := &model.WebauthnSession{
|
|
ExpiresAt: session.Expires,
|
|
Challenge: session.Challenge,
|
|
UserVerification: string(session.UserVerification),
|
|
}
|
|
|
|
if err = common.DB.Create(&sessionToStore).Error; err != nil {
|
|
utils.UnknownHandlerError(c, err)
|
|
return
|
|
}
|
|
|
|
c.SetCookie("session_id", sessionToStore.ID, int(common.WebAuthn.Config.Timeouts.Registration.Timeout.Seconds()), "/", "", false, true)
|
|
c.JSON(http.StatusOK, options.Response)
|
|
}
|
|
|
|
func verifyRegistrationHandler(c *gin.Context) {
|
|
sessionID, err := c.Cookie("session_id")
|
|
if err != nil {
|
|
utils.HandlerError(c, http.StatusBadRequest, "Session ID missing")
|
|
return
|
|
}
|
|
|
|
// Retrieve the session that was previously created by the beginRegistrationHandler
|
|
var storedSession model.WebauthnSession
|
|
err = common.DB.First(&storedSession, "id = ?", sessionID).Error
|
|
|
|
session := webauthn.SessionData{
|
|
Challenge: storedSession.Challenge,
|
|
Expires: storedSession.ExpiresAt,
|
|
UserID: []byte(c.GetString("userID")),
|
|
}
|
|
|
|
var user model.User
|
|
err = common.DB.Find(&user, "id = ?", c.GetString("userID")).Error
|
|
if err != nil {
|
|
utils.UnknownHandlerError(c, err)
|
|
return
|
|
}
|
|
|
|
credential, err := common.WebAuthn.FinishRegistration(&user, session, c.Request)
|
|
if err != nil {
|
|
utils.UnknownHandlerError(c, err)
|
|
return
|
|
}
|
|
|
|
credentialToStore := model.WebauthnCredential{
|
|
Name: "New Passkey",
|
|
CredentialID: string(credential.ID),
|
|
AttestationType: credential.AttestationType,
|
|
PublicKey: credential.PublicKey,
|
|
Transport: credential.Transport,
|
|
UserID: user.ID,
|
|
}
|
|
if err := common.DB.Create(&credentialToStore).Error; err != nil {
|
|
utils.UnknownHandlerError(c, err)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, credentialToStore)
|
|
}
|
|
|
|
func beginLoginHandler(c *gin.Context) {
|
|
options, session, err := common.WebAuthn.BeginDiscoverableLogin()
|
|
if err != nil {
|
|
utils.UnknownHandlerError(c, err)
|
|
return
|
|
}
|
|
|
|
// Save the webauthn session so we can retrieve it in the verifyLoginHandler
|
|
sessionToStore := &model.WebauthnSession{
|
|
ExpiresAt: session.Expires,
|
|
Challenge: session.Challenge,
|
|
UserVerification: string(session.UserVerification),
|
|
}
|
|
|
|
if err = common.DB.Create(&sessionToStore).Error; err != nil {
|
|
utils.UnknownHandlerError(c, err)
|
|
return
|
|
}
|
|
|
|
c.SetCookie("session_id", sessionToStore.ID, int(common.WebAuthn.Config.Timeouts.Registration.Timeout.Seconds()), "/", "", false, true)
|
|
c.JSON(http.StatusOK, options.Response)
|
|
}
|
|
|
|
func verifyLoginHandler(c *gin.Context) {
|
|
sessionID, err := c.Cookie("session_id")
|
|
if err != nil {
|
|
utils.HandlerError(c, http.StatusBadRequest, "Session ID missing")
|
|
return
|
|
}
|
|
|
|
credentialAssertionData, err := protocol.ParseCredentialRequestResponseBody(c.Request.Body)
|
|
if err != nil {
|
|
utils.HandlerError(c, http.StatusBadRequest, "Invalid body")
|
|
return
|
|
}
|
|
|
|
// Retrieve the session that was previously created by the beginLoginHandler
|
|
var storedSession model.WebauthnSession
|
|
if err := common.DB.First(&storedSession, "id = ?", sessionID).Error; err != nil {
|
|
utils.UnknownHandlerError(c, err)
|
|
return
|
|
}
|
|
|
|
session := webauthn.SessionData{
|
|
Challenge: storedSession.Challenge,
|
|
Expires: storedSession.ExpiresAt,
|
|
}
|
|
|
|
var user *model.User
|
|
_, err = common.WebAuthn.ValidateDiscoverableLogin(func(_, userHandle []byte) (webauthn.User, error) {
|
|
if err := common.DB.Preload("Credentials").First(&user, "id = ?", string(userHandle)).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return user, nil
|
|
}, session, credentialAssertionData)
|
|
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), gorm.ErrRecordNotFound.Error()) {
|
|
utils.HandlerError(c, http.StatusBadRequest, "no user with this passkey exists")
|
|
} else {
|
|
utils.UnknownHandlerError(c, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
err = common.DB.Find(&user, "id = ?", c.GetString("userID")).Error
|
|
if err != nil {
|
|
utils.UnknownHandlerError(c, err)
|
|
return
|
|
}
|
|
|
|
token, err := common.GenerateAccessToken(*user)
|
|
if err != nil {
|
|
utils.UnknownHandlerError(c, err)
|
|
return
|
|
}
|
|
|
|
c.SetCookie("access_token", token, int(time.Hour.Seconds()), "/", "", false, true)
|
|
c.JSON(http.StatusOK, user)
|
|
}
|
|
|
|
func listCredentialsHandler(c *gin.Context) {
|
|
var credentials []model.WebauthnCredential
|
|
if err := common.DB.Find(&credentials, "user_id = ?", c.GetString("userID")).Error; err != nil {
|
|
utils.UnknownHandlerError(c, err)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, credentials)
|
|
}
|
|
|
|
func deleteCredentialHandler(c *gin.Context) {
|
|
var passkeyCount int64
|
|
if err := common.DB.Model(&model.WebauthnCredential{}).Where("user_id = ?", c.GetString("userID")).Count(&passkeyCount).Error; err != nil {
|
|
utils.UnknownHandlerError(c, err)
|
|
return
|
|
}
|
|
|
|
if passkeyCount == 1 {
|
|
utils.HandlerError(c, http.StatusBadRequest, "You must have at least one passkey")
|
|
return
|
|
}
|
|
|
|
var credential model.WebauthnCredential
|
|
if err := common.DB.First(&credential, "id = ? AND user_id = ?", c.Param("id"), c.GetString("userID")).Error; err != nil {
|
|
utils.HandlerError(c, http.StatusNotFound, "Credential not found")
|
|
return
|
|
}
|
|
|
|
if err := common.DB.Delete(&credential).Error; err != nil {
|
|
utils.UnknownHandlerError(c, err)
|
|
return
|
|
}
|
|
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
|
|
func updateCredentialHandler(c *gin.Context) {
|
|
var credential model.WebauthnCredential
|
|
if err := common.DB.Where("id = ? AND user_id = ?", c.Param("id"), c.GetString("userID")).First(&credential).Error; err != nil {
|
|
utils.HandlerError(c, http.StatusNotFound, "Credential not found")
|
|
return
|
|
}
|
|
|
|
var input struct {
|
|
Name string `json:"name"`
|
|
}
|
|
if err := c.ShouldBindJSON(&input); err != nil {
|
|
utils.HandlerError(c, http.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
|
|
credential.Name = input.Name
|
|
|
|
if err := common.DB.Save(&credential).Error; err != nil {
|
|
utils.UnknownHandlerError(c, err)
|
|
return
|
|
}
|
|
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
|
|
func logoutHandler(c *gin.Context) {
|
|
c.SetCookie("access_token", "", 0, "/", "", false, true)
|
|
c.Status(http.StatusNoContent)
|
|
}
|