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 +414 -8
- package/dist/auth.cjs +500 -6
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.d.cts +180 -19
- package/dist/auth.d.ts +180 -19
- package/dist/auth.js +493 -6
- package/dist/auth.js.map +1 -1
- package/dist/headers.cjs +277 -7
- package/dist/headers.cjs.map +1 -1
- package/dist/headers.d.cts +162 -25
- package/dist/headers.d.ts +162 -25
- package/dist/headers.js +267 -6
- package/dist/headers.js.map +1 -1
- package/dist/index.cjs +2685 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +2634 -2
- package/dist/index.js.map +1 -1
- package/dist/path-BVbunPfR.d.cts +534 -0
- package/dist/path-BVbunPfR.d.ts +534 -0
- package/dist/validation.cjs +2031 -0
- package/dist/validation.cjs.map +1 -0
- package/dist/validation.d.cts +42 -0
- package/dist/validation.d.ts +42 -0
- package/dist/validation.js +1964 -0
- package/dist/validation.js.map +1 -0
- package/package.json +14 -1
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
|
-
- [
|
|
746
|
-
- [
|
|
747
|
-
- [
|
|
748
|
-
- [
|
|
749
|
-
|
|
750
|
-
- [
|
|
751
|
-
- [
|
|
752
|
-
- [
|
|
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
|
|