refactor: authorize API routes with per-endpoint scopes (#1823)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
Elias Schneider
2026-10-09 21:33:14 +02:00
committed by GitHub
co-authored by copilot-swe-agent[bot]
parent 1bd6f006c8
commit 0ec6bfa191
48 changed files with 1422 additions and 681 deletions
+7 -11
View File
@@ -7,10 +7,10 @@ import (
"net/http"
"net/url"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/ory/fosite"
"github.com/pocket-id/pocket-id/backend/internal/authz"
"github.com/pocket-id/pocket-id/backend/internal/httpserver"
"github.com/pocket-id/pocket-id/backend/internal/utils"
"github.com/pocket-id/pocket-id/backend/internal/utils/cookie"
@@ -35,10 +35,7 @@ func newAuthorizationHandler(
func (h *authorizationHandler) authorize(c *gin.Context) {
ctx := c.Request.Context()
userID := c.GetString("userID")
authenticationMethod := c.GetString("authenticationMethod")
authenticationTime, _ := c.Get("authenticationTime")
typedAuthenticationTime, _ := authenticationTime.(time.Time)
principal := authz.PrincipalFrom(c)
reauthenticationToken, _ := c.Cookie(cookie.ReauthenticationTokenCookieName)
// A request that resumes an interaction only carries the interaction ID; the original
@@ -73,9 +70,9 @@ func (h *authorizationHandler) authorize(c *gin.Context) {
}
authorization, err := h.authorizationService.authorize(ctx, authorizeInput{
userID: userID,
authenticationMethod: authenticationMethod,
authenticationTime: typedAuthenticationTime,
userID: principal.UserID,
authenticationMethod: principal.AuthenticationMethod,
authenticationTime: principal.AuthenticationTime,
requester: ar,
hasPushedAuthorizationRequest: hasPushedAuthorizationRequest,
reauthenticationToken: reauthenticationToken,
@@ -121,8 +118,7 @@ func (h *authorizationHandler) getInteractionSession(c *gin.Context) {
func (h *authorizationHandler) completeInteraction(c *gin.Context) {
interactionID := c.Param("id")
authenticationTime, _ := c.Get("authenticationTime")
typedAuthenticationTime, _ := authenticationTime.(time.Time)
principal := authz.PrincipalFrom(c)
var request completeInteractionRequest
if err := httpserver.BindJSON(c, &request); err != nil {
@@ -131,7 +127,7 @@ func (h *authorizationHandler) completeInteraction(c *gin.Context) {
}
reauthenticationToken, _ := c.Cookie(cookie.ReauthenticationTokenCookieName)
response, err := h.authorizationService.completeInteractionStep(c.Request.Context(), interactionID, c.GetString("userID"), request.Step, reauthenticationToken, typedAuthenticationTime, requestMetaFromGin(c))
response, err := h.authorizationService.completeInteractionStep(c.Request.Context(), interactionID, principal.UserID, request.Step, reauthenticationToken, principal.AuthenticationTime, requestMetaFromGin(c))
if err != nil {
_ = c.Error(err)
return
@@ -15,6 +15,7 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/pocket-id/pocket-id/backend/internal/authz"
"github.com/pocket-id/pocket-id/backend/internal/model"
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
testutils "github.com/pocket-id/pocket-id/backend/internal/utils/testing"
@@ -167,8 +168,12 @@ func testAuthorizationHandlerPAR(t *testing.T, clientType string, tt authorizati
rec := httptest.NewRecorder()
router := gin.New()
router.Handle(tt.method, "/authorize", func(c *gin.Context) {
c.Set("userID", userID)
c.Set("authenticationTime", time.Now().UTC().Add(-time.Minute))
authz.SetPrincipal(c, &authz.Principal{
Kind: authz.KindSession,
UserID: userID,
Scopes: authz.UserScopes(false, authz.KindSession),
AuthenticationTime: time.Now().UTC().Add(-time.Minute),
})
handler.authorize(c)
})
router.ServeHTTP(rec, req)
+6 -7
View File
@@ -4,11 +4,11 @@ import (
"errors"
"log/slog"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/ory/fosite"
"github.com/pocket-id/pocket-id/backend/internal/apperror"
"github.com/pocket-id/pocket-id/backend/internal/authz"
"github.com/pocket-id/pocket-id/backend/internal/utils/cookie"
)
@@ -39,8 +39,7 @@ func (h *deviceHandler) authorizeDevice(c *gin.Context) {
}
func (h *deviceHandler) verifyDeviceCode(c *gin.Context) {
authenticationTime, _ := c.Get("authenticationTime")
typedAuthenticationTime, _ := authenticationTime.(time.Time)
principal := authz.PrincipalFrom(c)
reauthenticationToken, _ := c.Cookie(cookie.ReauthenticationTokenCookieName)
userCode := c.Query("code")
@@ -52,9 +51,9 @@ func (h *deviceHandler) verifyDeviceCode(c *gin.Context) {
err := h.deviceService.acceptDeviceCode(
c.Request.Context(),
userCode,
c.GetString("userID"),
c.GetString("authenticationMethod"),
typedAuthenticationTime,
principal.UserID,
principal.AuthenticationMethod,
principal.AuthenticationTime,
reauthenticationToken,
requestMetaFromGin(c),
)
@@ -77,7 +76,7 @@ func (h *deviceHandler) deviceCodeInfo(c *gin.Context) {
return
}
deviceCodeInfo, err := h.deviceService.getDeviceCodeInfo(c.Request.Context(), userCode, c.GetString("userID"))
deviceCodeInfo, err := h.deviceService.getDeviceCodeInfo(c.Request.Context(), userCode, authz.PrincipalFrom(c).UserID)
if err != nil {
_ = c.Error(err)
return
+2 -1
View File
@@ -5,6 +5,7 @@ import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/pocket-id/pocket-id/backend/internal/authz"
"github.com/pocket-id/pocket-id/backend/internal/dto"
"github.com/pocket-id/pocket-id/backend/internal/utils/cookie"
)
@@ -28,7 +29,7 @@ func (h *endSessionHandler) endSession(c *gin.Context) {
return
}
callbackURL, err := h.endSessionService.endSession(c.Request.Context(), input, c.GetString("userID"))
callbackURL, err := h.endSessionService.endSession(c.Request.Context(), input, authz.PrincipalFrom(c).UserID)
if err != nil {
slog.WarnContext(c.Request.Context(), "Error getting logout callback URL, the user has to confirm the logout manually", "error", err)
c.Redirect(http.StatusFound, h.baseURL+"/logout")
+16 -16
View File
@@ -7,13 +7,13 @@ import (
"net/http"
"time"
"github.com/gin-gonic/gin"
francishost "github.com/italypaleale/francis/host"
"github.com/lestrrat-go/jwx/v4/jwa"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"gorm.io/gorm"
"github.com/pocket-id/pocket-id/backend/internal/auditlogs"
"github.com/pocket-id/pocket-id/backend/internal/authz"
"github.com/pocket-id/pocket-id/backend/internal/model"
)
@@ -143,26 +143,26 @@ func (m *Module) RefreshClientMetadata(ctx context.Context, clientID string) (mo
return m.cimdResolver.RefreshMetadataClient(ctx, clientID)
}
func (m *Module) RegisterRoutes(rootGroup *gin.RouterGroup, apiGroup *gin.RouterGroup, optionalBrowserAuth gin.HandlerFunc, browserAuth gin.HandlerFunc) {
rootGroup.GET("/authorize", optionalBrowserAuth, m.authorizationHandler.authorize)
rootGroup.POST("/authorize", optionalBrowserAuth, m.authorizationHandler.authorize)
func (m *Module) RegisterRoutes(root, api *authz.Router) {
root.Optional().GET("/authorize", authz.AccountSession, m.authorizationHandler.authorize)
root.Optional().POST("/authorize", authz.AccountSession, m.authorizationHandler.authorize)
apiGroup.GET("/oidc/interactions/:id", m.authorizationHandler.getInteractionSession)
apiGroup.POST("/oidc/interactions/:id/complete", browserAuth, m.authorizationHandler.completeInteraction)
api.Public().GET("/oidc/interactions/:id", m.authorizationHandler.getInteractionSession)
api.POST("/oidc/interactions/:id/complete", authz.AccountSession, m.authorizationHandler.completeInteraction)
apiGroup.POST("/oidc/par", m.parHandler.pushedAuthorizationRequest)
api.Public().POST("/oidc/par", m.parHandler.pushedAuthorizationRequest)
apiGroup.POST("/oidc/token", m.tokenHandler.token)
api.Public().POST("/oidc/token", m.tokenHandler.token)
apiGroup.GET("/oidc/userinfo", m.userInfoHandler.userInfo)
apiGroup.POST("/oidc/userinfo", m.userInfoHandler.userInfo)
api.Public().GET("/oidc/userinfo", m.userInfoHandler.userInfo)
api.Public().POST("/oidc/userinfo", m.userInfoHandler.userInfo)
apiGroup.POST("/oidc/introspect", m.introspectionHandler.introspectToken)
api.Public().POST("/oidc/introspect", m.introspectionHandler.introspectToken)
apiGroup.GET("/oidc/end-session", optionalBrowserAuth, m.endSessionHandler.endSession)
apiGroup.POST("/oidc/end-session", optionalBrowserAuth, m.endSessionHandler.endSession)
api.Optional().GET("/oidc/end-session", authz.AccountSession, m.endSessionHandler.endSession)
api.Optional().POST("/oidc/end-session", authz.AccountSession, m.endSessionHandler.endSession)
apiGroup.POST("/oidc/device/authorize", m.deviceHandler.authorizeDevice)
apiGroup.POST("/oidc/device/verify", browserAuth, m.deviceHandler.verifyDeviceCode)
apiGroup.GET("/oidc/device/info", browserAuth, m.deviceHandler.deviceCodeInfo)
api.Public().POST("/oidc/device/authorize", m.deviceHandler.authorizeDevice)
api.POST("/oidc/device/verify", authz.AccountSession, m.deviceHandler.verifyDeviceCode)
api.GET("/oidc/device/info", authz.AccountSession, m.deviceHandler.deviceCodeInfo)
}