No description
  • Go 97.1%
  • Go Template 2.9%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Jakob Borg d78b79998d
fix: return endpoints in the perspective of the caller
Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-07-01 08:31:30 +02:00
.github/workflows wip 2026-06-30 09:28:34 +02:00
chart/ptsidp wip 2026-06-30 17:41:02 +02:00
internal/idp fix: return endpoints in the perspective of the caller 2026-07-01 08:31:30 +02:00
.ko.yaml wip 2026-06-30 09:28:34 +02:00
go.mod wip 2026-06-30 09:28:34 +02:00
go.sum wip 2026-06-30 09:28:34 +02:00
main.go wip 2026-06-30 10:31:55 +02:00
README.md wip 2026-06-30 10:45:35 +02:00

ptsidp

Proxied Tailscale IDP.

Authentication

Identity comes entirely from the headers the Tailscale ingress proxy injects:

Header Mapped to
Tailscale-User-Login sub, email, preferred_username
Tailscale-User-Name name
Tailscale-User-Profile-Pic picture

email_verified is reported true only when the login looks like an email address. To change this mapping, edit internal/idp/user.go.

Configuration

Variable Default Description
LISTEN_ADDR :8080 Address to listen on (plain HTTP).
ISSUER (derived) Public issuer URL. If unset, derived from X-Forwarded-Proto/X-Forwarded-Host.
CLIENTS (none) JSON array of registered clients (see below). Only these clients are accepted.

Clients

CLIENTS is a JSON array. Each client has an id, an optional secret, and a list of allowed redirectURIs:

[
  {
    "id": "web",
    "secret": "topsecret",
    "redirectURIs": ["https://app.example.com/callback"]
  },
  {
    "id": "spa",
    "redirectURIs": ["https://spa.example.com/*"]
  }
]
  • A client with a secret is confidential and authenticates at the token endpoint via client_secret_basic or client_secret_post.
  • A client without a secret is public: it must use the authorisation code flow with PKCE (code_challenge at authorise, code_verifier at token), and presenting a secret is rejected.
  • redirectURIs are enforced. The redirect_uri request parameter must match one of them, and the same value must be presented again at the token endpoint. A * is a wildcard matching any run of characters, e.g. https://app.example.com/*. The match is anchored at both ends, so include the trailing slash (/*) to avoid matching look-alike hosts such as app.example.com.evil.com.

With no clients configured, every authorisation request is rejected.

Running locally

go run .
# In another terminal:
curl -s localhost:8080/.well-known/openid-configuration | jq

When ISSUER is unset the issuer is derived from X-Forwarded-* headers, and locally you can simulate both the issuer and a Tailscale identity by hand:

# Start with a registered client, e.g.
#   CLIENTS='[{"id":"demo","secret":"secret","redirectURIs":["https://client.example.com/cb"]}]' go run .
curl -s \
  -H 'X-Forwarded-Proto: https' \
  -H 'X-Forwarded-Host: ptsidp.example-tailnet.ts.net' \
  -H 'Tailscale-User-Login: alice@example.com' \
  -H 'Tailscale-User-Name: Alice Example' \
  'localhost:8080/authorize?response_type=code&client_id=demo&scope=openid&redirect_uri=https://client.example.com/cb&state=xyz'