Create phantom-token example code

This commit is contained in:
2025-03-26 12:01:34 +01:00
parent daafffa2a1
commit a2368dc559
16 changed files with 431 additions and 6 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"]

View File

@@ -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=="],
}
}

View File

@@ -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));
},
},
});

View File

@@ -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"
}
}

View File

@@ -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
}
}

View File

@@ -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"]

View File

@@ -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
)

View File

@@ -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=

View File

@@ -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))
}

View File

@@ -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"

View File

@@ -0,0 +1,9 @@
---
title: "Phantom token"
description: ""
date: 2025-03-26
draft: true
tags: []
---

View File

@@ -0,0 +1,7 @@
{pkgs ? import <nixpkgs> {}}:
pkgs.mkShell {
packages = with pkgs; [
bun
go
];
}

View File

@@ -1,7 +1,7 @@
{pkgs ? import <nixpkgs> {}}:
pkgs.mkShell {
packages = with pkgs; [
hugo
go
];
}
pkgs.mkShell {
packages = with pkgs; [
hugo
go
];
}