feat: add ability to force email verification (#1837)

This commit is contained in:
Elias Schneider
2026-10-13 22:04:33 +02:00
committed by GitHub
parent eb8ea6bed2
commit 0ffeb170cf
81 changed files with 2310 additions and 263 deletions
+47 -12
View File
@@ -1,13 +1,17 @@
package middleware
import (
"context"
"strings"
"github.com/gin-gonic/gin"
"github.com/pocket-id/pocket-id/backend/internal/apikey"
"github.com/pocket-id/pocket-id/backend/internal/appconfig"
"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/emailverification"
"github.com/pocket-id/pocket-id/backend/internal/model"
"github.com/pocket-id/pocket-id/backend/internal/service"
"github.com/pocket-id/pocket-id/backend/internal/utils/cookie"
)
@@ -17,21 +21,39 @@ const apiKeyHeader = "X-API-Key"
// NewAuthorization creates the authorization middleware with every credential Pocket ID accepts
// A browser session is tried before an API key, so a signed-in browser is never mistaken for an API client
func NewAuthorization(apiKeyModule *apikey.Module, userService *service.UserService, jwtService *service.JwtService) *authz.Middleware {
func NewAuthorization(apiKeyModule *apikey.Module, userService *service.UserService, jwtService *service.JwtService, appConfig appconfig.AppConfigResolver) *authz.Middleware {
return authz.NewMiddleware(
NewSessionAuthenticator(jwtService, userService),
NewAPIKeyAuthenticator(apiKeyModule),
NewSessionAuthenticator(jwtService, userService, appConfig),
NewAPIKeyAuthenticator(apiKeyModule, appConfig),
)
}
// restrictUnverified withholds the scopes a user who still has to verify their email may not use yet
func restrictUnverified(ctx context.Context, appConfig appconfig.AppConfigResolver, user *model.User, principal *authz.Principal) error {
if !emailverification.CanBePending(user) {
return nil
}
cfg, err := appConfig.GetConfig(ctx)
if err != nil {
return err
}
if emailverification.IsPending(cfg, user) {
principal.RestrictUnverified()
}
return nil
}
// SessionAuthenticator authenticates the session access token Pocket ID issues after sign-in
type SessionAuthenticator struct {
jwtService *service.JwtService
userService *service.UserService
appConfig appconfig.AppConfigResolver
}
func NewSessionAuthenticator(jwtService *service.JwtService, userService *service.UserService) *SessionAuthenticator {
return &SessionAuthenticator{jwtService: jwtService, userService: userService}
func NewSessionAuthenticator(jwtService *service.JwtService, userService *service.UserService, appConfig appconfig.AppConfigResolver) *SessionAuthenticator {
return &SessionAuthenticator{jwtService: jwtService, userService: userService, appConfig: appConfig}
}
func (a *SessionAuthenticator) Kind() authz.PrincipalKind {
@@ -59,7 +81,7 @@ func (a *SessionAuthenticator) Authenticate(c *gin.Context) (*authz.Principal, e
return nil, apperror.TokenInvalid()
}
// Load the user so disabling an account or changing its admin flag takes effect before the token expires
// Load the user so disabling an account, changing its admin flag or its email verification takes effect before the token expires
user, err := a.userService.GetUser(c, subject)
if err != nil {
return nil, apperror.NotSignedIn()
@@ -68,13 +90,19 @@ func (a *SessionAuthenticator) Authenticate(c *gin.Context) (*authz.Principal, e
return nil, apperror.UserDisabled()
}
return &authz.Principal{
principal := &authz.Principal{
Kind: authz.KindSession,
UserID: user.ID,
Scopes: authz.UserScopes(user.IsAdmin, authz.KindSession),
AuthenticationMethod: authenticationMethod,
AuthenticationTime: authenticationTime,
}, nil
}
err = restrictUnverified(c.Request.Context(), a.appConfig, &user, principal)
if err != nil {
return nil, err
}
return principal, nil
}
// sessionToken reads the session access token from its cookie, or from the Authorization header when the cookie is absent
@@ -91,10 +119,11 @@ func sessionToken(c *gin.Context) string {
// APIKeyAuthenticator authenticates a personal API key sent in the X-API-Key header
type APIKeyAuthenticator struct {
apiKeyModule *apikey.Module
appConfig appconfig.AppConfigResolver
}
func NewAPIKeyAuthenticator(apiKeyModule *apikey.Module) *APIKeyAuthenticator {
return &APIKeyAuthenticator{apiKeyModule: apiKeyModule}
func NewAPIKeyAuthenticator(apiKeyModule *apikey.Module, appConfig appconfig.AppConfigResolver) *APIKeyAuthenticator {
return &APIKeyAuthenticator{apiKeyModule: apiKeyModule, appConfig: appConfig}
}
func (a *APIKeyAuthenticator) Kind() authz.PrincipalKind {
@@ -114,9 +143,15 @@ func (a *APIKeyAuthenticator) Authenticate(c *gin.Context) (*authz.Principal, er
return nil, apperror.UserDisabled()
}
return &authz.Principal{
principal := &authz.Principal{
Kind: authz.KindAPIKey,
UserID: user.ID,
Scopes: authz.UserScopes(user.IsAdmin, authz.KindAPIKey),
}, nil
}
err = restrictUnverified(c.Request.Context(), a.appConfig, &user, principal)
if err != nil {
return nil, err
}
return principal, nil
}