Files
pocket-id/backend/internal/oidc/module.go
2026-07-10 16:31:45 -05:00

145 lines
7.3 KiB
Go

package oidc
import (
"context"
"fmt"
"net/http"
"time"
"github.com/danielgtaylor/huma/v2"
"github.com/gin-gonic/gin"
"github.com/lestrrat-go/jwx/v3/jwa"
"github.com/pocket-id/pocket-id/backend/internal/model"
httpapi "github.com/pocket-id/pocket-id/backend/internal/utils/huma"
"gorm.io/gorm"
)
type Config struct {
BaseURL string
TokenBaseURL string
Secret []byte
}
type TokenSigner interface {
GetPrivateKey() any
GetKeyAlg() (jwa.KeyAlgorithm, error)
GetKeyID() (string, bool)
}
type CustomClaimSource interface {
GetCustomClaimsForUserWithUserGroups(ctx context.Context, userID string, tx *gorm.DB) ([]model.CustomClaim, error)
}
type ReauthenticationTokenConsumer interface {
ConsumeReauthenticationToken(ctx context.Context, tx *gorm.DB, token string, userID string) (time.Time, error)
}
type AuditLogger interface {
Create(ctx context.Context, event model.AuditLogEvent, ipAddress, userAgent, userID string, data model.AuditLogData, tx *gorm.DB) (model.AuditLog, bool)
}
type Dependencies struct {
DB *gorm.DB
Config Config
HTTPClient *http.Client
Signer TokenSigner
CustomClaims CustomClaimSource
Reauth ReauthenticationTokenConsumer
AuditLog AuditLogger
APIAccess APIAccessProvider
}
type Module struct {
Preview *ClientPreviewBuilder
config Config
store *Store
authorizationHandler *authorizationHandler
tokenHandler *tokenHandler
userInfoHandler *userInfoHandler
parHandler *parHandler
introspectionHandler *introspectionHandler
endSessionHandler *endSessionHandler
deviceHandler *deviceHandler
}
func New(ctx context.Context, deps Dependencies) (*Module, error) {
store := NewStore(deps.DB, deps.APIAccess).WithIssuer(deps.Config.BaseURL)
authenticator, err := newFederatedClientAuthenticator(ctx, store, deps.HTTPClient, deps.Config.BaseURL)
if err != nil {
return nil, fmt.Errorf("failed to create federated client authenticator: %w", err)
}
provider, err := newProvider(store, authenticator, deps.Signer, deps.Config)
if err != nil {
return nil, fmt.Errorf("failed to create OAuth2 provider: %w", err)
}
claimsService := newClaimsService(deps.DB, deps.CustomClaims, deps.Config.BaseURL, deps.Signer)
previewBuilder := newClientPreviewBuilder(claimsService, provider.tokenStrategies)
interactionSessionService := newInteractionSessionService(deps.DB)
authorizationService := newAuthorizationService(deps.DB, interactionSessionService, claimsService, deps.Reauth, deps.AuditLog, deps.APIAccess)
deviceService := newDeviceService(provider, store, provider.deviceStrategy, authorizationService, claimsService, deps.AuditLog, deps.DB)
endSessionService := newEndSessionService(deps.DB, store, deps.Signer, deps.Config.BaseURL)
return &Module{
Preview: previewBuilder,
config: deps.Config,
store: store,
authorizationHandler: newAuthorizationHandler(provider, authorizationService, deps.Config.BaseURL),
tokenHandler: newTokenHandler(provider, claimsService, deps.APIAccess),
userInfoHandler: newUserInfoHandler(provider, claimsService, deps.Config.BaseURL),
parHandler: newPARHandler(provider),
introspectionHandler: newIntrospectionHandler(provider, authenticator, deps.Config.BaseURL),
endSessionHandler: newEndSessionHandler(endSessionService, deps.Config.BaseURL),
deviceHandler: newDeviceHandler(provider, deviceService),
}, nil
}
// RegisterRawRoutes mounts protocol endpoints that must retain direct Gin and Fosite response control
func (m *Module) RegisterRawRoutes(rootGroup *gin.RouterGroup, apiGroup *gin.RouterGroup, optionalBrowserAuth gin.HandlerFunc, api huma.API) {
rootGroup.GET("/authorize", optionalBrowserAuth, m.authorizationHandler.authorize)
rootGroup.POST("/authorize", optionalBrowserAuth, m.authorizationHandler.authorize)
apiGroup.POST("/oidc/par", m.parHandler.pushedAuthorizationRequest)
apiGroup.POST("/oidc/token", m.tokenHandler.token)
apiGroup.GET("/oidc/userinfo", m.userInfoHandler.userInfo)
apiGroup.POST("/oidc/userinfo", m.userInfoHandler.userInfo)
apiGroup.POST("/oidc/introspect", m.introspectionHandler.introspectToken)
apiGroup.GET("/oidc/end-session", optionalBrowserAuth, m.endSessionHandler.endSession)
apiGroup.POST("/oidc/end-session", optionalBrowserAuth, m.endSessionHandler.endSession)
apiGroup.POST("/oidc/device/authorize", m.deviceHandler.authorizeDevice)
tags := []string{"OIDC Protocol"}
httpapi.AddRawOperation(api, "authorize-get", http.MethodGet, "/authorize", "Authorize", tags, nil, http.StatusOK, http.StatusFound)
httpapi.AddRawOperation(api, "authorize-post", http.MethodPost, "/authorize", "Authorize", tags, nil, http.StatusOK, http.StatusFound)
httpapi.AddRawOperation(api, "pushed-authorization-request", http.MethodPost, "/api/oidc/par", "Create pushed authorization request", tags, []map[string][]string{{"OIDCClientBasic": {}}})
httpapi.AddRawOperation(api, "oidc-token", http.MethodPost, "/api/oidc/token", "Exchange an OIDC token", tags, []map[string][]string{{"OIDCClientBasic": {}}})
httpapi.AddRawOperation(api, "oidc-userinfo-get", http.MethodGet, "/api/oidc/userinfo", "Get OIDC user info", tags, []map[string][]string{{"OIDCAccessToken": {}}})
httpapi.AddRawOperation(api, "oidc-userinfo-post", http.MethodPost, "/api/oidc/userinfo", "Get OIDC user info", tags, []map[string][]string{{"OIDCAccessToken": {}}})
httpapi.AddRawOperation(api, "oidc-introspection", http.MethodPost, "/api/oidc/introspect", "Introspect an OIDC token", tags, []map[string][]string{{"OIDCClientBasic": {}}})
httpapi.AddRawOperation(api, "oidc-end-session-get", http.MethodGet, "/api/oidc/end-session", "End an OIDC session", tags, nil, http.StatusFound)
httpapi.AddRawOperation(api, "oidc-end-session-post", http.MethodPost, "/api/oidc/end-session", "End an OIDC session", tags, nil, http.StatusFound)
httpapi.AddRawOperation(api, "oidc-device-authorization", http.MethodPost, "/api/oidc/device/authorize", "Create device authorization", tags, []map[string][]string{{"OIDCClientBasic": {}}})
}
// RegisterTypedRoutes mounts JSON interaction and device verification endpoints
func (m *Module) RegisterTypedRoutes(api huma.API, browserAuth func(*huma.Operation)) {
httpapi.Register(api, huma.Operation{OperationID: "get-oidc-interaction", Method: http.MethodGet, Path: "/api/oidc/interactions/{id}", Summary: "Get OIDC interaction", Tags: []string{"OIDC Interactions"}}, m.authorizationHandler.getInteractionSession)
completeInteraction := huma.Operation{OperationID: "complete-oidc-interaction", Method: http.MethodPost, Path: "/api/oidc/interactions/{id}/complete", Summary: "Complete OIDC interaction", Tags: []string{"OIDC Interactions"}}
browserAuth(&completeInteraction)
httpapi.Register(api, completeInteraction, m.authorizationHandler.completeInteraction)
verifyDevice := huma.Operation{OperationID: "verify-oidc-device-code", Method: http.MethodPost, Path: "/api/oidc/device/verify", Summary: "Verify OIDC device code", Tags: []string{"OIDC Protocol"}, DefaultStatus: http.StatusNoContent}
browserAuth(&verifyDevice)
httpapi.Register(api, verifyDevice, m.deviceHandler.verifyDeviceCode)
deviceInfo := huma.Operation{OperationID: "get-oidc-device-info", Method: http.MethodGet, Path: "/api/oidc/device/info", Summary: "Get OIDC device code info", Tags: []string{"OIDC Protocol"}}
browserAuth(&deviceInfo)
httpapi.Register(api, deviceInfo, m.deviceHandler.deviceCodeInfo)
}