No description
  • Kotlin 86.7%
  • TypeScript 10.2%
  • CSS 2%
  • Dockerfile 0.8%
  • HTML 0.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
benBastianelli e188f5dba2
All checks were successful
LinkLab CI / Run tests (push) Successful in 3m3s
LinkLab CI / Build JAR (push) Successful in 2m24s
LinkLab CI / Build and push Docker image (push) Successful in 30s
Completed lab
2026-10-20 14:53:54 +01:00
.forgejo/workflows ci: test if this works for building and pushing the image 2026-08-23 20:17:38 +01:00
frontend fix github login link and token expiry notes 2026-10-18 12:08:51 +00:00
gradle/wrapper Week 7 blueprint 2026-07-11 17:12:41 +01:00
jenkins Week 7 blueprint 2026-07-11 17:12:41 +01:00
monitoring Week 7 blueprint 2026-07-11 17:12:41 +01:00
nginx Week 7 blueprint 2026-07-11 17:12:41 +01:00
scripts Week 7 blueprint 2026-07-11 17:12:41 +01:00
src Completed lab 2026-10-20 14:53:54 +01:00
.dockerignore ci: test if this works for building and pushing the image 2026-08-23 20:17:38 +01:00
.gitignore fix auth error codes and task markers 2026-10-18 10:53:00 +00:00
body.txt Completed lab 2026-10-20 14:53:54 +01:00
build.gradle.kts Week 7 blueprint 2026-07-11 17:12:41 +01:00
cookies.txt Completed lab 2026-10-20 14:53:54 +01:00
docker-compose.yml Week 7 blueprint 2026-07-11 17:12:41 +01:00
Dockerfile Week 7 blueprint 2026-07-11 17:12:41 +01:00
Dockerfile.ci ci: test if this works for building and pushing the image 2026-08-23 20:17:38 +01:00
gradlew ci: test if this works for building and pushing the image 2026-08-23 20:17:38 +01:00
gradlew.bat Week 7 blueprint 2026-07-11 17:12:41 +01:00
head.txt Completed lab 2026-10-20 14:53:54 +01:00
linklab.postman_collection.json fix oauth redirect target, expose role on /me, update role table and postman auth 2026-10-18 13:24:20 +00:00
login.json Completed lab 2026-10-20 14:53:54 +01:00
README.md fix oauth redirect target, expose role on /me, update role table and postman auth 2026-10-18 13:24:20 +00:00
register-response.json Completed lab 2026-10-20 14:53:54 +01:00
register.json Completed lab 2026-10-20 14:53:54 +01:00
settings.gradle.kts Week 7 blueprint 2026-07-11 17:12:41 +01:00

LinkLab — Week 7 Blueprint

CS4297 Applied System Design · Security: JWT, OAuth2 and RBAC

Complete Week 6 solution included. Clone this and start here.


What you are building this week

Part What happens Time
A Naive session auth — plain-text passwords and a server-side session, then watch it break behind NGINX 25 min
B JWT — implement register/login returning signed tokens 45 min
C RBAC — add @PreAuthorize to three admin endpoints 25 min
Week 7 Optional 1 GitHub OAuth2 — login with GitHub, get a JWT 30 min
Week 7 Optional 2 Refresh tokens — short-lived access + long-lived refresh 30 min
Week 7 Optional 3 Rate limiting — lock out after 5 failed login attempts 30 min

Weeks 4 and 6 left optional tasks of their own in this code, numbered 1-3 as well, so three unrelated "Optional Task 1" exist. They are still worth doing: grep for WEEK 4 OPTIONAL TASK or WEEK 6 OPTIONAL TASK to find them in the source. This week's three are described below and have no stubs in the code.


Prerequisites

  • JDK 21 — check with java -version
  • Docker Desktop with at least 6 GB RAM (Settings → Resources → Memory)
  • Forgejo account at forgejo.skynet.ie

Getting started

1. Start the infrastructure

docker compose up -d mysql redis dynamodb-local elasticsearch kafka
./gradlew bootRun

Part A needs two application instances behind NGINX, and those are built from source inside Docker. Start that build early — it compiles the whole project in a container and takes several minutes the first time:

docker compose build link-service-1 link-service-2

It builds from the files on disk, not from your editor's buffer, and it does not rebuild on its own. Every time you change code, build again before docker compose up -d link-service-1 link-service-2.

2. Start the frontend and open it

docker compose up -d --build frontend

Visit http://localhost:3001

The frontend is served by a Vite dev server in Docker with hot reload. Edit files in frontend/src/ and the browser updates automatically.

3. Run the tests

./gradlew test
  • All Week 6 tests pass (solution complete)
  • AuthControllerTest — 5 of 6 tests fail until you implement register and login. (The 6th, the short-password case, passes already — @Valid rejects it before your code ever runs.)
  • SearchControllerTest, LinkControllerTest, KafkaOptionalControllerTest — one test each (... as non-admin should return 403) fails until you add @PreAuthorize("hasRole('ADMIN')") to reindex(), stats(), and lag() respectively (Part C). Everything else in those files passes already.

What to implement

src/main/kotlin/ie/ul/cs4297/linklab/auth/
└── AuthController.kt          # WEEK 7 TASK: implement register() and login()

src/main/kotlin/ie/ul/cs4297/linklab/controller/
├── SearchController.kt        # WEEK 7 TASK: add @PreAuthorize to reindex()
├── LinkController.kt          # WEEK 7 TASK: add @PreAuthorize to stats()
└── KafkaOptionalController.kt # WEEK 7 TASK: add @PreAuthorize to lag()

