Skip to content

Public OIDC (token_endpoint_auth_method=none) sends unintended Basic Auth on token request and misses code_verifier #372

@TheRealKingS

Description

@TheRealKingS

Summary

When configured as a public client (token_endpoint_auth_method: none) with PKCE S256, opkssh first sends a token request with an Authorization: Basic <client_id:> header (empty secret), then a second request without that header but missing code_verifier (and sometimes redirect_uri). The OP/IdP responds with 400 invalid_request or 401 invalid_client.

Expected behavior: one token POST without an Authorization header, and with client_id, redirect_uri, and code_verifier in the form body.

Environment

OS: Windows 11 (PowerShell) [or: Linux/macOS]

opkssh version: 0.10.0

OP/IdP: Shibboleth IdP (test system). Discovery advertises "token_endpoint_auth_methods_supported": ["none", ...].

Flow: Authorization Code + PKCE (S256), public client

Provider config (redacted)

providers:
  - alias: opkssh
    issuer: https://<issuer.example.org>
    client_id: opkssh
    token_endpoint_auth_method: none
    use_pkce: true
    pkce_method: S256
    scopes: openid email profile
    redirect_uris:
      - http://localhost:3000/login-callback

Steps to reproduce

  1. Run opkssh login --provider "https://<issuer.example.org>,opkssh"
  2. Complete login in the browser; you are redirected to http://localhost:3000/login-callback?code=…&state=…
  3. Observe token exchange via debug/HTTP2 logs.

Expected behavior

  • A single POST to /idp/profile/oidc/token (Shibboleth IdP)
  • No Authorization: header
  • application/x-www-form-urlencoded body includes:
    • grant_type=authorization_code
    • code=...
    • redirect_uri=http://localhost:3000/login-callback
    • client_id=opkssh
    • code_verifier=... (PKCE S256)

Actual behavior (anonymized debug excerpts)

1) Unexpected request

POST /idp/profile/oidc/token
Authorization: Basic b3Brc3NoOg== # "opkssh:" (empty secret)
Content-Type: application/x-www-form-urlencoded
→ 400 {"error":"invalid_request","error_description":"UnableToDecode"}

2) Follow-up request (missing fields)

POST /idp/profile/oidc/token
Content-Type: application/x-www-form-urlencoded
BODY: client_id=opkssh&code=... # missing code_verifier and/or redirect_uri
→ 401 {"error":"invalid_client","error_description":"Client authentication failed"}

Likely cause

Internals likely rely on golang.org/x/oauth2 with the default/auto auth style. Without forcing AuthStyleInParams, the library first tries client_secret_basic, which produces Authorization: Basic <client_id:> even for public clients. The subsequent attempt does not reliably include code_verifier (and sometimes not the exact redirect_uri), resulting in 401.

DEBUG Log

