@insureco/cli 0.1.11 → 0.1.12
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/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +39 -65
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/sample.js +3 -3
- package/dist/commands/sample.js.map +1 -1
- package/dist/lib/builder.js +12 -9
- package/dist/lib/builder.js.map +1 -1
- package/dist/templates/nextjs-oauth/.dockerignore +6 -0
- package/dist/templates/nextjs-oauth/.env.example +9 -11
- package/dist/templates/nextjs-oauth/README.md +72 -85
- package/dist/templates/nextjs-oauth/helm/Chart.yaml +6 -0
- package/dist/templates/nextjs-oauth/helm/templates/_helpers.tpl +49 -0
- package/dist/templates/nextjs-oauth/helm/templates/configmap.yaml +10 -0
- package/dist/templates/nextjs-oauth/helm/templates/deployment.yaml +56 -0
- package/dist/templates/nextjs-oauth/helm/templates/ingress.yaml +41 -0
- package/dist/templates/nextjs-oauth/helm/templates/service.yaml +15 -0
- package/dist/templates/nextjs-oauth/helm/values.yaml +69 -0
- package/dist/templates/nextjs-oauth/package.json +1 -1
- package/dist/templates/nextjs-oauth/src/app/api/auth/callback/route.ts +22 -9
- package/dist/templates/nextjs-oauth/src/app/api/auth/logout/route.ts +2 -4
- package/dist/templates/nextjs-oauth/src/app/api/auth/session/route.ts +5 -3
- package/dist/templates/nextjs-oauth/src/lib/auth.ts +80 -69
- package/dist/templates/nextjs-oauth/src/middleware.ts +7 -6
- package/dist/types/index.d.ts +9 -5
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
apiVersion: apps/v1
|
|
2
|
+
kind: Deployment
|
|
3
|
+
metadata:
|
|
4
|
+
name: {{ include "app.fullname" . }}
|
|
5
|
+
labels:
|
|
6
|
+
{{- include "app.labels" . | nindent 4 }}
|
|
7
|
+
spec:
|
|
8
|
+
{{- if not .Values.autoscaling.enabled }}
|
|
9
|
+
replicas: {{ .Values.replicaCount }}
|
|
10
|
+
{{- end }}
|
|
11
|
+
selector:
|
|
12
|
+
matchLabels:
|
|
13
|
+
{{- include "app.selectorLabels" . | nindent 6 }}
|
|
14
|
+
template:
|
|
15
|
+
metadata:
|
|
16
|
+
labels:
|
|
17
|
+
{{- include "app.labels" . | nindent 8 }}
|
|
18
|
+
spec:
|
|
19
|
+
{{- with .Values.imagePullSecrets }}
|
|
20
|
+
imagePullSecrets:
|
|
21
|
+
{{- toYaml . | nindent 8 }}
|
|
22
|
+
{{- end }}
|
|
23
|
+
containers:
|
|
24
|
+
- name: {{ .Chart.Name }}
|
|
25
|
+
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
|
26
|
+
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
|
27
|
+
ports:
|
|
28
|
+
- name: http
|
|
29
|
+
containerPort: {{ .Values.service.port }}
|
|
30
|
+
protocol: TCP
|
|
31
|
+
envFrom:
|
|
32
|
+
- configMapRef:
|
|
33
|
+
name: {{ include "app.fullname" . }}-config
|
|
34
|
+
{{- if .Values.secretRef }}
|
|
35
|
+
- secretRef:
|
|
36
|
+
name: {{ .Values.secretRef }}
|
|
37
|
+
optional: true
|
|
38
|
+
{{- end }}
|
|
39
|
+
livenessProbe:
|
|
40
|
+
{{- toYaml .Values.livenessProbe | nindent 12 }}
|
|
41
|
+
readinessProbe:
|
|
42
|
+
{{- toYaml .Values.readinessProbe | nindent 12 }}
|
|
43
|
+
resources:
|
|
44
|
+
{{- toYaml .Values.resources | nindent 12 }}
|
|
45
|
+
{{- with .Values.nodeSelector }}
|
|
46
|
+
nodeSelector:
|
|
47
|
+
{{- toYaml . | nindent 8 }}
|
|
48
|
+
{{- end }}
|
|
49
|
+
{{- with .Values.tolerations }}
|
|
50
|
+
tolerations:
|
|
51
|
+
{{- toYaml . | nindent 8 }}
|
|
52
|
+
{{- end }}
|
|
53
|
+
{{- with .Values.affinity }}
|
|
54
|
+
affinity:
|
|
55
|
+
{{- toYaml . | nindent 8 }}
|
|
56
|
+
{{- end }}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{{- if .Values.ingress.enabled -}}
|
|
2
|
+
apiVersion: networking.k8s.io/v1
|
|
3
|
+
kind: Ingress
|
|
4
|
+
metadata:
|
|
5
|
+
name: {{ include "app.fullname" . }}
|
|
6
|
+
labels:
|
|
7
|
+
{{- include "app.labels" . | nindent 4 }}
|
|
8
|
+
{{- with .Values.ingress.annotations }}
|
|
9
|
+
annotations:
|
|
10
|
+
{{- toYaml . | nindent 4 }}
|
|
11
|
+
{{- end }}
|
|
12
|
+
spec:
|
|
13
|
+
{{- if .Values.ingress.className }}
|
|
14
|
+
ingressClassName: {{ .Values.ingress.className }}
|
|
15
|
+
{{- end }}
|
|
16
|
+
{{- if .Values.ingress.tls }}
|
|
17
|
+
tls:
|
|
18
|
+
{{- range .Values.ingress.tls }}
|
|
19
|
+
- hosts:
|
|
20
|
+
{{- range .hosts }}
|
|
21
|
+
- {{ . | quote }}
|
|
22
|
+
{{- end }}
|
|
23
|
+
secretName: {{ .secretName }}
|
|
24
|
+
{{- end }}
|
|
25
|
+
{{- end }}
|
|
26
|
+
rules:
|
|
27
|
+
{{- range .Values.ingress.hosts }}
|
|
28
|
+
- host: {{ .host | quote }}
|
|
29
|
+
http:
|
|
30
|
+
paths:
|
|
31
|
+
{{- range .paths }}
|
|
32
|
+
- path: {{ .path }}
|
|
33
|
+
pathType: {{ .pathType }}
|
|
34
|
+
backend:
|
|
35
|
+
service:
|
|
36
|
+
name: {{ include "app.fullname" $ }}
|
|
37
|
+
port:
|
|
38
|
+
number: {{ $.Values.service.port }}
|
|
39
|
+
{{- end }}
|
|
40
|
+
{{- end }}
|
|
41
|
+
{{- end }}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
apiVersion: v1
|
|
2
|
+
kind: Service
|
|
3
|
+
metadata:
|
|
4
|
+
name: {{ include "app.fullname" . }}
|
|
5
|
+
labels:
|
|
6
|
+
{{- include "app.labels" . | nindent 4 }}
|
|
7
|
+
spec:
|
|
8
|
+
type: {{ .Values.service.type }}
|
|
9
|
+
ports:
|
|
10
|
+
- port: {{ .Values.service.port }}
|
|
11
|
+
targetPort: http
|
|
12
|
+
protocol: TCP
|
|
13
|
+
name: http
|
|
14
|
+
selector:
|
|
15
|
+
{{- include "app.selectorLabels" . | nindent 4 }}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
replicaCount: 1
|
|
2
|
+
|
|
3
|
+
image:
|
|
4
|
+
repository: registry.digitalocean.com/insureco/{{name}}
|
|
5
|
+
pullPolicy: IfNotPresent
|
|
6
|
+
tag: "latest"
|
|
7
|
+
|
|
8
|
+
imagePullSecrets:
|
|
9
|
+
- name: insureco
|
|
10
|
+
|
|
11
|
+
nameOverride: ""
|
|
12
|
+
fullnameOverride: ""
|
|
13
|
+
|
|
14
|
+
service:
|
|
15
|
+
type: ClusterIP
|
|
16
|
+
port: 3000
|
|
17
|
+
|
|
18
|
+
ingress:
|
|
19
|
+
enabled: true
|
|
20
|
+
className: nginx
|
|
21
|
+
annotations:
|
|
22
|
+
nginx.ingress.kubernetes.io/proxy-body-size: "10m"
|
|
23
|
+
nginx.ingress.kubernetes.io/proxy-read-timeout: "60"
|
|
24
|
+
hosts:
|
|
25
|
+
- host: {{name}}.sandbox.tawa.insureco.io
|
|
26
|
+
paths:
|
|
27
|
+
- path: /
|
|
28
|
+
pathType: Prefix
|
|
29
|
+
tls: []
|
|
30
|
+
|
|
31
|
+
resources:
|
|
32
|
+
limits:
|
|
33
|
+
cpu: 500m
|
|
34
|
+
memory: 512Mi
|
|
35
|
+
requests:
|
|
36
|
+
cpu: 100m
|
|
37
|
+
memory: 256Mi
|
|
38
|
+
|
|
39
|
+
autoscaling:
|
|
40
|
+
enabled: false
|
|
41
|
+
|
|
42
|
+
env:
|
|
43
|
+
NODE_ENV: production
|
|
44
|
+
PORT: "3000"
|
|
45
|
+
HOSTNAME: "0.0.0.0"
|
|
46
|
+
BIO_ID_URL: "https://bio.tawa.insureco.io"
|
|
47
|
+
|
|
48
|
+
# K8s Secret containing OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, JWT_SECRET
|
|
49
|
+
# Create with: kubectl create secret generic {{name}}-secrets --from-env-file=.env -n <namespace>
|
|
50
|
+
secretRef: {{name}}-secrets
|
|
51
|
+
|
|
52
|
+
livenessProbe:
|
|
53
|
+
httpGet:
|
|
54
|
+
path: /api/health
|
|
55
|
+
port: http
|
|
56
|
+
initialDelaySeconds: 15
|
|
57
|
+
periodSeconds: 10
|
|
58
|
+
failureThreshold: 3
|
|
59
|
+
|
|
60
|
+
readinessProbe:
|
|
61
|
+
httpGet:
|
|
62
|
+
path: /api/health
|
|
63
|
+
port: http
|
|
64
|
+
initialDelaySeconds: 5
|
|
65
|
+
periodSeconds: 5
|
|
66
|
+
|
|
67
|
+
nodeSelector: {}
|
|
68
|
+
tolerations: []
|
|
69
|
+
affinity: {}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server'
|
|
2
2
|
import { cookies } from 'next/headers'
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import {
|
|
4
|
+
exchangeCodeForTokens,
|
|
5
|
+
fetchUserInfo,
|
|
6
|
+
createSessionToken,
|
|
7
|
+
setSessionCookies,
|
|
8
|
+
sanitizeReturnTo,
|
|
9
|
+
getAppUrl,
|
|
10
|
+
} from '@/lib/auth'
|
|
6
11
|
|
|
7
12
|
export async function GET(request: NextRequest) {
|
|
8
13
|
const searchParams = request.nextUrl.searchParams
|
|
@@ -23,7 +28,7 @@ export async function GET(request: NextRequest) {
|
|
|
23
28
|
|
|
24
29
|
// Handle error from authorization server
|
|
25
30
|
if (error) {
|
|
26
|
-
const errorUrl = new URL('/',
|
|
31
|
+
const errorUrl = new URL('/', getAppUrl())
|
|
27
32
|
errorUrl.searchParams.set('error', error)
|
|
28
33
|
if (errorDescription) {
|
|
29
34
|
errorUrl.searchParams.set('error_description', errorDescription)
|
|
@@ -33,7 +38,7 @@ export async function GET(request: NextRequest) {
|
|
|
33
38
|
|
|
34
39
|
// Validate state to prevent CSRF
|
|
35
40
|
if (!state || state !== storedState) {
|
|
36
|
-
const errorUrl = new URL('/',
|
|
41
|
+
const errorUrl = new URL('/', getAppUrl())
|
|
37
42
|
errorUrl.searchParams.set('error', 'invalid_state')
|
|
38
43
|
errorUrl.searchParams.set('error_description', 'State parameter mismatch')
|
|
39
44
|
return NextResponse.redirect(errorUrl)
|
|
@@ -41,19 +46,27 @@ export async function GET(request: NextRequest) {
|
|
|
41
46
|
|
|
42
47
|
// Validate required params
|
|
43
48
|
if (!code || !codeVerifier) {
|
|
44
|
-
const errorUrl = new URL('/',
|
|
49
|
+
const errorUrl = new URL('/', getAppUrl())
|
|
45
50
|
errorUrl.searchParams.set('error', 'invalid_request')
|
|
46
51
|
errorUrl.searchParams.set('error_description', 'Missing authorization code')
|
|
47
52
|
return NextResponse.redirect(errorUrl)
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
try {
|
|
56
|
+
// Exchange code for bio-id tokens
|
|
51
57
|
const tokens = await exchangeCodeForTokens(code, codeVerifier)
|
|
52
|
-
|
|
58
|
+
|
|
59
|
+
// Fetch user info from bio-id using the access token
|
|
60
|
+
const user = await fetchUserInfo(tokens.access_token)
|
|
61
|
+
|
|
62
|
+
// Create our own session JWT and store it (not bio-id's access token)
|
|
63
|
+
const sessionToken = await createSessionToken(user)
|
|
64
|
+
await setSessionCookies(sessionToken, tokens.refresh_token)
|
|
65
|
+
|
|
53
66
|
const safeReturnTo = sanitizeReturnTo(returnTo)
|
|
54
|
-
return NextResponse.redirect(new URL(safeReturnTo,
|
|
67
|
+
return NextResponse.redirect(new URL(safeReturnTo, getAppUrl()))
|
|
55
68
|
} catch (err) {
|
|
56
|
-
const errorUrl = new URL('/',
|
|
69
|
+
const errorUrl = new URL('/', getAppUrl())
|
|
57
70
|
errorUrl.searchParams.set('error', 'token_exchange_failed')
|
|
58
71
|
errorUrl.searchParams.set(
|
|
59
72
|
'error_description',
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
-
import { clearAuthCookies } from '@/lib/auth'
|
|
3
|
-
|
|
4
|
-
const APP_URL = process.env.APP_URL || 'http://localhost:3000'
|
|
2
|
+
import { clearAuthCookies, getAppUrl } from '@/lib/auth'
|
|
5
3
|
|
|
6
4
|
export async function POST() {
|
|
7
5
|
await clearAuthCookies()
|
|
8
|
-
return NextResponse.redirect(new URL('/',
|
|
6
|
+
return NextResponse.redirect(new URL('/', getAppUrl()), { status: 303 })
|
|
9
7
|
}
|
|
@@ -3,8 +3,9 @@ import { cookies } from 'next/headers'
|
|
|
3
3
|
import {
|
|
4
4
|
getCurrentUser,
|
|
5
5
|
refreshAccessToken,
|
|
6
|
-
setAuthCookies,
|
|
7
6
|
fetchUserInfo,
|
|
7
|
+
createSessionToken,
|
|
8
|
+
setSessionCookies,
|
|
8
9
|
COOKIE_PREFIX,
|
|
9
10
|
} from '@/lib/auth'
|
|
10
11
|
|
|
@@ -18,15 +19,16 @@ export async function GET() {
|
|
|
18
19
|
return NextResponse.json({ user }, { headers: NO_CACHE_HEADERS })
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
//
|
|
22
|
+
// Session expired — try refreshing via bio-id refresh token
|
|
22
23
|
const cookieStore = await cookies()
|
|
23
24
|
const refreshToken = cookieStore.get(`${COOKIE_PREFIX}_refresh_token`)?.value
|
|
24
25
|
|
|
25
26
|
if (refreshToken) {
|
|
26
27
|
try {
|
|
27
28
|
const tokens = await refreshAccessToken(refreshToken)
|
|
28
|
-
await setAuthCookies(tokens.access_token, tokens.refresh_token, tokens.expires_in)
|
|
29
29
|
const refreshedUser = await fetchUserInfo(tokens.access_token)
|
|
30
|
+
const sessionToken = await createSessionToken(refreshedUser)
|
|
31
|
+
await setSessionCookies(sessionToken, tokens.refresh_token)
|
|
30
32
|
return NextResponse.json({ user: refreshedUser }, { headers: NO_CACHE_HEADERS })
|
|
31
33
|
} catch {
|
|
32
34
|
// Refresh token also invalid — user must re-login
|
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import { cookies } from 'next/headers'
|
|
2
|
-
import { jwtVerify } from 'jose'
|
|
2
|
+
import { SignJWT, jwtVerify } from 'jose'
|
|
3
3
|
import crypto from 'crypto'
|
|
4
4
|
|
|
5
|
-
// Configuration —
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
// Configuration — read lazily so `next build` doesn't fail when env vars are
|
|
6
|
+
// absent (they are injected at runtime via ConfigMap / Secret in K8s).
|
|
7
|
+
function getBioIdUrl(): string {
|
|
8
|
+
return process.env.BIO_ID_URL || 'http://localhost:6100'
|
|
9
|
+
}
|
|
10
|
+
function getAppUrl(): string {
|
|
11
|
+
return process.env.APP_URL || 'http://localhost:3000'
|
|
12
|
+
}
|
|
13
|
+
function getClientId(): string {
|
|
14
|
+
const id = process.env.OAUTH_CLIENT_ID
|
|
15
|
+
if (!id) throw new Error('OAUTH_CLIENT_ID is not configured')
|
|
16
|
+
return id
|
|
17
|
+
}
|
|
18
|
+
function getClientSecret(): string {
|
|
19
|
+
const secret = process.env.OAUTH_CLIENT_SECRET
|
|
20
|
+
if (!secret) throw new Error('OAUTH_CLIENT_SECRET is not configured')
|
|
21
|
+
return secret
|
|
22
|
+
}
|
|
11
23
|
function getJwtSecret(): Uint8Array {
|
|
12
24
|
const secret = process.env.JWT_SECRET
|
|
13
25
|
if (!secret && process.env.NODE_ENV === 'production') {
|
|
@@ -16,26 +28,22 @@ function getJwtSecret(): Uint8Array {
|
|
|
16
28
|
return new TextEncoder().encode(secret || 'dev-only-secret-do-not-use-in-production')
|
|
17
29
|
}
|
|
18
30
|
|
|
19
|
-
const JWT_SECRET = getJwtSecret()
|
|
20
|
-
|
|
21
|
-
if (process.env.NODE_ENV === 'production' && (!CLIENT_ID || !CLIENT_SECRET)) {
|
|
22
|
-
throw new Error('OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET are required in production')
|
|
23
|
-
}
|
|
24
|
-
|
|
25
31
|
const COOKIE_PREFIX = '{{name}}'
|
|
32
|
+
const SESSION_MAX_AGE = 60 * 60 // 1 hour
|
|
26
33
|
const REFRESH_TOKEN_MAX_AGE = 60 * 60 * 24 * 30 // 30 days
|
|
27
|
-
const OAUTH_FLOW_MAX_AGE = 60 * 10
|
|
34
|
+
const OAUTH_FLOW_MAX_AGE = 60 * 10 // 10 minutes
|
|
28
35
|
|
|
29
36
|
export interface User {
|
|
30
|
-
id: string
|
|
37
|
+
id: string
|
|
31
38
|
email: string
|
|
32
39
|
name: string
|
|
33
40
|
roles: string[]
|
|
34
41
|
}
|
|
35
42
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Helpers
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
39
47
|
export function sanitizeReturnTo(returnTo: string | undefined, fallback = '/dashboard'): string {
|
|
40
48
|
if (!returnTo || !returnTo.startsWith('/') || returnTo.startsWith('//')) {
|
|
41
49
|
return fallback
|
|
@@ -43,34 +51,26 @@ export function sanitizeReturnTo(returnTo: string | undefined, fallback = '/dash
|
|
|
43
51
|
return returnTo
|
|
44
52
|
}
|
|
45
53
|
|
|
46
|
-
/**
|
|
47
|
-
* Generate PKCE code verifier and S256 challenge
|
|
48
|
-
*/
|
|
49
54
|
export function generatePKCE(): { codeVerifier: string; codeChallenge: string } {
|
|
50
55
|
const codeVerifier = crypto.randomBytes(32).toString('base64url')
|
|
51
56
|
const codeChallenge = crypto
|
|
52
57
|
.createHash('sha256')
|
|
53
58
|
.update(codeVerifier)
|
|
54
59
|
.digest('base64url')
|
|
55
|
-
|
|
56
60
|
return { codeVerifier, codeChallenge }
|
|
57
61
|
}
|
|
58
62
|
|
|
59
|
-
/**
|
|
60
|
-
* Build Bio-id authorization URL
|
|
61
|
-
*/
|
|
62
63
|
export function getAuthorizationUrl(state: string, codeChallenge: string): string {
|
|
63
64
|
const params = new URLSearchParams({
|
|
64
|
-
client_id:
|
|
65
|
-
redirect_uri: `${
|
|
65
|
+
client_id: getClientId(),
|
|
66
|
+
redirect_uri: `${getAppUrl()}/api/auth/callback`,
|
|
66
67
|
response_type: 'code',
|
|
67
68
|
scope: 'openid profile email',
|
|
68
69
|
state,
|
|
69
70
|
code_challenge: codeChallenge,
|
|
70
71
|
code_challenge_method: 'S256',
|
|
71
72
|
})
|
|
72
|
-
|
|
73
|
-
return `${BIO_ID_URL}/oauth/authorize?${params.toString()}`
|
|
73
|
+
return `${getBioIdUrl()}/oauth/authorize?${params.toString()}`
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
async function parseErrorResponse(response: Response, fallbackMessage: string): Promise<string> {
|
|
@@ -82,9 +82,10 @@ async function parseErrorResponse(response: Response, fallbackMessage: string):
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Bio-id API calls
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
|
|
88
89
|
export async function exchangeCodeForTokens(
|
|
89
90
|
code: string,
|
|
90
91
|
codeVerifier: string
|
|
@@ -96,15 +97,15 @@ export async function exchangeCodeForTokens(
|
|
|
96
97
|
scope: string
|
|
97
98
|
id_token?: string
|
|
98
99
|
}> {
|
|
99
|
-
const response = await fetch(`${
|
|
100
|
+
const response = await fetch(`${getBioIdUrl()}/api/oauth/token`, {
|
|
100
101
|
method: 'POST',
|
|
101
102
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
102
103
|
body: new URLSearchParams({
|
|
103
104
|
grant_type: 'authorization_code',
|
|
104
105
|
code,
|
|
105
|
-
redirect_uri: `${
|
|
106
|
-
client_id:
|
|
107
|
-
client_secret:
|
|
106
|
+
redirect_uri: `${getAppUrl()}/api/auth/callback`,
|
|
107
|
+
client_id: getClientId(),
|
|
108
|
+
client_secret: getClientSecret(),
|
|
108
109
|
code_verifier: codeVerifier,
|
|
109
110
|
}).toString(),
|
|
110
111
|
})
|
|
@@ -116,9 +117,6 @@ export async function exchangeCodeForTokens(
|
|
|
116
117
|
return response.json()
|
|
117
118
|
}
|
|
118
119
|
|
|
119
|
-
/**
|
|
120
|
-
* Refresh an expired access token
|
|
121
|
-
*/
|
|
122
120
|
export async function refreshAccessToken(refreshToken: string): Promise<{
|
|
123
121
|
access_token: string
|
|
124
122
|
token_type: string
|
|
@@ -126,14 +124,14 @@ export async function refreshAccessToken(refreshToken: string): Promise<{
|
|
|
126
124
|
refresh_token: string
|
|
127
125
|
scope: string
|
|
128
126
|
}> {
|
|
129
|
-
const response = await fetch(`${
|
|
127
|
+
const response = await fetch(`${getBioIdUrl()}/api/oauth/token`, {
|
|
130
128
|
method: 'POST',
|
|
131
129
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
132
130
|
body: new URLSearchParams({
|
|
133
131
|
grant_type: 'refresh_token',
|
|
134
132
|
refresh_token: refreshToken,
|
|
135
|
-
client_id:
|
|
136
|
-
client_secret:
|
|
133
|
+
client_id: getClientId(),
|
|
134
|
+
client_secret: getClientSecret(),
|
|
137
135
|
}).toString(),
|
|
138
136
|
})
|
|
139
137
|
|
|
@@ -144,11 +142,8 @@ export async function refreshAccessToken(refreshToken: string): Promise<{
|
|
|
144
142
|
return response.json()
|
|
145
143
|
}
|
|
146
144
|
|
|
147
|
-
/**
|
|
148
|
-
* Fetch user info from Bio-id userinfo endpoint
|
|
149
|
-
*/
|
|
150
145
|
export async function fetchUserInfo(accessToken: string): Promise<User> {
|
|
151
|
-
const response = await fetch(`${
|
|
146
|
+
const response = await fetch(`${getBioIdUrl()}/api/oauth/userinfo`, {
|
|
152
147
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
153
148
|
})
|
|
154
149
|
|
|
@@ -166,26 +161,44 @@ export async function fetchUserInfo(accessToken: string): Promise<User> {
|
|
|
166
161
|
}
|
|
167
162
|
}
|
|
168
163
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// Session management
|
|
166
|
+
//
|
|
167
|
+
// After the OAuth callback, we create OUR OWN session JWT signed with
|
|
168
|
+
// JWT_SECRET. This avoids needing to verify bio-id's access token locally
|
|
169
|
+
// (bio-id uses HS256 signed with its own server secret).
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
export async function createSessionToken(user: User): Promise<string> {
|
|
173
|
+
return new SignJWT({
|
|
174
|
+
sub: user.id,
|
|
175
|
+
email: user.email,
|
|
176
|
+
name: user.name,
|
|
177
|
+
roles: user.roles,
|
|
178
|
+
})
|
|
179
|
+
.setProtectedHeader({ alg: 'HS256' })
|
|
180
|
+
.setIssuedAt()
|
|
181
|
+
.setExpirationTime(`${SESSION_MAX_AGE}s`)
|
|
182
|
+
.setIssuer(getAppUrl())
|
|
183
|
+
.sign(getJwtSecret())
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export async function setSessionCookies(
|
|
187
|
+
sessionToken: string,
|
|
188
|
+
bioRefreshToken: string
|
|
176
189
|
): Promise<void> {
|
|
177
190
|
const cookieStore = await cookies()
|
|
178
|
-
const secure =
|
|
191
|
+
const secure = getAppUrl().startsWith('https://')
|
|
179
192
|
|
|
180
|
-
cookieStore.set(`${COOKIE_PREFIX}
|
|
193
|
+
cookieStore.set(`${COOKIE_PREFIX}_session`, sessionToken, {
|
|
181
194
|
httpOnly: true,
|
|
182
195
|
secure,
|
|
183
196
|
sameSite: 'lax',
|
|
184
|
-
maxAge:
|
|
197
|
+
maxAge: SESSION_MAX_AGE,
|
|
185
198
|
path: '/',
|
|
186
199
|
})
|
|
187
200
|
|
|
188
|
-
cookieStore.set(`${COOKIE_PREFIX}_refresh_token`,
|
|
201
|
+
cookieStore.set(`${COOKIE_PREFIX}_refresh_token`, bioRefreshToken, {
|
|
189
202
|
httpOnly: true,
|
|
190
203
|
secure,
|
|
191
204
|
sameSite: 'lax',
|
|
@@ -194,12 +207,9 @@ export async function setAuthCookies(
|
|
|
194
207
|
})
|
|
195
208
|
}
|
|
196
209
|
|
|
197
|
-
/**
|
|
198
|
-
* Clear all auth cookies
|
|
199
|
-
*/
|
|
200
210
|
export async function clearAuthCookies(): Promise<void> {
|
|
201
211
|
const cookieStore = await cookies()
|
|
202
|
-
cookieStore.delete(`${COOKIE_PREFIX}
|
|
212
|
+
cookieStore.delete(`${COOKIE_PREFIX}_session`)
|
|
203
213
|
cookieStore.delete(`${COOKIE_PREFIX}_refresh_token`)
|
|
204
214
|
cookieStore.delete('oauth_state')
|
|
205
215
|
cookieStore.delete('oauth_code_verifier')
|
|
@@ -207,23 +217,24 @@ export async function clearAuthCookies(): Promise<void> {
|
|
|
207
217
|
}
|
|
208
218
|
|
|
209
219
|
/**
|
|
210
|
-
* Get the current
|
|
220
|
+
* Get the current user from the session cookie (no network call).
|
|
221
|
+
* Verifies OUR session JWT — not bio-id's access token.
|
|
211
222
|
*/
|
|
212
223
|
export async function getCurrentUser(): Promise<User | null> {
|
|
213
224
|
const cookieStore = await cookies()
|
|
214
|
-
const
|
|
225
|
+
const sessionToken = cookieStore.get(`${COOKIE_PREFIX}_session`)?.value
|
|
215
226
|
|
|
216
|
-
if (!
|
|
227
|
+
if (!sessionToken) {
|
|
217
228
|
return null
|
|
218
229
|
}
|
|
219
230
|
|
|
220
231
|
try {
|
|
221
|
-
const { payload } = await jwtVerify(
|
|
222
|
-
issuer:
|
|
232
|
+
const { payload } = await jwtVerify(sessionToken, getJwtSecret(), {
|
|
233
|
+
issuer: getAppUrl(),
|
|
223
234
|
})
|
|
224
235
|
|
|
225
236
|
return {
|
|
226
|
-
id:
|
|
237
|
+
id: payload.sub || '',
|
|
227
238
|
email: payload.email as string,
|
|
228
239
|
name: payload.name as string,
|
|
229
240
|
roles: (payload.roles as string[]) || [],
|
|
@@ -238,7 +249,7 @@ export async function getCurrentUser(): Promise<User | null> {
|
|
|
238
249
|
*/
|
|
239
250
|
export async function getLoginUrl(returnTo?: string): Promise<string> {
|
|
240
251
|
const cookieStore = await cookies()
|
|
241
|
-
const secure =
|
|
252
|
+
const secure = getAppUrl().startsWith('https://')
|
|
242
253
|
|
|
243
254
|
const state = crypto.randomBytes(16).toString('hex')
|
|
244
255
|
const { codeVerifier, codeChallenge } = generatePKCE()
|
|
@@ -271,4 +282,4 @@ export async function getLoginUrl(returnTo?: string): Promise<string> {
|
|
|
271
282
|
return getAuthorizationUrl(state, codeChallenge)
|
|
272
283
|
}
|
|
273
284
|
|
|
274
|
-
export { COOKIE_PREFIX,
|
|
285
|
+
export { COOKIE_PREFIX, getBioIdUrl, getAppUrl }
|
|
@@ -36,24 +36,25 @@ export async function middleware(request: NextRequest) {
|
|
|
36
36
|
return NextResponse.next()
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
// Check for our session cookie (not bio-id's access token)
|
|
40
|
+
const sessionToken = request.cookies.get(`${COOKIE_PREFIX}_session`)?.value
|
|
40
41
|
|
|
41
|
-
if (!
|
|
42
|
+
if (!sessionToken) {
|
|
42
43
|
const loginUrl = new URL('/api/auth/login', request.url)
|
|
43
44
|
loginUrl.searchParams.set('returnTo', pathname)
|
|
44
45
|
return NextResponse.redirect(loginUrl)
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
// Verify JWT
|
|
48
|
+
// Verify our own session JWT (signed with JWT_SECRET, issued by APP_URL)
|
|
48
49
|
try {
|
|
49
50
|
const secret = new TextEncoder().encode(
|
|
50
51
|
process.env.JWT_SECRET || 'dev-only-secret-do-not-use-in-production'
|
|
51
52
|
)
|
|
52
|
-
await jwtVerify(
|
|
53
|
-
issuer: process.env.
|
|
53
|
+
await jwtVerify(sessionToken, secret, {
|
|
54
|
+
issuer: process.env.APP_URL || 'http://localhost:3000',
|
|
54
55
|
})
|
|
55
56
|
} catch {
|
|
56
|
-
//
|
|
57
|
+
// Session expired or invalid — redirect to login
|
|
57
58
|
const loginUrl = new URL('/api/auth/login', request.url)
|
|
58
59
|
loginUrl.searchParams.set('returnTo', pathname)
|
|
59
60
|
return NextResponse.redirect(loginUrl)
|
package/dist/types/index.d.ts
CHANGED
|
@@ -278,17 +278,21 @@ export interface Version {
|
|
|
278
278
|
updatedAt: string;
|
|
279
279
|
}
|
|
280
280
|
export interface PushRequest {
|
|
281
|
-
serviceId: string;
|
|
282
281
|
serviceName: string;
|
|
283
282
|
archive: Buffer;
|
|
284
|
-
|
|
283
|
+
environment?: string;
|
|
284
|
+
branch?: string;
|
|
285
285
|
message?: string;
|
|
286
286
|
}
|
|
287
287
|
export interface PushResponse {
|
|
288
|
-
|
|
289
|
-
|
|
288
|
+
buildId: string;
|
|
289
|
+
serviceId: string;
|
|
290
|
+
serviceName: string;
|
|
291
|
+
version: string;
|
|
292
|
+
environment: string;
|
|
293
|
+
tarballId: string;
|
|
294
|
+
status: string;
|
|
290
295
|
message: string;
|
|
291
|
-
size: number;
|
|
292
296
|
}
|
|
293
297
|
export interface DeployVersionResponse {
|
|
294
298
|
buildId: string;
|