Already provided (do not modify):

  • JwtUtil.kt — generates and validates JWTs
  • JwtAuthFilter.kt — extracts Bearer token from every request
  • SecurityConfig.kt — Spring Security filter chain, CORS, public endpoints
  • OAuth2SuccessHandler.kt — issues JWT after GitHub OAuth2 login
  • LinkLabUserDetailsService.kt — loads users from MySQL
  • User.kt + UserRepository.kt — user entity and repository
  • The full React frontend in frontend/

Role model

Role Endpoints
anyone, no token POST /api/auth/**, GET /{code}, GET /api/links/search, /actuator/**
ROLE_USER the above + POST /api/links and everything else that is not admin-only
ROLE_ADMIN all of the above + POST /api/links/reindex, GET /api/links/{code}/stats, GET /api/optional/kafka/lag/{groupId}

The redirect and the search endpoint are deliberately public — a short link nobody can follow without logging in is not a short link. SecurityConfig is where that is decided.

Register users with different roles to test:

# Regular user
curl -X POST http://localhost:8080/api/auth/register \
     -H "Content-Type: application/json" \
     -d '{"username":"alice","password":"password123","role":"ROLE_USER"}'

# Admin
curl -X POST http://localhost:8080/api/auth/register \
     -H "Content-Type: application/json" \
     -d '{"username":"admin","password":"password123","role":"ROLE_ADMIN"}'

Finding your JWT in the browser

After logging in via the frontend, your token is stored in localStorage. To find it:

  1. Open browser DevTools (F12 or Cmd+Option+I)
  2. Go to Application tab (Chrome) or Storage tab (Firefox)
  3. Expand Local Storage → http://localhost:3001
  4. Click token — copy the value

You can then paste it into jwt.io to inspect the claims, or into the token collection variable in linklab.postman_collection.json — the collection sends it as a Bearer token on every request.


JWT quick reference

# Register and get token
TOKEN=$(curl -s -X POST http://localhost:8080/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"alice","password":"password123"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])")

# Use token
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/links \
     -X POST -H "Content-Type: application/json" -d '{"url":"https://ul.ie"}'

# Paste token at https://jwt.io to inspect claims

Week 7 Optional Task 1 — GitHub OAuth2

  1. github.com/settings/developers → New OAuth App
  2. Homepage URL: http://localhost:3001
  3. Callback URL: http://localhost:8080/login/oauth2/code/github
  4. Add Client ID and Secret to application.properties
  5. Restart, click "Continue with GitHub" on the login page

Week-by-week progression

Week Content Blueprint
2 REST API + MySQL Stubs for LINK-001
3 Docker · Redis · NGINX · Grafana Complete Week 2
4 SQL vs NoSQL · DynamoDB Complete Week 3
5 CI/CD · ElasticSearch · Fan-out Complete Week 4
6 Kafka async messaging + scale Complete Week 5
7 Security: JWT, OAuth2 and RBAC ← you are here
8 Capstone: full integration Complete Week 7
9 Capstone: demo and review Complete Week 8

Common issues

Your own controller tests fail with 403 Forbidden Most endpoints require authentication now. Add @WithMockUser (class-level or per-test) to any test hitting a protected endpoint — see LinkControllerTest, SearchControllerTest, or KafkaOptionalControllerTest for examples, including how to override to roles = ["ADMIN"] for a specific test. AuthControllerTest doesn't need this — /api/auth/** is public.

Frontend shows blank page Check docker compose logs frontend. If it says vite: not found, run docker compose build frontend.

POST /api/auth/register returns 405 Method Not Allowed Spring Security is intercepting before your controller. Check SecurityConfig permits /api/auth/**.

GitHub OAuth2 redirect fails Callback URL in GitHub app settings must match exactly: http://localhost:8080/login/oauth2/code/github

A protected endpoint answers 401 and you did send a token The token is expired (one hour) or was signed with a different linklab.jwt.secret. Register again and use the new token. If you lowered linklab.jwt.expiry-seconds while experimenting, put it back to 3600.

You lowered linklab.jwt.expiry-seconds, waited, and the token still works Only the process that re-read application.properties changed. ./gradlew bootRun on port 8080 picks the new value up on restart; link-service-1 and link-service-2 behind NGINX on port 80 keep the old one until you docker compose build them again. The expiresIn field in the register response tells you which instance answered.

Your code changes have no effect on port 80 Port 80 is NGINX in front of two containers built from a snapshot of your source. They never rebuild themselves: run docker compose build link-service-1 link-service-2, then docker compose up -d link-service-1 link-service-2, and check that up reports Recreated or Started. If it prints nothing, the build failed — scroll up and read it.

Everything you registered has disappeared spring.jpa.hibernate.ddl-auto=create-drop drops and recreates the tables on every start, so restarting either instance empties the users table. Register again after a restart, and register against the pair only once both instances are up.

One of link-service-1 / link-service-2 exits on startup Both create the ElasticSearch index at startup and allow five seconds for it. Started together on a busy laptop, one can lose that race and exit. Start them one at a time, or docker compose start link-service-1 again.

The application will not start: 5,000 milliseconds timeout on connection It creates its ElasticSearch index at startup and allows five seconds. That fails when ElasticSearch cannot allocate shards, which on a laptop means a disk over 90% full — check curl localhost:9200/_cat/allocation?v. Free the space, then delete the half-made index with curl -X DELETE localhost:9200/linklab-links, or the next start finds it in the same state.

./gradlew bootRun fails with IllegalArgumentException Set Gradle JVM to Java 21 in IntelliJ Settings → Build Tools → Gradle → Gradle JVM.