From c5631cff744778a421abdbf1013342fe6ed2201f Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Fri, 12 Jun 2026 08:47:46 +0000 Subject: [PATCH] =?UTF-8?q?tripit:=20Shell=20auth=20surface=20=E2=80=94=20?= =?UTF-8?q?tripit-app=20OAuth2=20provider=20+=20bearer-only=20tripit-api?= =?UTF-8?q?=20host?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Viktor is adding the Android APK (Capacitor Shell) for TripIt. The Shell cannot use the browser's forward-auth cookie dance, so per tripit ADR-0017 it logs in with OIDC Code+PKCE and calls the API with bearer JWTs: - authentik.tf: tripit-app OAuth2 provider (public client + PKCE — an APK holds no secret), custom-scheme redirect me.viktorbarzin.tripit://callback, RS256, 1h access / 90d refresh (offline_access mapping attached so refresh tokens are issued), plus the TripIt App application. - main.tf: new ingress host tripit-api.viktorbarzin.me -> same tripit Service, no forward-auth (backend validates the JWTs itself once tripit AUTH_MODE=hybrid lands — slice 2), inbound X-authentik-* deleted via the existing traefik strip-auth-headers middleware so the header fallback can never be spoofed through this host. Closes nothing here; tracked as viktor/tripit#49. Co-Authored-By: Claude Fable 5 --- stacks/tripit/authentik.tf | 84 ++++++++++++++++++++++++++++++++++++++ stacks/tripit/main.tf | 19 +++++++++ 2 files changed, 103 insertions(+) create mode 100644 stacks/tripit/authentik.tf diff --git a/stacks/tripit/authentik.tf b/stacks/tripit/authentik.tf new file mode 100644 index 00000000..c6aa1a2c --- /dev/null +++ b/stacks/tripit/authentik.tf @@ -0,0 +1,84 @@ +# Authentik OAuth2 provider for the TripIt App (the native Android Shell) — +# tripit ADR-0017, viktor/tripit#49. The Shell does an Authorization Code + +# PKCE login in the system browser and lands back in the app via the +# custom-scheme redirect; the backend validates the issued RS256 JWTs itself +# (AUTH_MODE=hybrid, tripit slice 2). client_type "public": an APK cannot keep +# a client secret, PKCE is the binding. The bearer-only ingress host this +# pairs with is module.ingress_api in main.tf. + +data "vault_kv_secret_v2" "authentik_tf" { + mount = "secret" + name = "authentik" +} + +provider "authentik" { + url = "https://authentik.viktorbarzin.me" + token = data.vault_kv_secret_v2.authentik_tf.data["tf_api_token"] +} + +data "authentik_flow" "default_authorization_implicit_consent" { + slug = "default-provider-authorization-implicit-consent" +} + +data "authentik_flow" "default_provider_invalidation" { + slug = "default-provider-invalidation-flow" +} + +data "authentik_certificate_key_pair" "signing" { + name = "authentik Self-signed Certificate" +} + +data "authentik_property_mapping_provider_scope" "openid" { + managed = "goauthentik.io/providers/oauth2/scope-openid" +} + +data "authentik_property_mapping_provider_scope" "profile" { + managed = "goauthentik.io/providers/oauth2/scope-profile" +} + +data "authentik_property_mapping_provider_scope" "email" { + managed = "goauthentik.io/providers/oauth2/scope-email" +} + +# offline_access is what makes Authentik issue a refresh token — the Shell +# stores only that and re-derives short-lived access tokens. Looked up by +# scope_name (the managed identifier is version-dependent). +data "authentik_property_mapping_provider_scope" "offline_access" { + scope_name = "offline_access" +} + +resource "authentik_provider_oauth2" "tripit_app" { + name = "tripit-app" + client_id = "tripit-app" + client_type = "public" + + authorization_flow = data.authentik_flow.default_authorization_implicit_consent.id + invalidation_flow = data.authentik_flow.default_provider_invalidation.id + + allowed_redirect_uris = [ + { + matching_mode = "strict" + url = "me.viktorbarzin.tripit://callback" + }, + ] + + access_token_validity = "hours=1" + refresh_token_validity = "days=90" + include_claims_in_id_token = true + signing_key = data.authentik_certificate_key_pair.signing.id + + property_mappings = [ + data.authentik_property_mapping_provider_scope.openid.id, + data.authentik_property_mapping_provider_scope.profile.id, + data.authentik_property_mapping_provider_scope.email.id, + data.authentik_property_mapping_provider_scope.offline_access.id, + ] +} + +resource "authentik_application" "tripit_app" { + name = "TripIt App" + slug = "tripit-app" + protocol_provider = authentik_provider_oauth2.tripit_app.id + meta_launch_url = "https://tripit.viktorbarzin.me" + policy_engine_mode = "any" +} diff --git a/stacks/tripit/main.tf b/stacks/tripit/main.tf index a70bf4ab..b165c5a8 100644 --- a/stacks/tripit/main.tf +++ b/stacks/tripit/main.tf @@ -820,3 +820,22 @@ module "ingress_planner_slack" { port = 8080 tls_secret_name = var.tls_secret_name } + +# Bearer-only API host for the native Shell (tripit ADR-0017, viktor/tripit#49). +# auth = "none": the backend itself validates OIDC bearer JWTs from the +# tripit-app Authentik provider (AUTH_MODE=hybrid, tripit slice 2) — a WebView +# client can't do the forward-auth cookie dance, and CORS preflights would die +# at the outpost. strip-auth-headers deletes inbound X-authentik-* so the +# hybrid fallback header can never be spoofed through this host. +module "ingress_api" { + source = "../../modules/kubernetes/ingress_factory" + auth = "none" + anti_ai_scraping = false + dns_type = "proxied" + namespace = kubernetes_namespace.tripit.metadata[0].name + name = "tripit-api" + service_name = "tripit" + port = 8080 + tls_secret_name = var.tls_secret_name + extra_middlewares = ["traefik-strip-auth-headers@kubernetescrd"] +}