time="2025-10-15T15:25:13+02:00" level=info msg="listening on http://127.0.0.1:3000/"
time="2025-10-15T15:25:13+02:00" level=info msg="press ctrl+c to stop"
2025/10/15 15:25:13 http2: Transport failed to get client conn for issuer.example.org:443: http2: no cached connection was available
2025/10/15 15:25:13 http2: Transport creating client conn 0xc000002180 to [XXXXX]:443
2025/10/15 15:25:13 http2: Framer 0xc00017ca80: wrote SETTINGS len=18, settings: ENABLE_PUSH=0, INITIAL_WINDOW_SIZE=4194304, MAX_HEADER_LIST_SIZE=10485760
2025/10/15 15:25:13 http2: Framer 0xc00017ca80: wrote WINDOW_UPDATE len=4 (conn) incr=1073741824
2025/10/15 15:25:13 http2: Transport encoding header ":authority" = "issuer.example.org"
2025/10/15 15:25:13 http2: Transport encoding header ":method" = "GET"
2025/10/15 15:25:13 http2: Transport encoding header ":path" = "/.well-known/openid-configuration"
2025/10/15 15:25:13 http2: Transport encoding header ":scheme" = "https"
2025/10/15 15:25:13 http2: Transport encoding header "accept-encoding" = "gzip"
2025/10/15 15:25:13 http2: Transport encoding header "user-agent" = "Go-http-client/2.0"
2025/10/15 15:25:13 http2: Framer 0xc00017ca80: wrote HEADERS flags=END_STREAM|END_HEADERS stream=1 len=66
2025/10/15 15:25:13 http2: Framer 0xc00017ca80: read SETTINGS len=18, settings: MAX_CONCURRENT_STREAMS=10, INITIAL_WINDOW_SIZE=32768, MAX_HEADER_LIST_SIZE=65536
2025/10/15 15:25:13 http2: Transport received SETTINGS len=18, settings: MAX_CONCURRENT_STREAMS=10, INITIAL_WINDOW_SIZE=32768, MAX_HEADER_LIST_SIZE=65536
2025/10/15 15:25:13 http2: Framer 0xc00017ca80: wrote SETTINGS flags=ACK len=0
2025/10/15 15:25:13 http2: Framer 0xc00017ca80: read SETTINGS flags=ACK len=0
2025/10/15 15:25:13 http2: Transport received SETTINGS flags=ACK len=0
2025/10/15 15:25:13 http2: Framer 0xc00017ca80: read HEADERS flags=END_HEADERS stream=1 len=424
2025/10/15 15:25:13 http2: decoded hpack field header field ":status" = "200"
2025/10/15 15:25:13 http2: decoded hpack field header field "date" = "Wed, 15 Oct 2025 13:25:13 GMT"
2025/10/15 15:25:13 http2: decoded hpack field header field "server" = "Apache"
2025/10/15 15:25:13 http2: decoded hpack field header field "access-control-allow-methods" = "GET, OPTIONS"
2025/10/15 15:25:13 http2: decoded hpack field header field "access-control-allow-headers" = "Content-Type, Authorization"
2025/10/15 15:25:13 http2: decoded hpack field header field "access-control-allow-origin" = "*"
2025/10/15 15:25:13 http2: decoded hpack field header field "cache-control" = "no-store"
2025/10/15 15:25:13 http2: decoded hpack field header field "set-cookie" = "__Host-JSESSIONID=9B4AC258316D008334540491465DDC79; Path=/; Secure; HttpOnly"
2025/10/15 15:25:13 http2: decoded hpack field header field "x-frame-options" = "DENY"
2025/10/15 15:25:13 http2: decoded hpack field header field "strict-transport-security" = "max-age=31536000"
2025/10/15 15:25:13 http2: decoded hpack field header field "content-security-policy" = "frame-ancestors 'none'; base-uri 'none';"
2025/10/15 15:25:13 http2: decoded hpack field header field "content-type" = "application/json;charset=UTF-8"
2025/10/15 15:25:13 http2: decoded hpack field header field "x-xss-protection" = "1; mode=block"
2025/10/15 15:25:13 http2: decoded hpack field header field "x-content-type-options" = "nosniff"
2025/10/15 15:25:13 http2: decoded hpack field header field "access-control-allow-origin" = "*"
2025/10/15 15:25:13 http2: decoded hpack field header field "expires" = "0"
2025/10/15 15:25:13 http2: decoded hpack field header field "pragme" = "no-cache"
2025/10/15 15:25:13 http2: decoded hpack field header field "cache-control" = "no-cache, no-store, must-revalidate"
2025/10/15 15:25:13 http2: decoded hpack field header field "strict-transport-security" = "max-age=31536000 ; includeSubDomains ; preload"
2025/10/15 15:25:13 http2: Transport received HEADERS flags=END_HEADERS stream=1 len=424
2025/10/15 15:25:13 http2: Framer 0xc00017ca80: read DATA stream=1 len=2048 data="{\"authorization_endpoint\":\"https:\\/\\/issuer.example.org\\/idp\\/profile\\/oidc\\/authorize\",\"token_endpoint\":\"https:\\/\\/issuer.example.org\\/idp\\/profile\\/oidc\\/token\",\"registration_endpoint\":\"https:\\/\\/issuer.example.org\\/idp\\/profile\\/oidc\\/reg" (1792 bytes omitted)
2025/10/15 15:25:13 http2: Transport received DATA stream=1 len=2048 data="{\"authorization_endpoint\":\"https:\\/\\/issuer.example.org\\/idp\\/profile\\/oidc\\/authorize\",\"token_endpoint\":\"https:\\/\\/issuer.example.org\\/idp\\/profile\\/oidc\\/token\",\"registration_endpoint\":\"https:\\/\\/issuer.example.org\\/idp\\/profile\\/oidc\\/reg" (1792 bytes omitted)
2025/10/15 15:25:13 http2: Framer 0xc00017ca80: read DATA flags=END_STREAM stream=1 len=503 data="d\":[\"RSA1_5\",\"RSA-OAEP\",\"RSA-OAEP-256\",\"RSA-OAEP-384\",\"RSA-OAEP-512\",\"A128KW\",\"A192KW\",\"A256KW\",\"A128GCMKW\",\"A192GCMKW\",\"A256GCMKW\",\"ECDH-ES\",\"ECDH-ES+A128KW\",\"ECDH-ES+A192KW\",\"ECDH-ES+A256KW\"],\"userinfo_encryption_enc_values_supported\":[\"A128CBC-HS256\",\"A" (247 bytes omitted)
2025/10/15 15:25:13 http2: Transport received DATA flags=END_STREAM stream=1 len=503 data="d\":[\"RSA1_5\",\"RSA-OAEP\",\"RSA-OAEP-256\",\"RSA-OAEP-384\",\"RSA-OAEP-512\",\"A128KW\",\"A192KW\",\"A256KW\",\"A128GCMKW\",\"A192GCMKW\",\"A256GCMKW\",\"ECDH-ES\",\"ECDH-ES+A128KW\",\"ECDH-ES+A192KW\",\"ECDH-ES+A256KW\"],\"userinfo_encryption_enc_values_supported\":[\"A128CBC-HS256\",\"A" (247 bytes omitted)
time="2025-10-15T15:25:13+02:00" level=info msg="Opening browser to http://localhost:3000/login "
2025/10/15 15:25:24 http2: Transport encoding header ":authority" = "issuer.example.org"
2025/10/15 15:25:24 http2: Transport encoding header ":method" = "POST"
2025/10/15 15:25:24 http2: Transport encoding header ":path" = "/idp/profile/oidc/token"
2025/10/15 15:25:24 http2: Transport encoding header ":scheme" = "https"
2025/10/15 15:25:24 http2: Transport encoding header "content-type" = "application/x-www-form-urlencoded"
2025/10/15 15:25:24 http2: Transport encoding header "authorization" = "Basic b3Brc3NoOg=="
2025/10/15 15:25:24 http2: Transport encoding header "content-length" = "774"
2025/10/15 15:25:24 http2: Transport encoding header "accept-encoding" = "gzip"
2025/10/15 15:25:24 http2: Transport encoding header "user-agent" = "Go-http-client/2.0"
2025/10/15 15:25:24 http2: Framer 0xc00017ca80: wrote HEADERS flags=END_HEADERS stream=3 len=71
2025/10/15 15:25:24 http2: Framer 0xc00017ca80: wrote DATA flags=END_STREAM stream=3 len=774 data="code=<CODE>" (518 bytes omitted)
2025/10/15 15:25:24 http2: Framer 0xc00017ca80: read HEADERS flags=END_HEADERS stream=3 len=241
2025/10/15 15:25:24 http2: decoded hpack field header field ":status" = "400"
2025/10/15 15:25:24 http2: decoded hpack field header field "date" = "Wed, 15 Oct 2025 13:25:24 GMT"
2025/10/15 15:25:24 http2: decoded hpack field header field "server" = "Apache"
2025/10/15 15:25:24 http2: decoded hpack field header field "x-frame-options" = "SAMEORIGIN, SAMEORIGIN"
2025/10/15 15:25:24 http2: decoded hpack field header field "access-control-allow-origin" = "*"
2025/10/15 15:25:24 http2: decoded hpack field header field "access-control-allow-methods" = "GET, POST, OPTIONS"
2025/10/15 15:25:24 http2: decoded hpack field header field "access-control-allow-headers" = "Authorization, Content-Type, Accept"
2025/10/15 15:25:24 http2: decoded hpack field header field "access-control-max-age" = "86400"
2025/10/15 15:25:24 http2: decoded hpack field header field "set-cookie" = "__Host-JSESSIONID=DBA49B17915F1F2D2BC764C9AEABFA2C; Path=/; Secure; HttpOnly"
2025/10/15 15:25:24 http2: decoded hpack field header field "cache-control" = "no-store"
2025/10/15 15:25:24 http2: decoded hpack field header field "cache-control" = "no-store"
2025/10/15 15:25:24 http2: decoded hpack field header field "pragma" = "no-cache"
2025/10/15 15:25:24 http2: decoded hpack field header field "x-frame-options" = "DENY"
2025/10/15 15:25:24 http2: decoded hpack field header field "strict-transport-security" = "max-age=31536000"
2025/10/15 15:25:24 http2: decoded hpack field header field "content-security-policy" = "frame-ancestors 'none'; base-uri 'none';"
2025/10/15 15:25:24 http2: decoded hpack field header field "content-type" = "application/json;charset=UTF-8"
2025/10/15 15:25:24 http2: decoded hpack field header field "x-xss-protection" = "1; mode=block"
2025/10/15 15:25:24 http2: decoded hpack field header field "x-content-type-options" = "nosniff"
2025/10/15 15:25:24 http2: decoded hpack field header field "expires" = "0"
2025/10/15 15:25:24 http2: decoded hpack field header field "pragme" = "no-cache"
2025/10/15 15:25:24 http2: decoded hpack field header field "cache-control" = "no-cache, no-store, must-revalidate"
2025/10/15 15:25:24 http2: decoded hpack field header field "strict-transport-security" = "max-age=31536000 ; includeSubDomains ; preload"
2025/10/15 15:25:24 http2: Transport received HEADERS flags=END_HEADERS stream=3 len=241
2025/10/15 15:25:24 http2: Framer 0xc00017ca80: read DATA stream=3 len=64 data="{\"error\":\"invalid_request\",\"error_description\":\"UnableToDecode\"}"
2025/10/15 15:25:24 http2: Transport received DATA stream=3 len=64 data="{\"error\":\"invalid_request\",\"error_description\":\"UnableToDecode\"}"
2025/10/15 15:25:24 http2: Framer 0xc00017ca80: read DATA flags=END_STREAM stream=3 len=0 data=""
2025/10/15 15:25:24 http2: Transport received DATA flags=END_STREAM stream=3 len=0 data=""
2025/10/15 15:25:24 http2: Transport encoding header ":authority" = "issuer.example.org"
2025/10/15 15:25:24 http2: Transport encoding header ":method" = "POST"
2025/10/15 15:25:24 http2: Transport encoding header ":path" = "/idp/profile/oidc/token"
2025/10/15 15:25:24 http2: Transport encoding header ":scheme" = "https"
2025/10/15 15:25:24 http2: Transport encoding header "content-type" = "application/x-www-form-urlencoded"
2025/10/15 15:25:24 http2: Transport encoding header "content-length" = "791"
2025/10/15 15:25:24 http2: Transport encoding header "accept-encoding" = "gzip"
2025/10/15 15:25:24 http2: Transport encoding header "user-agent" = "Go-http-client/2.0"
2025/10/15 15:25:24 http2: Framer 0xc00017ca80: wrote HEADERS flags=END_HEADERS stream=5 len=12
2025/10/15 15:25:24 http2: Framer 0xc00017ca80: wrote DATA flags=END_STREAM stream=5 len=791 data="client_id=opkssh&code=<CODE>" (535 bytes omitted)
2025/10/15 15:25:24 http2: Framer 0xc00017ca80: read HEADERS flags=END_HEADERS stream=5 len=230
2025/10/15 15:25:24 http2: decoded hpack field header field ":status" = "401"
2025/10/15 15:25:24 http2: decoded hpack field header field "date" = "Wed, 15 Oct 2025 13:25:24 GMT"
2025/10/15 15:25:24 http2: decoded hpack field header field "server" = "Apache"
2025/10/15 15:25:24 http2: decoded hpack field header field "x-frame-options" = "SAMEORIGIN, SAMEORIGIN"
2025/10/15 15:25:24 http2: decoded hpack field header field "access-control-allow-origin" = "*"
2025/10/15 15:25:24 http2: decoded hpack field header field "access-control-allow-methods" = "GET, POST, OPTIONS"
2025/10/15 15:25:24 http2: decoded hpack field header field "access-control-allow-headers" = "Authorization, Content-Type, Accept"
2025/10/15 15:25:24 http2: decoded hpack field header field "access-control-max-age" = "86400"
2025/10/15 15:25:24 http2: decoded hpack field header field "set-cookie" = "__Host-JSESSIONID=05E221C14F6A2D2EBB6A82A20D9FCF31; Path=/; Secure; HttpOnly"
2025/10/15 15:25:24 http2: decoded hpack field header field "cache-control" = "no-store"
2025/10/15 15:25:24 http2: decoded hpack field header field "cache-control" = "no-store"
2025/10/15 15:25:24 http2: decoded hpack field header field "pragma" = "no-cache"
2025/10/15 15:25:24 http2: decoded hpack field header field "x-frame-options" = "DENY"
2025/10/15 15:25:24 http2: decoded hpack field header field "strict-transport-security" = "max-age=31536000"
2025/10/15 15:25:24 http2: decoded hpack field header field "content-security-policy" = "frame-ancestors 'none'; base-uri 'none';"
2025/10/15 15:25:24 http2: decoded hpack field header field "content-type" = "application/json;charset=UTF-8"
2025/10/15 15:25:24 http2: decoded hpack field header field "x-xss-protection" = "1; mode=block"
2025/10/15 15:25:24 http2: decoded hpack field header field "x-content-type-options" = "nosniff"
2025/10/15 15:25:24 http2: decoded hpack field header field "expires" = "0"
2025/10/15 15:25:24 http2: decoded hpack field header field "pragme" = "no-cache"
2025/10/15 15:25:24 http2: decoded hpack field header field "cache-control" = "no-cache, no-store, must-revalidate"
2025/10/15 15:25:24 http2: decoded hpack field header field "strict-transport-security" = "max-age=31536000 ; includeSubDomains ; preload"
2025/10/15 15:25:24 http2: Transport received HEADERS flags=END_HEADERS stream=5 len=230
2025/10/15 15:25:24 http2: Framer 0xc00017ca80: read DATA stream=5 len=77 data="{\"error\":\"invalid_client\",\"error_description\":\"Client authentication failed\"}"
2025/10/15 15:25:24 http2: Transport received DATA stream=5 len=77 data="{\"error\":\"invalid_client\",\"error_description\":\"Client authentication failed\"}"
2025/10/15 15:25:24 http2: Framer 0xc00017ca80: read DATA flags=END_STREAM stream=5 len=0 data=""
2025/10/15 15:25:24 http2: Transport received DATA flags=END_STREAM stream=5 len=0 data=""

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions