tripit: Shell auth surface — tripit-app OAuth2 provider + bearer-only tripit-api host
Some checks failed
ci/woodpecker/push/default Pipeline failed
ci/woodpecker/push/build-cli Pipeline was successful

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 <noreply@anthropic.com>
This commit is contained in:
Viktor Barzin 2026-06-12 08:47:46 +00:00
parent b985686661
commit c5631cff74
2 changed files with 103 additions and 0 deletions

View file

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

View file

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