Files
pocket-id/backend/internal/devicelogin/handler.go
Alessandro (Ale) Segala 563c0f93a6 refactor: migrate LDAP sync to an actor (#1651)
Co-authored-by: Kyle Mendell <kmendell@ofkm.us>
2026-08-07 09:18:30 -07:00

145 lines
4.5 KiB
Go

package devicelogin
import (
"fmt"
"net/http"
"net/url"
"github.com/gin-gonic/gin"
"github.com/pocket-id/pocket-id/backend/internal/appconfig"
"github.com/pocket-id/pocket-id/backend/internal/dto"
"github.com/pocket-id/pocket-id/backend/internal/httpserver"
"github.com/pocket-id/pocket-id/backend/internal/utils/cookie"
)
type handler struct {
service *Service
baseURL string
appConfig appconfig.AppConfigResolver
}
func newHandler(service *Service, baseURL string, appConfig appconfig.AppConfigResolver) *handler {
return &handler{
service: service,
baseURL: baseURL,
appConfig: appConfig,
}
}
// createRequest godoc
// @Summary Create device login request
// @Description Create a short-lived request that can be approved from another authenticated device
// @Tags Device Login
// @Produce json
// @Success 201 {object} requestCreateDto "Created device login request"
// @Router /api/device-login/requests [post]
func (h *handler) createRequest(c *gin.Context) error {
request, deviceToken, err := h.service.Create(c.Request.Context(), c.ClientIP(), c.Request.UserAgent())
if err != nil {
return err
}
verificationURI := h.baseURL + "/device"
verificationURIComplete := verificationURI + "?user_code=" + url.QueryEscape(request.Code)
cookie.AddDeviceLoginTokenCookie(c, request.ID, deviceToken)
c.JSON(http.StatusCreated, requestCreateDto{
ID: request.ID,
UserCode: request.Code,
VerificationURI: verificationURI,
VerificationURIComplete: verificationURIComplete,
ExpiresAt: request.ExpiresAt,
Interval: PollingInterval,
})
return nil
}
// exchangeRequest godoc
// @Summary Exchange device login request
// @Description Wait for a device login decision and create a browser session after it has been approved
// @Tags Device Login
// @Produce json
// @Param id path string true "Device login request ID"
// @Success 200 {object} dto.UserDto "Approved request exchanged for a user session"
// @Success 202 "Authorization pending"
// @Router /api/device-login/requests/{id}/exchange [post]
func (h *handler) exchangeRequest(c *gin.Context) error {
dbConfig, err := h.appConfig.GetConfig(c.Request.Context())
if err != nil {
return fmt.Errorf("error loading app configuration: %w", err)
}
requestID := c.Param("id")
deviceToken, _ := c.Cookie(cookie.DeviceLoginTokenCookieName)
sessionDuration := dbConfig.SessionDuration.AsDurationMinutes()
user, accessToken, status, err := h.service.Exchange(c.Request.Context(), requestID, deviceToken, c.ClientIP(), c.Request.UserAgent(), sessionDuration)
if err != nil {
if c.Request.Context().Err() != nil {
// Context canceled = the client stopped the request
// Nothing to do here
return c.Request.Context().Err()
}
return err
}
if status == RequestStatusPending {
// Request is pending, so respond with a 202
c.Status(http.StatusAccepted)
return nil
}
maxAge := int(sessionDuration.Seconds())
cookie.AddAccessTokenCookie(c, maxAge, accessToken)
c.JSON(http.StatusOK, dto.UserDto(user))
return nil
}
// inspectRequest godoc
// @Summary Inspect device login request
// @Description Retrieve the requesting device details for an authenticated user before approval or denial
// @Tags Device Login
// @Accept json
// @Produce json
// @Param request body verificationDto true "Device login code"
// @Success 200 {object} verificationInfoDto "Device login request details"
// @Router /api/device-login/verification [post]
func (h *handler) inspectRequest(c *gin.Context) error {
var input verificationDto
err := httpserver.BindJSON(c, &input)
if err != nil {
return err
}
info, err := h.service.Inspect(c.Request.Context(), input.Code)
if err != nil {
return err
}
c.JSON(http.StatusOK, verificationInfoDto(info))
return nil
}
// decideRequest godoc
// @Summary Decide device login request
// @Description Approve or deny a device login request; approval requires fresh passkey reauthentication
// @Tags Device Login
// @Accept json
// @Param decision body decisionDto true "Device login decision"
// @Success 204 "No Content"
// @Router /api/device-login/verification/decision [post]
func (h *handler) decideRequest(c *gin.Context) error {
var input decisionDto
err := httpserver.BindJSON(c, &input)
if err != nil {
return err
}
reauthenticationToken, _ := c.Cookie(cookie.ReauthenticationTokenCookieName)
err = h.service.Decide(c.Request.Context(), input.Code, input.Decision, c.GetString("userID"), reauthenticationToken)
if err != nil {
return err
}
c.Status(http.StatusNoContent)
return nil
}