From a2368dc5594139930146611215b03779ab4a7707 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Wed, 26 Mar 2025 12:01:34 +0100 Subject: [PATCH] Create phantom-token example code --- content/blogs/cancellation/index.md | 16 +++ content/blogs/phantom-token/README.md | 24 ++++ content/blogs/phantom-token/api/.gitignore | 34 ++++++ content/blogs/phantom-token/api/Dockerfile | 20 ++++ content/blogs/phantom-token/api/bun.lock | 32 +++++ content/blogs/phantom-token/api/index.ts | 23 ++++ content/blogs/phantom-token/api/package.json | 15 +++ content/blogs/phantom-token/api/tsconfig.json | 27 +++++ content/blogs/phantom-token/auth/Dockerfile | 12 ++ content/blogs/phantom-token/auth/go.mod | 20 ++++ content/blogs/phantom-token/auth/go.sum | 37 ++++++ content/blogs/phantom-token/auth/main.go | 113 ++++++++++++++++++ .../blogs/phantom-token/docker-compose.yaml | 36 ++++++ content/blogs/phantom-token/index.md | 9 ++ content/blogs/phantom-token/shell.nix | 7 ++ shell.nix | 12 +- 16 files changed, 431 insertions(+), 6 deletions(-) create mode 100644 content/blogs/cancellation/index.md create mode 100644 content/blogs/phantom-token/README.md create mode 100644 content/blogs/phantom-token/api/.gitignore create mode 100644 content/blogs/phantom-token/api/Dockerfile create mode 100644 content/blogs/phantom-token/api/bun.lock create mode 100644 content/blogs/phantom-token/api/index.ts create mode 100644 content/blogs/phantom-token/api/package.json create mode 100644 content/blogs/phantom-token/api/tsconfig.json create mode 100644 content/blogs/phantom-token/auth/Dockerfile create mode 100644 content/blogs/phantom-token/auth/go.mod create mode 100644 content/blogs/phantom-token/auth/go.sum create mode 100644 content/blogs/phantom-token/auth/main.go create mode 100644 content/blogs/phantom-token/docker-compose.yaml create mode 100644 content/blogs/phantom-token/index.md create mode 100644 content/blogs/phantom-token/shell.nix diff --git a/content/blogs/cancellation/index.md b/content/blogs/cancellation/index.md new file mode 100644 index 0000000..de50f53 --- /dev/null +++ b/content/blogs/cancellation/index.md @@ -0,0 +1,16 @@ +--- +title: "Cancellation in async" +description: "" +draft: true +tags: [] +--- + +How cancellation is implemented adapted: + +## Via a `request-cancelation` parameter +(c#, golang) + +## You don't +(js (ok there's `AbortControler` for like 3 functions that use it)) + +## Special case: bash diff --git a/content/blogs/phantom-token/README.md b/content/blogs/phantom-token/README.md new file mode 100644 index 0000000..0276c43 --- /dev/null +++ b/content/blogs/phantom-token/README.md @@ -0,0 +1,24 @@ +Simple phantom token example project + +The `auth` module has 3 endpoints: + - `/auth/user` `{ "name": "toto" }` -> create a new user & return a session token + - `/auth/jwt` -> Convert the session token in the `authorization` header (after the `Bearer ` prefix) to a jwt + - `/.well-known/jwks.json` -> Get a jwks (aka a public key) to verify the jwt's signature + +The `api` module only has one enpoint: + - `/api/me` -> Returns the content of the jwt it gets in the `authorization` header. + +Normal flow + +```bash +TOKEN=$(curl localhost:7891/auth/user -d '{ "name": "toto" }' | jq .token -r) +curl localhost:7891/api/me -H "authorization: Bearer $TOKEN" +``` + +when calling `/api/me`, we call it with the opaque token retrieved from the `/auth/user` endpoint. +the gateway will replace intercept this call and send a request to `/auth/jwt` with the `authorization` header. +if `/auth/jwt` returns an error (non 2XX response) -> it simply return this response to the client & abort the request +else, it will replace the `authorization` header with the one from the `/auth/jwt` (or it's body) and then make the request to `/api/me` + + +https://doc.traefik.io/traefik/middlewares/http/forwardauth/ for more info on how this works on traefik diff --git a/content/blogs/phantom-token/api/.gitignore b/content/blogs/phantom-token/api/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/content/blogs/phantom-token/api/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/content/blogs/phantom-token/api/Dockerfile b/content/blogs/phantom-token/api/Dockerfile new file mode 100644 index 0000000..579f24e --- /dev/null +++ b/content/blogs/phantom-token/api/Dockerfile @@ -0,0 +1,20 @@ +FROM oven/bun +WORKDIR /app + +COPY package.json bun.lock . +RUN bun install --production + +COPY index.ts tsconfig.json . + +ENV NODE_ENV=production +RUN bun build \ + --compile \ + --minify-whitespace \ + --minify-syntax \ + --target bun \ + --outfile server \ + ./index.ts + +EXPOSE 3000 +CMD ["./server"] + diff --git a/content/blogs/phantom-token/api/bun.lock b/content/blogs/phantom-token/api/bun.lock new file mode 100644 index 0000000..10924ec --- /dev/null +++ b/content/blogs/phantom-token/api/bun.lock @@ -0,0 +1,32 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "back", + "dependencies": { + "jose": "^6.0.10", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.2.6", "", { "dependencies": { "bun-types": "1.2.6" } }, "sha512-fY9CAmTdJH1Llx7rugB0FpgWK2RKuHCs3g2cFDYXUutIy1QGiPQxKkGY8owhfZ4MXWNfxwIbQLChgH5gDsY7vw=="], + + "@types/node": ["@types/node@22.13.13", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-ClsL5nMwKaBRwPcCvH8E7+nU4GxHVx1axNvMZTFHMEfNI7oahimt26P5zjVCRrjiIWj6YFXfE1v3dEp94wLcGQ=="], + + "@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="], + + "bun-types": ["bun-types@1.2.6", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-FbCKyr5KDiPULUzN/nm5oqQs9nXCHD8dVc64BArxJadCvbNzAI6lUWGh9fSJZWeDIRD38ikceBU8Kj/Uh+53oQ=="], + + "jose": ["jose@6.0.10", "", {}, "sha512-skIAxZqcMkOrSwjJvplIPYrlXGpxTPnro2/QWTDCxAdWQrSTV5/KqspMWmi5WAx5+ULswASJiZ0a+1B/Lxt9cw=="], + + "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], + + "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + } +} diff --git a/content/blogs/phantom-token/api/index.ts b/content/blogs/phantom-token/api/index.ts new file mode 100644 index 0000000..5bd1138 --- /dev/null +++ b/content/blogs/phantom-token/api/index.ts @@ -0,0 +1,23 @@ +import { createRemoteJWKSet, jwtVerify } from "jose"; + +const jwks = createRemoteJWKSet( + new URL( + ".well-known/jwks.json", + process.env.AUTH_SERVER ?? "http://auth:8080", + ), +); + +Bun.serve({ + routes: { + "/api/me": async (req) => { + const auth = req.headers.get("authorization"); + const bearer = auth?.slice(7); + console.log("auth", auth, "bearer", bearer); + if (!bearer) return new Response("Forbidden", { status: 403 }); + + const { payload } = await jwtVerify(bearer, jwks); + console.log("success", "jwt payload", payload); + return new Response(JSON.stringify(payload)); + }, + }, +}); diff --git a/content/blogs/phantom-token/api/package.json b/content/blogs/phantom-token/api/package.json new file mode 100644 index 0000000..2e651c6 --- /dev/null +++ b/content/blogs/phantom-token/api/package.json @@ -0,0 +1,15 @@ +{ + "name": "api", + "module": "index.ts", + "type": "module", + "private": true, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "jose": "^6.0.10" + } +} diff --git a/content/blogs/phantom-token/api/tsconfig.json b/content/blogs/phantom-token/api/tsconfig.json new file mode 100644 index 0000000..238655f --- /dev/null +++ b/content/blogs/phantom-token/api/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/content/blogs/phantom-token/auth/Dockerfile b/content/blogs/phantom-token/auth/Dockerfile new file mode 100644 index 0000000..bf0f3e8 --- /dev/null +++ b/content/blogs/phantom-token/auth/Dockerfile @@ -0,0 +1,12 @@ +FROM golang:1.24 +WORKDIR /app + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . +RUN CGO_ENABLED=0 GOOS=linux go build -o /keibi + +EXPOSE 8080 +CMD ["/keibi"] + diff --git a/content/blogs/phantom-token/auth/go.mod b/content/blogs/phantom-token/auth/go.mod new file mode 100644 index 0000000..98d5bb4 --- /dev/null +++ b/content/blogs/phantom-token/auth/go.mod @@ -0,0 +1,20 @@ +module github.com/zoriya/kyoo/blog/phantom-token + +go 1.24.1 + +require ( + github.com/golang-jwt/jwt/v5 v5.2.2 + github.com/lestrrat-go/jwx v1.2.30 +) + +require ( + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect + github.com/goccy/go-json v0.10.3 // indirect + github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect + github.com/lestrrat-go/blackmagic v1.0.2 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/iter v1.0.2 // indirect + github.com/lestrrat-go/option v1.0.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + golang.org/x/crypto v0.25.0 // indirect +) diff --git a/content/blogs/phantom-token/auth/go.sum b/content/blogs/phantom-token/auth/go.sum new file mode 100644 index 0000000..2bb75cc --- /dev/null +++ b/content/blogs/phantom-token/auth/go.sum @@ -0,0 +1,37 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A= +github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= +github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= +github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= +github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= +github.com/lestrrat-go/jwx v1.2.30 h1:VKIFrmjYn0z2J51iLPadqoHIVLzvWNa1kCsTqNDHYPA= +github.com/lestrrat-go/jwx v1.2.30/go.mod h1:vMxrwFhunGZ3qddmfmEm2+uced8MSI6QFWGTKygjSzQ= +github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= +github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/content/blogs/phantom-token/auth/main.go b/content/blogs/phantom-token/auth/main.go new file mode 100644 index 0000000..1ac8732 --- /dev/null +++ b/content/blogs/phantom-token/auth/main.go @@ -0,0 +1,113 @@ +package main + +import ( + "crypto/rand" + "crypto/rsa" + "encoding/base64" + "encoding/json" + "fmt" + "log" + "net/http" + "strings" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/lestrrat-go/jwx/jwk" +) + +type User struct { + Name string `json:"name"` +} + +func main() { + rsakey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + log.Fatal(err) + } + + users := make(map[string]User) + + http.HandleFunc("/auth/user", func(w http.ResponseWriter, r *http.Request) { + decoder := json.NewDecoder(r.Body) + var u User + err := decoder.Decode(&u) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // generate random session id + id := make([]byte, 64) + _, err = rand.Read(id) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + token := base64.StdEncoding.EncodeToString(id) + + users[token] = u + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(struct { + Name string `json:"name"` + Token string `json:"token"` + }{Name: u.Name, Token: token}) + }) + + http.HandleFunc("/auth/jwt", func(w http.ResponseWriter, r *http.Request) { + auth := r.Header.Get("Authorization") + if !strings.HasPrefix(auth, "Bearer ") { + http.Error(w, "Missing session token", http.StatusUnauthorized) + return + } + token := auth[len("Bearer "):] + + user, ok := users[token] + if !ok { + http.Error(w, "Invalid token", http.StatusForbidden) + return + } + + claims := make(jwt.MapClaims) + claims["sub"] = user.Name + claims["iss"] = "keibi-blog" + claims["iat"] = &jwt.NumericDate{ + Time: time.Now().UTC(), + } + claims["exp"] = &jwt.NumericDate{ + Time: time.Now().UTC().Add(time.Hour), + } + jwt := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + t, err := jwt.SignedString(rsakey) + if err != nil { + http.Error(w, "Could not sign token", http.StatusInternalServerError) + return + } + + w.Header().Add("Authorization", fmt.Sprintf("Bearer %s", t)) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(jwt) + }) + + http.HandleFunc("/.well-known/jwks.json", func(w http.ResponseWriter, r *http.Request) { + key, err := jwk.New(rsakey.PublicKey) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + key.Set("use", "sig") + key.Set("key_ops", "verify") + set := jwk.NewSet() + set.Add(key) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(set) + }) + + log.Fatal(http.ListenAndServe(":8080", nil)) +} diff --git a/content/blogs/phantom-token/docker-compose.yaml b/content/blogs/phantom-token/docker-compose.yaml new file mode 100644 index 0000000..b349582 --- /dev/null +++ b/content/blogs/phantom-token/docker-compose.yaml @@ -0,0 +1,36 @@ +services: + auth: + build: ./auth + restart: on-failure + ports: + - "8080:8080" + labels: + - "traefik.enable=true" + - "traefik.http.routers.auth.rule=PathPrefix(`/auth/`) || PathPrefix(`/.well-known/`)" + + api: + build: ./api + restart: on-failure + ports: + - "3000:3000" + labels: + - "traefik.enable=true" + - "traefik.http.routers.api.rule=PathPrefix(`/api/`)" + - "traefik.http.routers.api.middlewares=phantom-token" + - "traefik.http.middlewares.phantom-token.forwardauth.address=http://auth:8080/auth/jwt" + - "traefik.http.middlewares.phantom-token.forwardauth.authRequestHeaders=Authorization,X-Api-Key" + - "traefik.http.middlewares.phantom-token.forwardauth.authResponseHeaders=Authorization" + + traefik: + image: traefik:v3.3 + restart: on-failure + command: + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--entryPoints.web.address=:7891" + - "--accesslog=true" + ports: + - "7891:7891" + volumes: + - "/var/run/docker.sock:/var/run/docker.sock:ro" + diff --git a/content/blogs/phantom-token/index.md b/content/blogs/phantom-token/index.md new file mode 100644 index 0000000..1998ce8 --- /dev/null +++ b/content/blogs/phantom-token/index.md @@ -0,0 +1,9 @@ +--- +title: "Phantom token" +description: "" +date: 2025-03-26 +draft: true +tags: [] +--- + + diff --git a/content/blogs/phantom-token/shell.nix b/content/blogs/phantom-token/shell.nix new file mode 100644 index 0000000..8004e63 --- /dev/null +++ b/content/blogs/phantom-token/shell.nix @@ -0,0 +1,7 @@ +{pkgs ? import {}}: +pkgs.mkShell { + packages = with pkgs; [ + bun + go + ]; +} diff --git a/shell.nix b/shell.nix index 45a89cd..5c53306 100644 --- a/shell.nix +++ b/shell.nix @@ -1,7 +1,7 @@ {pkgs ? import {}}: - pkgs.mkShell { - packages = with pkgs; [ - hugo - go - ]; - } +pkgs.mkShell { + packages = with pkgs; [ + hugo + go + ]; +}