package auth import ( "context" "errors" "fmt" "log/slog" "net/http" "time" "git.optclblast.xyz/draincloud/draincloud-core/internal/common" "git.optclblast.xyz/draincloud/draincloud-core/internal/errs" "git.optclblast.xyz/draincloud/draincloud-core/internal/logger" "git.optclblast.xyz/draincloud/draincloud-core/internal/storage" models "git.optclblast.xyz/draincloud/draincloud-core/internal/storage/models/auth" ) const ( AuthResolverV1Name = "auth.v1" ) const ( csrfTokenCookie = "__Csrf_token" sessionTokenCookie = "__Session_token" ) type AuthResolver struct { authStorage storage.AuthStorage } func NewAuthResolver(authStorage storage.AuthStorage) *AuthResolver { return &AuthResolver{ authStorage: authStorage, } } func (r *AuthResolver) Resolve(ctx context.Context, req *common.Request, _ any) error { return r.authorize(ctx, req) } func (r *AuthResolver) GetRequiredResolveParams() []string { return nil } func (p *AuthResolver) authorize(ctx context.Context, req *common.Request) error { session, err := p.getSession(ctx, req) if err != nil && !errors.Is(err, http.ErrNoCookie) { return errs.ErrorUnauthorized } if session == nil { return errs.ErrorUnauthorized } if err := validateSession(ctx, req, session); err != nil { // TODO add audit log entry return errs.ErrorUnauthorized } user, err := p.authStorage.GetUserByID(ctx, session.UserID) if err != nil { return fmt.Errorf("failed to fetch user by id: %w", err) } logger.Debug(ctx, "[authorize] user authorized", slog.String("session_id", session.ID.String())) req.User = user req.Session = session return nil } func (d *AuthResolver) getSession(ctx context.Context, req *common.Request) (*models.Session, error) { token, err := common.GetValue[string](req.Metadata, sessionTokenCookie) if err != nil { return nil, fmt.Errorf("failed to fetch session cookie from request: %w", err) } if len(token) == 0 { return nil, fmt.Errorf("session token or csrf token is empty") } session, err := d.authStorage.GetSession(ctx, token) if err != nil { return nil, fmt.Errorf("failed to fetch session from repo: %w", err) } return session, nil } func validateSession(_ context.Context, req *common.Request, session *models.Session) error { if session == nil { return errs.ErrorAccessDenied } csrfToken, err := common.GetValue[string](req.Metadata, csrfTokenCookie) if err != nil { return fmt.Errorf("failed to fetch csrf cookie from request: %w", err) } if session.CsrfToken != csrfToken { return errs.ErrorAccessDenied } if session.ExpiredAt.Before(time.Now()) { return errs.ErrorSessionExpired } return nil }