Tested KeyCloak validation - AUTH
Test Keycloak JWT validation against access token issued via NextAuth; ensure Spring Boot resource server correctly validates token signature, expiration, issuer, and optional audience.
This commit is contained in:
1 parent
92971a3983
commit
7306ad1b26
18 files changed
+621
-64
No files matched your search
@@ -8,6 +8,11 @@ public class BackendApplication {
|
|||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(BackendApplication.class, args);
|
SpringApplication.run(BackendApplication.class, args);
|
||||||
|
//call the test
|
||||||
|
test t = new test();
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.envsync.backend;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.config.Customizer;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||||
|
http.authorizeHttpRequests(auth -> auth
|
||||||
|
.requestMatchers("/api/secure").authenticated()
|
||||||
|
.anyRequest().permitAll()
|
||||||
|
)
|
||||||
|
.oauth2ResourceServer(oauth2 -> oauth2
|
||||||
|
.jwt(Customizer.withDefaults()) // uses issuer-uri/jwk-set-uri
|
||||||
|
);
|
||||||
|
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.envsync.backend;
|
||||||
|
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
public class test {
|
||||||
|
|
||||||
|
@GetMapping("/api/secure")
|
||||||
|
public String secure() {
|
||||||
|
//send json response
|
||||||
|
return "{\"message\": \"This is a secure endpoint!\"}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,32 @@
|
|||||||
spring.application.name=backend
|
spring.application.name=backend
|
||||||
|
# ----- Postgres -----
|
||||||
|
spring.datasource.url=jdbc:postgresql://localhost:5432/postgres
|
||||||
|
spring.datasource.username=admin
|
||||||
|
spring.datasource.password=admin
|
||||||
|
spring.datasource.driver-class-name=org.postgresql.Driver
|
||||||
|
|
||||||
|
spring.jpa.hibernate.ddl-auto=update
|
||||||
|
spring.jpa.show-sql=true
|
||||||
|
spring.jpa.properties.hibernate.format_sql=true
|
||||||
|
|
||||||
|
# ----- Redis -----
|
||||||
|
spring.data.redis.host=localhost
|
||||||
|
spring.data.redis.port=6379
|
||||||
|
|
||||||
|
# ----- MinIO -----
|
||||||
|
minio.url=http://localhost:9000
|
||||||
|
minio.access-key=minioadmin
|
||||||
|
minio.secret-key=minioadmin
|
||||||
|
minio.bucket=envsync-backups
|
||||||
|
|
||||||
|
# ----- Keycloak -----
|
||||||
|
# Set audience to your Keycloak client-id to enforce aud validation (optional).
|
||||||
|
app.security.jwt.audience=envsync-app
|
||||||
|
# Allow clock skew in seconds for exp/nbf validation.
|
||||||
|
app.security.jwt.clock-skew-seconds=60
|
||||||
|
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8180/realms/envsync
|
||||||
|
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:8180/realms/envsync/protocol/openid-connect/certs
|
||||||
|
logging.level.org.springframework.security=DEBUG
|
||||||
|
logging.level.org.springframework.security.oauth2=DEBUG
|
||||||
|
# ----- Server port -----
|
||||||
|
server.port=8081
|
||||||
+11
-7
@@ -1,5 +1,3 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:15-alpine
|
image: postgres:15-alpine
|
||||||
@@ -11,6 +9,11 @@ services:
|
|||||||
POSTGRES_USER: admin
|
POSTGRES_USER: admin
|
||||||
POSTGRES_PASSWORD: admin
|
POSTGRES_PASSWORD: admin
|
||||||
POSTGRES_DB: postgres
|
POSTGRES_DB: postgres
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U admin -d postgres"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 10
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
- ./init:/docker-entrypoint-initdb.d # place init.sql here to create additional databases
|
- ./init:/docker-entrypoint-initdb.d # place init.sql here to create additional databases
|
||||||
@@ -49,17 +52,18 @@ services:
|
|||||||
container_name: keycloak
|
container_name: keycloak
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "8180:8080" # Host port 8180 → container 8080 (Keycloak default)
|
- "127.0.0.1:8180:8080" # Local-only for dev; avoids external probes
|
||||||
environment:
|
environment:
|
||||||
KEYCLOAK_ADMIN: admin
|
KC_BOOTSTRAP_ADMIN_USERNAME: admin
|
||||||
KEYCLOAK_ADMIN_PASSWORD: admin
|
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
|
||||||
KC_DB: postgres
|
KC_DB: postgres
|
||||||
KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
|
KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
|
||||||
KC_DB_USERNAME: admin
|
KC_DB_USERNAME: admin
|
||||||
KC_DB_PASSWORD: admin
|
KC_DB_PASSWORD: admin
|
||||||
command: start-dev # Use start-dev for development mode
|
command: start-dev --http-enabled true
|
||||||
depends_on:
|
depends_on:
|
||||||
- postgres
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
networks:
|
networks:
|
||||||
- backend
|
- backend
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,31 @@
|
|||||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||||
|
|
||||||
|
## NextAuth + Keycloak setup
|
||||||
|
|
||||||
|
1. Copy environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env.local
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Set values in `.env.local`:
|
||||||
|
|
||||||
|
- `NEXTAUTH_SECRET`: generate one with `openssl rand -base64 32`
|
||||||
|
- `KEYCLOAK_ISSUER`: e.g. `http://localhost:8080/realms/<your-realm>`
|
||||||
|
- `KEYCLOAK_CLIENT_ID` and `KEYCLOAK_CLIENT_SECRET`: from your Keycloak client
|
||||||
|
|
||||||
|
3. In Keycloak client settings, add callback URL:
|
||||||
|
|
||||||
|
- `http://localhost:3000/api/auth/callback/keycloak`
|
||||||
|
|
||||||
|
4. Start the app and test sign-in:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open `http://localhost:3000` and click **Sign in with Keycloak**.
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
First, run the development server:
|
First, run the development server:
|
||||||
|
|||||||
Generated
+163
@@ -9,6 +9,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
|
"next-auth": "^4.24.13",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3"
|
"react-dom": "19.2.3"
|
||||||
},
|
},
|
||||||
@@ -228,6 +229,15 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/runtime": {
|
||||||
|
"version": "7.28.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
|
||||||
|
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/template": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.28.6",
|
"version": "7.28.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
||||||
@@ -1226,6 +1236,15 @@
|
|||||||
"node": ">=12.4.0"
|
"node": ">=12.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@panva/hkdf": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/panva"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rtsao/scc": {
|
"node_modules/@rtsao/scc": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
||||||
@@ -2626,6 +2645,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/cookie": {
|
||||||
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -4421,6 +4449,15 @@
|
|||||||
"jiti": "lib/jiti-cli.mjs"
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jose": {
|
||||||
|
"version": "4.15.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz",
|
||||||
|
"integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/panva"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@@ -5023,6 +5060,38 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/next-auth": {
|
||||||
|
"version": "4.24.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.13.tgz",
|
||||||
|
"integrity": "sha512-sgObCfcfL7BzIK76SS5TnQtc3yo2Oifp/yIpfv6fMfeBOiBJkDWF3A2y9+yqnmJ4JKc2C+nMjSjmgDeTwgN1rQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.20.13",
|
||||||
|
"@panva/hkdf": "^1.0.2",
|
||||||
|
"cookie": "^0.7.0",
|
||||||
|
"jose": "^4.15.5",
|
||||||
|
"oauth": "^0.9.15",
|
||||||
|
"openid-client": "^5.4.0",
|
||||||
|
"preact": "^10.6.3",
|
||||||
|
"preact-render-to-string": "^5.1.19",
|
||||||
|
"uuid": "^8.3.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@auth/core": "0.34.3",
|
||||||
|
"next": "^12.2.5 || ^13 || ^14 || ^15 || ^16",
|
||||||
|
"nodemailer": "^7.0.7",
|
||||||
|
"react": "^17.0.2 || ^18 || ^19",
|
||||||
|
"react-dom": "^17.0.2 || ^18 || ^19"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@auth/core": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"nodemailer": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/next/node_modules/postcss": {
|
"node_modules/next/node_modules/postcss": {
|
||||||
"version": "8.4.31",
|
"version": "8.4.31",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||||
@@ -5077,6 +5146,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/oauth": {
|
||||||
|
"version": "0.9.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
|
||||||
|
"integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/object-assign": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
@@ -5087,6 +5162,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/object-hash": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/object-inspect": {
|
"node_modules/object-inspect": {
|
||||||
"version": "1.13.4",
|
"version": "1.13.4",
|
||||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||||
@@ -5200,6 +5284,48 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/oidc-token-hash": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.2.0.tgz",
|
||||||
|
"integrity": "sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^10.13.0 || >=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/openid-client": {
|
||||||
|
"version": "5.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz",
|
||||||
|
"integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jose": "^4.15.9",
|
||||||
|
"lru-cache": "^6.0.0",
|
||||||
|
"object-hash": "^2.2.0",
|
||||||
|
"oidc-token-hash": "^5.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/panva"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/openid-client/node_modules/lru-cache": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"yallist": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/openid-client/node_modules/yallist": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/optionator": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||||
@@ -5366,6 +5492,28 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/preact": {
|
||||||
|
"version": "10.28.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.28.4.tgz",
|
||||||
|
"integrity": "sha512-uKFfOHWuSNpRFVTnljsCluEFq57OKT+0QdOiQo8XWnQ/pSvg7OpX5eNOejELXJMWy+BwM2nobz0FkvzmnpCNsQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/preact"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/preact-render-to-string": {
|
||||||
|
"version": "5.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz",
|
||||||
|
"integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pretty-format": "^3.8.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"preact": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prelude-ls": {
|
"node_modules/prelude-ls": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||||
@@ -5376,6 +5524,12 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pretty-format": {
|
||||||
|
"version": "3.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",
|
||||||
|
"integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/prop-types": {
|
"node_modules/prop-types": {
|
||||||
"version": "15.8.1",
|
"version": "15.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
@@ -6417,6 +6571,15 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "8.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||||
|
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
|
"next-auth": "^4.24.13",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3"
|
"react-dom": "19.2.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import NextAuth from "next-auth";
|
||||||
|
|
||||||
|
import { authOptions } from "@/lib/auth";
|
||||||
|
|
||||||
|
const handler = NextAuth(authOptions);
|
||||||
|
|
||||||
|
export { handler as GET, handler as POST };
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { getToken } from "next-auth/jwt";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import type { NextRequest } from "next/server";
|
||||||
|
|
||||||
|
const BACKEND_SECURE_URL = "http://localhost:8081/api/secure";
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET });
|
||||||
|
|
||||||
|
if (!token || typeof token.accessToken !== "string") {
|
||||||
|
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const backendResponse = await fetch(BACKEND_SECURE_URL, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token.accessToken}`,
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
cache: "no-store",
|
||||||
|
});
|
||||||
|
|
||||||
|
const contentType = backendResponse.headers.get("content-type") ?? "";
|
||||||
|
|
||||||
|
if (!backendResponse.ok) {
|
||||||
|
const body = await backendResponse.text();
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: "Backend request failed",
|
||||||
|
status: backendResponse.status,
|
||||||
|
body,
|
||||||
|
},
|
||||||
|
{ status: 502 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentType.includes("application/json")) {
|
||||||
|
const data = await backendResponse.json();
|
||||||
|
return NextResponse.json(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await backendResponse.text();
|
||||||
|
return NextResponse.json({ data: text });
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
|
||||||
|
export default function ClientSessionPage() {
|
||||||
|
const { data: session, status } = useSession();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="mx-auto flex min-h-screen w-full max-w-4xl flex-col gap-6 px-6 py-20">
|
||||||
|
<h1 className="text-3xl font-semibold tracking-tight">/client session example</h1>
|
||||||
|
<p className="text-sm text-zinc-600 dark:text-zinc-300">This page reads session in the browser with useSession(). Access token and ID token are not exposed here.</p>
|
||||||
|
|
||||||
|
<div className="rounded-xl border border-black/10 p-6 dark:border-white/20">
|
||||||
|
{status === "loading" ? (
|
||||||
|
<p className="text-sm">Loading session…</p>
|
||||||
|
) : session ? (
|
||||||
|
<pre className="overflow-x-auto text-sm">{JSON.stringify(session, null, 2)}</pre>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm">No active session.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<Link className="rounded-md border border-black/10 px-4 py-2 text-sm dark:border-white/20" href="/">
|
||||||
|
Home
|
||||||
|
</Link>
|
||||||
|
<Link className="rounded-md border border-black/10 px-4 py-2 text-sm dark:border-white/20" href="/server">
|
||||||
|
Server example
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
|
|
||||||
|
import { AuthSessionProvider } from "@/components/session-provider";
|
||||||
|
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
@@ -27,7 +30,7 @@ export default function RootLayout({
|
|||||||
<body
|
<body
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
>
|
>
|
||||||
{children}
|
<AuthSessionProvider>{children}</AuthSessionProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
+41
-56
@@ -1,65 +1,50 @@
|
|||||||
import Image from "next/image";
|
import { getServerSession } from "next-auth";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import { authOptions } from "@/lib/auth";
|
||||||
|
import { signOut } from "next-auth/react";
|
||||||
|
|
||||||
|
export default async function Home() {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
<main className="mx-auto flex min-h-screen w-full max-w-3xl flex-col justify-center gap-6 px-6 py-20">
|
||||||
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
<h1 className="text-3xl font-semibold tracking-tight">EnvSync Auth Example</h1>
|
||||||
<Image
|
|
||||||
className="dark:invert"
|
<div className="rounded-xl border border-black/10 p-6 dark:border-white/20">
|
||||||
src="/next.svg"
|
{session ? (
|
||||||
alt="Next.js logo"
|
<>
|
||||||
width={100}
|
<p className="text-sm text-zinc-600 dark:text-zinc-300">Signed in as</p>
|
||||||
height={20}
|
<p className="mt-1 text-lg font-medium">{session.user?.email ?? session.user?.name ?? "Keycloak user"}</p>
|
||||||
priority
|
<Link
|
||||||
/>
|
className="mt-4 inline-flex h-10 items-center rounded-md bg-black px-4 text-sm font-medium text-white dark:bg-white dark:text-black"
|
||||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
href={"/api/auth/signout"}
|
||||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
|
||||||
To get started, edit the page.tsx file.
|
|
||||||
</h1>
|
|
||||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
|
||||||
Looking for a starting point or more instructions? Head over to{" "}
|
|
||||||
<a
|
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
|
||||||
>
|
>
|
||||||
Templates
|
Sign out
|
||||||
</a>{" "}
|
</Link>
|
||||||
or the{" "}
|
</>
|
||||||
<a
|
) : (
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
<>
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
<p className="text-sm text-zinc-600 dark:text-zinc-300">You are not signed in.</p>
|
||||||
|
<Link
|
||||||
|
className="mt-4 inline-flex h-10 items-center rounded-md bg-black px-4 text-sm font-medium text-white dark:bg-white dark:text-black"
|
||||||
|
href="/api/auth/signin/keycloak"
|
||||||
>
|
>
|
||||||
Learning
|
Sign in with Keycloak
|
||||||
</a>{" "}
|
</Link>
|
||||||
center.
|
</>
|
||||||
</p>
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
|
||||||
<a
|
<div className="flex gap-3">
|
||||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
<Link className="rounded-md border border-black/10 px-4 py-2 text-sm dark:border-white/20" href="/server">
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
/server example
|
||||||
target="_blank"
|
</Link>
|
||||||
rel="noopener noreferrer"
|
<Link className="rounded-md border border-black/10 px-4 py-2 text-sm dark:border-white/20" href="/client">
|
||||||
>
|
/client example
|
||||||
<Image
|
</Link>
|
||||||
className="dark:invert"
|
|
||||||
src="/vercel.svg"
|
|
||||||
alt="Vercel logomark"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Deploy Now
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Documentation
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
type PollResult = {
|
||||||
|
ok: boolean;
|
||||||
|
status: number;
|
||||||
|
body: unknown;
|
||||||
|
at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PublicPage() {
|
||||||
|
const [latest, setLatest] = useState<PollResult | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let active = true;
|
||||||
|
|
||||||
|
const run = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/secure", { cache: "no-store" });
|
||||||
|
const body = await response.json().catch(() => null);
|
||||||
|
|
||||||
|
if (!active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLatest({
|
||||||
|
ok: response.ok,
|
||||||
|
status: response.status,
|
||||||
|
body,
|
||||||
|
at: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
setError(null);
|
||||||
|
} catch (err) {
|
||||||
|
if (!active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = err instanceof Error ? err.message : "Request failed";
|
||||||
|
setError(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
run();
|
||||||
|
const interval = setInterval(run, 2000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
clearInterval(interval);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="mx-auto flex min-h-screen w-full max-w-4xl flex-col gap-6 px-6 py-20">
|
||||||
|
<h1 className="text-3xl font-semibold tracking-tight">/public polling example</h1>
|
||||||
|
<p className="text-sm text-zinc-600 dark:text-zinc-300">
|
||||||
|
Polling /api/secure every 2 seconds. The API route forwards your access token to the backend.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="rounded-xl border border-black/10 p-6 dark:border-white/20">
|
||||||
|
{error && <p className="text-sm text-red-600">{error}</p>}
|
||||||
|
{!error && !latest && <p className="text-sm">Waiting for first response…</p>}
|
||||||
|
{latest && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<p className="text-sm">
|
||||||
|
Status: <span className="font-medium">{latest.ok ? "success" : "failed"}</span> ({latest.status})
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-zinc-500">Last updated: {latest.at}</p>
|
||||||
|
<pre className="overflow-x-auto text-sm">{JSON.stringify(latest.body, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<Link className="rounded-md border border-black/10 px-4 py-2 text-sm dark:border-white/20" href="/">
|
||||||
|
Home
|
||||||
|
</Link>
|
||||||
|
<Link className="rounded-md border border-black/10 px-4 py-2 text-sm dark:border-white/20" href="/server">
|
||||||
|
Server example
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { getServerSession } from "next-auth";
|
||||||
|
import { getToken } from "next-auth/jwt";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { headers } from "next/headers";
|
||||||
|
import { NextRequest } from "next/server";
|
||||||
|
|
||||||
|
import { authOptions } from "@/lib/auth";
|
||||||
|
|
||||||
|
export default async function ServerSessionPage() {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
const requestHeaders = await headers();
|
||||||
|
const request = new NextRequest("http://localhost:3000/server", {
|
||||||
|
headers: requestHeaders,
|
||||||
|
});
|
||||||
|
const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET });
|
||||||
|
console.log("ServerSessionPage token:", token); // Debug log to check token contents
|
||||||
|
|
||||||
|
const serverOnlyData = {
|
||||||
|
hasAccessToken: typeof token?.accessToken === "string",
|
||||||
|
hasIdToken: typeof token?.idToken === "string",
|
||||||
|
preferredUsername: typeof token?.preferredUsername === "string" ? token.preferredUsername : null,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="mx-auto flex min-h-screen w-full max-w-4xl flex-col gap-6 px-6 py-20">
|
||||||
|
<h1 className="text-3xl font-semibold tracking-tight">/server session example</h1>
|
||||||
|
<p className="text-sm text-zinc-600 dark:text-zinc-300">This page reads session + server-only token state (via getToken()).</p>
|
||||||
|
|
||||||
|
<div className="rounded-xl border border-black/10 p-6 dark:border-white/20">
|
||||||
|
{session ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<p className="mb-2 text-sm font-medium">Session (safe for client)</p>
|
||||||
|
<pre className="overflow-x-auto text-sm">{JSON.stringify(session, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="mb-2 text-sm font-medium">Server-only token presence</p>
|
||||||
|
<pre className="overflow-x-auto text-sm">{JSON.stringify(serverOnlyData, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm">No active session.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<Link className="rounded-md border border-black/10 px-4 py-2 text-sm dark:border-white/20" href="/">
|
||||||
|
Home
|
||||||
|
</Link>
|
||||||
|
<Link className="rounded-md border border-black/10 px-4 py-2 text-sm dark:border-white/20" href="/client">
|
||||||
|
Client example
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { SessionProvider } from "next-auth/react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function AuthSessionProvider({ children }: Props) {
|
||||||
|
return <SessionProvider>{children}</SessionProvider>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import type { NextAuthOptions } from "next-auth";
|
||||||
|
import KeycloakProvider from "next-auth/providers/keycloak";
|
||||||
|
|
||||||
|
function requiredEnv(name: string): string {
|
||||||
|
const value = process.env[name];
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
throw new Error(`Missing required environment variable: ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const authOptions: NextAuthOptions = {
|
||||||
|
providers: [
|
||||||
|
KeycloakProvider({
|
||||||
|
clientId: requiredEnv("KEYCLOAK_CLIENT_ID"),
|
||||||
|
clientSecret: requiredEnv("KEYCLOAK_CLIENT_SECRET"),
|
||||||
|
issuer: requiredEnv("KEYCLOAK_ISSUER"),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
session: {
|
||||||
|
strategy: "jwt",
|
||||||
|
},
|
||||||
|
callbacks: {
|
||||||
|
async jwt({ token, account, profile }) {
|
||||||
|
if (account) {
|
||||||
|
token.accessToken = account.access_token;
|
||||||
|
token.idToken = account.id_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profile && typeof profile === "object" && "preferred_username" in profile) {
|
||||||
|
token.preferredUsername = String(profile.preferred_username);
|
||||||
|
}
|
||||||
|
|
||||||
|
return token;
|
||||||
|
},
|
||||||
|
async session({ session, token }) {
|
||||||
|
session.preferredUsername = typeof token.preferredUsername === "string" ? token.preferredUsername : undefined;
|
||||||
|
|
||||||
|
return session;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
Vendored
+16
@@ -0,0 +1,16 @@
|
|||||||
|
import "next-auth";
|
||||||
|
import "next-auth/jwt";
|
||||||
|
|
||||||
|
declare module "next-auth" {
|
||||||
|
interface Session {
|
||||||
|
preferredUsername?: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "next-auth/jwt" {
|
||||||
|
interface JWT {
|
||||||
|
accessToken?: string;
|
||||||
|
idToken?: string;
|
||||||
|
preferredUsername?: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user