nextjs-secure 0.2.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -52,6 +52,16 @@ pnpm add nextjs-secure
52
52
  - [Client-Side Usage](#client-side-usage)
53
53
  - [Configuration](#configuration-1)
54
54
  - [Manual Validation](#manual-validation)
55
+ - [Security Headers](#security-headers)
56
+ - [Quick Start](#quick-start-1)
57
+ - [Presets](#presets)
58
+ - [Custom Configuration](#custom-configuration)
59
+ - [Authentication](#authentication)
60
+ - [JWT Authentication](#jwt-authentication)
61
+ - [API Key Authentication](#api-key-authentication)
62
+ - [Session Authentication](#session-authentication)
63
+ - [Role-Based Access Control](#role-based-access-control)
64
+ - [Combined Authentication](#combined-authentication)
55
65
  - [Utilities](#utilities)
56
66
  - [API Reference](#api-reference)
57
67
  - [Examples](#examples)
@@ -515,6 +525,393 @@ Set `CSRF_SECRET` in your environment:
515
525
  CSRF_SECRET=your-secret-key-min-32-chars-recommended
516
526
  ```
517
527
 
528
+ ## Security Headers
529
+
530
+ Add security headers to your responses with pre-configured presets or custom configuration.
531
+
532
+ ### Quick Start
533
+
534
+ ```typescript
535
+ import { withSecurityHeaders } from 'nextjs-secure/headers'
536
+
537
+ // Use strict preset (default)
538
+ export const GET = withSecurityHeaders(async (req) => {
539
+ return Response.json({ data: 'protected' })
540
+ })
541
+ ```
542
+
543
+ ### Presets
544
+
545
+ Three presets available: `strict`, `relaxed`, `api`
546
+
547
+ ```typescript
548
+ // Strict: Maximum security (default)
549
+ export const GET = withSecurityHeaders(handler, { preset: 'strict' })
550
+
551
+ // Relaxed: Development-friendly, allows inline scripts
552
+ export const GET = withSecurityHeaders(handler, { preset: 'relaxed' })
553
+
554
+ // API: Optimized for JSON APIs
555
+ export const GET = withSecurityHeaders(handler, { preset: 'api' })
556
+ ```
557
+
558
+ ### Custom Configuration
559
+
560
+ ```typescript
561
+ import { withSecurityHeaders } from 'nextjs-secure/headers'
562
+
563
+ export const GET = withSecurityHeaders(handler, {
564
+ config: {
565
+ // Content-Security-Policy
566
+ contentSecurityPolicy: {
567
+ defaultSrc: ["'self'"],
568
+ scriptSrc: ["'self'", "'unsafe-inline'"],
569
+ styleSrc: ["'self'", "'unsafe-inline'"],
570
+ imgSrc: ["'self'", 'data:', 'https:'],
571
+ },
572
+
573
+ // Strict-Transport-Security
574
+ strictTransportSecurity: {
575
+ maxAge: 31536000,
576
+ includeSubDomains: true,
577
+ preload: true,
578
+ },
579
+
580
+ // Other headers
581
+ xFrameOptions: 'DENY', // or 'SAMEORIGIN'
582
+ xContentTypeOptions: true, // X-Content-Type-Options: nosniff
583
+ referrerPolicy: 'strict-origin-when-cross-origin',
584
+
585
+ // Cross-Origin headers
586
+ crossOriginOpenerPolicy: 'same-origin',
587
+ crossOriginEmbedderPolicy: 'require-corp',
588
+ crossOriginResourcePolicy: 'same-origin',
589
+
590
+ // Permissions-Policy (disable features)
591
+ permissionsPolicy: {
592
+ camera: [],
593
+ microphone: [],
594
+ geolocation: [],
595
+ },
596
+ }
597
+ })
598
+ ```
599
+
600
+ ### Disable Specific Headers
601
+
602
+ ```typescript
603
+ export const GET = withSecurityHeaders(handler, {
604
+ config: {
605
+ contentSecurityPolicy: false, // Disable CSP
606
+ xFrameOptions: false, // Disable X-Frame-Options
607
+ }
608
+ })
609
+ ```
610
+
611
+ ### Manual Header Creation
612
+
613
+ ```typescript
614
+ import { createSecurityHeaders } from 'nextjs-secure/headers'
615
+
616
+ export async function GET() {
617
+ const headers = createSecurityHeaders({ preset: 'api' })
618
+
619
+ return new Response(JSON.stringify({ ok: true }), {
620
+ headers,
621
+ })
622
+ }
623
+ ```
624
+
625
+ ### Available Headers
626
+
627
+ | Header | Description |
628
+ |--------|-------------|
629
+ | Content-Security-Policy | Controls resources the page can load |
630
+ | Strict-Transport-Security | Forces HTTPS connections |
631
+ | X-Frame-Options | Prevents clickjacking |
632
+ | X-Content-Type-Options | Prevents MIME sniffing |
633
+ | Referrer-Policy | Controls referrer information |
634
+ | Permissions-Policy | Disables browser features |
635
+ | Cross-Origin-Opener-Policy | Isolates browsing context |
636
+ | Cross-Origin-Embedder-Policy | Controls embedding |
637
+ | Cross-Origin-Resource-Policy | Controls resource sharing |
638
+
639
+ ## Authentication
640
+
641
+ Flexible authentication middleware supporting JWT, API keys, session cookies, and role-based access control.
642
+
643
+ ### JWT Authentication
644
+
645
+ ```typescript
646
+ import { withJWT } from 'nextjs-secure/auth'
647
+
648
+ export const GET = withJWT(
649
+ async (req, ctx) => {
650
+ // ctx.user contains the authenticated user
651
+ return Response.json({ user: ctx.user })
652
+ },
653
+ {
654
+ secret: process.env.JWT_SECRET,
655
+ // or use publicKey for RS256/ES256
656
+ }
657
+ )
658
+ ```
659
+
660
+ #### Configuration
661
+
662
+ ```typescript
663
+ export const GET = withJWT(handler, {
664
+ // Secret for HMAC algorithms (HS256, HS384, HS512)
665
+ secret: process.env.JWT_SECRET,
666
+
667
+ // Public key for RSA/ECDSA (RS256, ES256, etc.)
668
+ publicKey: process.env.JWT_PUBLIC_KEY,
669
+
670
+ // Allowed algorithms (default: ['HS256'])
671
+ algorithms: ['HS256', 'RS256'],
672
+
673
+ // Validate issuer
674
+ issuer: 'https://myapp.com',
675
+ // or multiple issuers
676
+ issuer: ['https://auth.myapp.com', 'https://api.myapp.com'],
677
+
678
+ // Validate audience
679
+ audience: 'my-api',
680
+
681
+ // Clock tolerance in seconds (for exp/nbf claims)
682
+ clockTolerance: 30,
683
+
684
+ // Custom token extraction
685
+ getToken: (req) => req.headers.get('x-auth-token'),
686
+
687
+ // Custom user mapping from JWT payload
688
+ mapUser: (payload) => ({
689
+ id: payload.sub,
690
+ email: payload.email,
691
+ roles: payload.roles || [],
692
+ }),
693
+ })
694
+ ```
695
+
696
+ ### API Key Authentication
697
+
698
+ ```typescript
699
+ import { withAPIKey } from 'nextjs-secure/auth'
700
+
701
+ export const GET = withAPIKey(
702
+ async (req, ctx) => {
703
+ return Response.json({ user: ctx.user })
704
+ },
705
+ {
706
+ validate: async (apiKey, req) => {
707
+ // Return user object if valid, null if invalid
708
+ const user = await db.users.findByApiKey(apiKey)
709
+ return user || null
710
+ },
711
+ }
712
+ )
713
+ ```
714
+
715
+ #### Configuration
716
+
717
+ ```typescript
718
+ export const GET = withAPIKey(handler, {
719
+ // Required: validation function
720
+ validate: async (apiKey, req) => {
721
+ // Lookup API key and return user or null
722
+ return db.apiKeys.findUser(apiKey)
723
+ },
724
+
725
+ // Header name (default: 'x-api-key')
726
+ headerName: 'x-api-key',
727
+
728
+ // Query parameter name (default: 'api_key')
729
+ queryParam: 'api_key',
730
+ })
731
+ ```
732
+
733
+ API keys can be sent via header or query parameter:
734
+ ```bash
735
+ # Via header
736
+ curl -H "x-api-key: YOUR_API_KEY" https://api.example.com/data
737
+
738
+ # Via query parameter
739
+ curl https://api.example.com/data?api_key=YOUR_API_KEY
740
+ ```
741
+
742
+ ### Session Authentication
743
+
744
+ ```typescript
745
+ import { withSession } from 'nextjs-secure/auth'
746
+
747
+ export const GET = withSession(
748
+ async (req, ctx) => {
749
+ return Response.json({ user: ctx.user })
750
+ },
751
+ {
752
+ validate: async (sessionId, req) => {
753
+ // Return user object if session valid, null if invalid
754
+ const session = await db.sessions.find(sessionId)
755
+ return session?.user || null
756
+ },
757
+ }
758
+ )
759
+ ```
760
+
761
+ #### Configuration
762
+
763
+ ```typescript
764
+ export const GET = withSession(handler, {
765
+ // Required: session validation function
766
+ validate: async (sessionId, req) => {
767
+ const session = await redis.get(`session:${sessionId}`)
768
+ if (!session) return null
769
+ return JSON.parse(session)
770
+ },
771
+
772
+ // Cookie name (default: 'session')
773
+ cookieName: 'session',
774
+ })
775
+ ```
776
+
777
+ ### Role-Based Access Control
778
+
779
+ Use `withRoles` after an authentication middleware to enforce role/permission requirements.
780
+
781
+ ```typescript
782
+ import { withJWT, withRoles } from 'nextjs-secure/auth'
783
+
784
+ // Chain with JWT auth
785
+ const authenticatedHandler = withJWT(
786
+ withRoles(
787
+ async (req, ctx) => {
788
+ return Response.json({ admin: true })
789
+ },
790
+ { roles: ['admin'] }
791
+ ),
792
+ { secret: process.env.JWT_SECRET }
793
+ )
794
+
795
+ export const GET = authenticatedHandler
796
+ ```
797
+
798
+ #### Configuration
799
+
800
+ ```typescript
801
+ withRoles(handler, {
802
+ // Required roles (any match = authorized)
803
+ roles: ['admin', 'moderator'],
804
+
805
+ // Required permissions (all must match)
806
+ permissions: ['read', 'write'],
807
+
808
+ // Custom role extraction
809
+ getUserRoles: (user) => user.roles || [],
810
+
811
+ // Custom permission extraction
812
+ getUserPermissions: (user) => user.permissions || [],
813
+
814
+ // Custom authorization logic
815
+ authorize: async (user, req) => {
816
+ // Return true if authorized, false otherwise
817
+ return user.subscriptionTier === 'pro'
818
+ },
819
+ })
820
+ ```
821
+
822
+ ### Combined Authentication
823
+
824
+ Use `withAuth` for flexible multi-strategy authentication:
825
+
826
+ ```typescript
827
+ import { withAuth } from 'nextjs-secure/auth'
828
+
829
+ export const GET = withAuth(
830
+ async (req, ctx) => {
831
+ // Authenticated via any method
832
+ return Response.json({ user: ctx.user })
833
+ },
834
+ {
835
+ // Try JWT first
836
+ jwt: {
837
+ secret: process.env.JWT_SECRET,
838
+ },
839
+
840
+ // Fall back to API key
841
+ apiKey: {
842
+ validate: (key) => db.apiKeys.findUser(key),
843
+ },
844
+
845
+ // Fall back to session
846
+ session: {
847
+ validate: (id) => db.sessions.findUser(id),
848
+ },
849
+
850
+ // Optional RBAC
851
+ rbac: {
852
+ roles: ['user', 'admin'],
853
+ },
854
+
855
+ // Callbacks
856
+ onSuccess: async (req, user) => {
857
+ // Log successful auth
858
+ console.log(`Authenticated: ${user.id}`)
859
+ },
860
+
861
+ onError: (req, error) => {
862
+ // Custom error response
863
+ return Response.json({ error: error.message }, { status: error.status })
864
+ },
865
+ }
866
+ )
867
+ ```
868
+
869
+ ### Optional Authentication
870
+
871
+ For routes that work with or without authentication:
872
+
873
+ ```typescript
874
+ import { withOptionalAuth } from 'nextjs-secure/auth'
875
+
876
+ export const GET = withOptionalAuth(
877
+ async (req, ctx) => {
878
+ if (ctx.user) {
879
+ // Authenticated user
880
+ return Response.json({ user: ctx.user })
881
+ }
882
+ // Anonymous access
883
+ return Response.json({ guest: true })
884
+ },
885
+ {
886
+ jwt: { secret: process.env.JWT_SECRET },
887
+ }
888
+ )
889
+ ```
890
+
891
+ ### JWT Utilities
892
+
893
+ ```typescript
894
+ import { verifyJWT, decodeJWT, extractBearerToken } from 'nextjs-secure/auth'
895
+
896
+ // Verify and decode JWT
897
+ const { payload, error } = await verifyJWT(token, {
898
+ secret: process.env.JWT_SECRET,
899
+ issuer: 'myapp',
900
+ })
901
+
902
+ if (error) {
903
+ console.log(error.code) // 'expired_token', 'invalid_signature', etc.
904
+ }
905
+
906
+ // Decode without verification (for inspection only)
907
+ const decoded = decodeJWT(token)
908
+ // { header, payload, signature }
909
+
910
+ // Extract token from Authorization header
911
+ const token = extractBearerToken(req.headers.get('authorization'))
912
+ // 'Bearer xxx' -> 'xxx'
913
+ ```
914
+
518
915
  ## Utilities
519
916
 
520
917
  ### Duration Parsing
@@ -742,14 +1139,23 @@ export const POST = withRateLimit(
742
1139
  - [x] Memory store
743
1140
  - [x] Redis store
744
1141
  - [x] Upstash store
745
- - [ ] Authentication (v0.2.0)
746
- - [ ] JWT validation
747
- - [ ] Supabase provider
748
- - [ ] NextAuth provider
749
- - [ ] Clerk provider
750
- - [ ] RBAC support
751
- - [ ] CSRF Protection (v0.3.0)
752
- - [ ] Security Headers (v0.4.0)
1142
+ - [x] CSRF Protection (v0.2.0)
1143
+ - [x] Double submit cookie pattern
1144
+ - [x] Token generation/validation
1145
+ - [x] Configurable cookie settings
1146
+ - [x] Security Headers (v0.3.0)
1147
+ - [x] Content-Security-Policy
1148
+ - [x] Strict-Transport-Security
1149
+ - [x] X-Frame-Options, X-Content-Type-Options
1150
+ - [x] Permissions-Policy
1151
+ - [x] COOP, COEP, CORP
1152
+ - [x] Presets (strict, relaxed, api)
1153
+ - [x] Authentication (v0.4.0)
1154
+ - [x] JWT validation (HS256, RS256, ES256)
1155
+ - [x] API Key authentication
1156
+ - [x] Session/Cookie authentication
1157
+ - [x] Role-Based Access Control (RBAC)
1158
+ - [x] Combined multi-strategy auth
753
1159
  - [ ] Input Validation (v0.5.0)
754
1160
  - [ ] Audit Logging (v0.6.0)
755
1161