Files
pocket-id/backend/internal/middleware/authenticator.go
T

123 lines
3.9 KiB
Go

package middleware
import (
"strings"
"github.com/gin-gonic/gin"
"github.com/pocket-id/pocket-id/backend/internal/apikey"
"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/service"
"github.com/pocket-id/pocket-id/backend/internal/utils/cookie"
)
// #nosec G101 -- this is the name of the header that carries the API key, not a credential
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 {
return authz.NewMiddleware(
NewSessionAuthenticator(jwtService, userService),
NewAPIKeyAuthenticator(apiKeyModule),
)
}
// SessionAuthenticator authenticates the session access token Pocket ID issues after sign-in
type SessionAuthenticator struct {
jwtService *service.JwtService
userService *service.UserService
}
func NewSessionAuthenticator(jwtService *service.JwtService, userService *service.UserService) *SessionAuthenticator {
return &SessionAuthenticator{jwtService: jwtService, userService: userService}
}
func (a *SessionAuthenticator) Kind() authz.PrincipalKind {
return authz.KindSession
}
func (a *SessionAuthenticator) Present(c *gin.Context) bool {
return sessionToken(c) != ""
}
func (a *SessionAuthenticator) Authenticate(c *gin.Context) (*authz.Principal, error) {
// Verify the token signature, audience and type
token, err := a.jwtService.VerifyAccessToken(sessionToken(c))
if err != nil {
return nil, apperror.NotSignedIn()
}
authenticationMethod, err := a.jwtService.GetAuthenticationMethod(token)
if err != nil {
return nil, apperror.NotSignedIn()
}
authenticationTime, _ := token.IssuedAt()
subject, ok := token.Subject()
if !ok {
return nil, apperror.TokenInvalid()
}
// Load the user so disabling an account or changing its admin flag takes effect before the token expires
user, err := a.userService.GetUser(c, subject)
if err != nil {
return nil, apperror.NotSignedIn()
}
if user.Disabled {
return nil, apperror.UserDisabled()
}
return &authz.Principal{
Kind: authz.KindSession,
UserID: user.ID,
Scopes: authz.UserScopes(user.IsAdmin, authz.KindSession),
AuthenticationMethod: authenticationMethod,
AuthenticationTime: authenticationTime,
}, nil
}
// sessionToken reads the session access token from its cookie, or from the Authorization header when the cookie is absent
// An invalid cookie deliberately does not fall back to the header
func sessionToken(c *gin.Context) string {
if accessToken, err := c.Cookie(cookie.AccessTokenCookieName); err == nil {
return accessToken
}
_, accessToken, _ := strings.Cut(c.GetHeader("Authorization"), " ")
return accessToken
}
// APIKeyAuthenticator authenticates a personal API key sent in the X-API-Key header
type APIKeyAuthenticator struct {
apiKeyModule *apikey.Module
}
func NewAPIKeyAuthenticator(apiKeyModule *apikey.Module) *APIKeyAuthenticator {
return &APIKeyAuthenticator{apiKeyModule: apiKeyModule}
}
func (a *APIKeyAuthenticator) Kind() authz.PrincipalKind {
return authz.KindAPIKey
}
func (a *APIKeyAuthenticator) Present(c *gin.Context) bool {
return c.GetHeader(apiKeyHeader) != ""
}
func (a *APIKeyAuthenticator) Authenticate(c *gin.Context) (*authz.Principal, error) {
user, err := a.apiKeyModule.ValidateApiKey(c.Request.Context(), c.GetHeader(apiKeyHeader))
if err != nil {
return nil, apperror.NotSignedIn()
}
if user.Disabled {
return nil, apperror.UserDisabled()
}
return &authz.Principal{
Kind: authz.KindAPIKey,
UserID: user.ID,
Scopes: authz.UserScopes(user.IsAdmin, authz.KindAPIKey),
}, nil
}