Skip to content

feat(ciba): RFC 9509 backchannel authentication (Poll + Ping modes) - #66

Merged
thegdsks merged 3 commits into
mainfrom
feat/ciba-2026-06-22
Jun 22, 2026
Merged

feat(ciba): RFC 9509 backchannel authentication (Poll + Ping modes)#66
thegdsks merged 3 commits into
mainfrom
feat/ciba-2026-06-22

Conversation

@thegdsks

Copy link
Copy Markdown
Member

Summary

  • Implements CIBA (OpenID Client-Initiated Backchannel Authentication, RFC 9509) for the theauth-go authorization server in Poll and Ping delivery modes; Push mode deferred
  • Adds POST /oauth/bc-authorize endpoint and extends POST /oauth/token with grant_type=urn:openid:params:grant-type:ciba; exposes ApproveBackchannelAuth / DenyBackchannelAuth service methods on the root TheAuth type
  • Ships CIBAStorage as an optional duck-typed interface; CIBA auto-disables at startup when the configured storage does not satisfy it, keeping deployments that do not need CIBA unaffected

Changes

New files

File Purpose
ciba.go Root-package re-exports: AuthenticationDevice, CIBAConfig, CIBANotification, approve/deny methods
storage_ciba.go Root-package CIBAStorage interface alias
internal/as/ciba.go BackchannelAuthenticate, PollBackchannelToken, ApproveBackchannelRequest, DenyBackchannelRequest
internal/as/config_ciba.go CIBAConfig, AuthenticationDevice interface, CIBANotification, defaults
internal/as/storage_ciba.go CIBAStorage interface (5 methods)
internal/as/token_ciba.go MintCIBATokens helper delegating to existing mintAccessAndRefresh
internal/as/handlers/handlers_ciba.go HTTP handler for bc-authorize; error mapping for CIBA token errors
internal/as/e2e_ciba_test.go 7 E2E tests (see below)
internal/models/models_ciba.go BackchannelRequest struct and status/grant-type constants
internal/models/errors_ciba.go Sentinel errors: authorization_pending, slow_down, access_denied, expired_token, etc.
storage/memory/memory_ciba.go In-memory CIBAStorage implementation
storage/postgres/postgres_ciba.go Postgres CIBAStorage implementation using pgx/v5
storage/postgres/migrations/0016_backchannel_requests.{up,down}.sql Schema migration

Modified files

  • internal/as/config.go: added CIBA *CIBAConfig field; validation calls applyCIBADefaults
  • internal/as/dcr.go: redirect_uris no longer required for CIBA-only or client-credentials clients; added GrantTypeCIBA/GrantTypeClientCredentials/GrantTypeTokenExchange to the allowed set
  • internal/as/metadata.go: ASMetadataDoc and grantTypesAdvertised conditionally populate CIBA discovery fields
  • internal/as/handlers/handlers.go: mounts /oauth/bc-authorize when CIBA enabled; dispatches CIBA grant in handleToken
  • as.go, wiring.go, models.go, errors.go: thin wiring and re-exports
  • storage/memory/memory.go: adds ciba *cibaState to in-memory store
  • storagetest/helpers.go: formatting fix

E2E test coverage

  1. TestCIBAPollHappyPath: full round-trip: register, bc-authorize, pending poll, approve, token poll returns access token
  2. TestCIBASlowDown: rapid consecutive polls trigger slow_down (400)
  3. TestCIBAAccessDenied: DenyBackchannelAuth causes next poll to return access_denied
  4. TestCIBAExpiredToken: request with 2s TTL returns expired_token after expiry
  5. TestCIBARequiresLoginHint: bc-authorize with no hint returns invalid_request (400)
  6. TestCIBABindingMessageInNotification: binding_message is forwarded to AuthenticationDevice.Notify
  7. TestCIBAStorageMissing: storage that does not implement CIBAStorage causes 404 on bc-authorize

Test plan

  • gofmt -l . returns empty (verified clean)
  • go vet ./... passes (verified)
  • go test -race -count=3 ./... passes (verified with 300s timeout; sleep-heavy CIBA tests need ~180s per count on CI)
  • No em-dashes in any .go file (verified)
  • mcpresource/ is untouched (verified via git diff origin/main -- mcpresource/)

@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Benchmark regression report

Threshold: 25%

goos: linux
goarch: amd64
pkg: github.com/glincker/theauth-go
cpu: AMD EPYC 7763 64-Core Processor                
                     │ base-bench.txt │         pr-bench.txt          │
                     │     sec/op     │   sec/op     vs base          │
RateLimitReadHeavy-4      81.07n ± 3%   81.27n ± 4%  ~ (p=0.579 n=10)

                     │ base-bench.txt │          pr-bench.txt          │
                     │      B/op      │    B/op     vs base            │
RateLimitReadHeavy-4       0.000 ± 0%   0.000 ± 0%  ~ (p=1.000 n=10) ¹
¹ all samples are equal

                     │ base-bench.txt │          pr-bench.txt          │
                     │   allocs/op    │ allocs/op   vs base            │
RateLimitReadHeavy-4       0.000 ± 0%   0.000 ± 0%  ~ (p=1.000 n=10) ¹
¹ all samples are equal

pkg: github.com/glincker/theauth-go/crypto
               │ base-bench.txt │            pr-bench.txt            │
               │     sec/op     │   sec/op     vs base               │
Argon2Hash-4        49.79m ± 2%   47.43m ± 2%  -4.74% (p=0.000 n=10)
Argon2Verify-4      49.73m ± 2%   47.88m ± 1%  -3.72% (p=0.000 n=10)
geomean             49.76m        47.65m       -4.23%

               │ base-bench.txt │            pr-bench.txt             │
               │      B/op      │     B/op      vs base               │
Argon2Hash-4       64.01Mi ± 0%   64.01Mi ± 0%       ~ (p=0.631 n=10)
Argon2Verify-4     64.01Mi ± 0%   64.01Mi ± 0%       ~ (p=0.109 n=10)
geomean            64.01Mi        64.01Mi       -0.00%

               │ base-bench.txt │           pr-bench.txt            │
               │   allocs/op    │ allocs/op   vs base               │
Argon2Hash-4         102.0 ± 0%   102.0 ± 0%       ~ (p=1.000 n=10)
Argon2Verify-4       103.0 ± 1%   103.0 ± 1%       ~ (p=1.000 n=10)
geomean              102.5        102.5       +0.00%

pkg: github.com/glincker/theauth-go/internal/bench
                               │ base-bench.txt │            pr-bench.txt             │
                               │     sec/op     │   sec/op     vs base                │
AuditRedactor-4                     1.411µ ± 2%   1.379µ ± 0%   -2.30% (p=0.000 n=10)
OAuthCallback-4                     1.119µ ± 2%   1.006µ ± 0%  -10.06% (p=0.000 n=10)
OAuthCodeFlow-4                     49.89µ ± 1%   49.02µ ± 2%   -1.75% (p=0.002 n=10)
OAuthTokenEndpointRefreshHit-4      50.89µ ± 1%   49.67µ ± 1%   -2.40% (p=0.000 n=10)
JWKSEndpoint-4                      7.469µ ± 2%   6.784µ ± 1%   -9.18% (p=0.000 n=10)
SCIMTokenAuth-4                     22.20µ ± 3%   18.95µ ± 2%  -14.61% (p=0.000 n=10)
SessionLookup-4                    11.205µ ± 4%   9.456µ ± 2%  -15.61% (p=0.000 n=10)
geomean                             9.587µ        8.806µ        -8.15%

                               │ base-bench.txt │             pr-bench.txt              │
                               │      B/op      │     B/op      vs base                 │
AuditRedactor-4                      672.0 ± 0%     672.0 ± 0%       ~ (p=1.000 n=10) ¹
OAuthCallback-4                    1.344Ki ± 0%   1.344Ki ± 0%       ~ (p=1.000 n=10) ¹
OAuthCodeFlow-4                    15.79Ki ± 0%   15.79Ki ± 0%  -0.03% (p=0.022 n=10)
OAuthTokenEndpointRefreshHit-4     15.55Ki ± 0%   15.54Ki ± 0%  -0.01% (p=0.000 n=10)
JWKSEndpoint-4                     2.963Ki ± 0%   2.963Ki ± 0%       ~ (p=1.000 n=10) ¹
SCIMTokenAuth-4                    9.765Ki ± 0%   9.765Ki ± 0%       ~ (p=0.317 n=10)
SessionLookup-4                    8.451Ki ± 0%   8.451Ki ± 0%       ~ (p=1.000 n=10)
geomean                            4.729Ki        4.729Ki       -0.01%
¹ all samples are equal

                               │ base-bench.txt │            pr-bench.txt             │
                               │   allocs/op    │ allocs/op   vs base                 │
AuditRedactor-4                      4.000 ± 0%   4.000 ± 0%       ~ (p=1.000 n=10) ¹
OAuthCallback-4                      5.000 ± 0%   5.000 ± 0%       ~ (p=1.000 n=10) ¹
OAuthCodeFlow-4                      100.0 ± 0%   100.0 ± 0%       ~ (p=1.000 n=10) ¹
OAuthTokenEndpointRefreshHit-4       100.0 ± 0%   100.0 ± 0%       ~ (p=1.000 n=10) ¹
JWKSEndpoint-4                       37.00 ± 0%   37.00 ± 0%       ~ (p=1.000 n=10) ¹
SCIMTokenAuth-4                      56.00 ± 0%   56.00 ± 0%       ~ (p=1.000 n=10) ¹
SessionLookup-4                      42.00 ± 0%   42.00 ± 0%       ~ (p=1.000 n=10) ¹
geomean                              29.04        29.04       +0.00%
¹ all samples are equal

pkg: github.com/glincker/theauth-go/internal/jwt
            │ base-bench.txt │            pr-bench.txt            │
            │     sec/op     │   sec/op     vs base               │
JWTSign-4        31.29µ ± 1%   30.98µ ± 0%  -1.00% (p=0.000 n=10)
JWTVerify-4      72.23µ ± 0%   72.12µ ± 0%       ~ (p=0.247 n=10)
geomean          47.54µ        47.27µ       -0.58%

            │ base-bench.txt │             pr-bench.txt              │
            │      B/op      │     B/op      vs base                 │
JWTSign-4       2.782Ki ± 0%   2.782Ki ± 0%       ~ (p=1.000 n=10) ¹
JWTVerify-4     3.422Ki ± 0%   3.422Ki ± 0%       ~ (p=1.000 n=10) ¹
geomean         3.086Ki        3.086Ki       +0.00%
¹ all samples are equal

            │ base-bench.txt │            pr-bench.txt             │
            │   allocs/op    │ allocs/op   vs base                 │
JWTSign-4         15.00 ± 0%   15.00 ± 0%       ~ (p=1.000 n=10) ¹
JWTVerify-4       61.00 ± 0%   61.00 ± 0%       ~ (p=1.000 n=10) ¹
geomean           30.25        30.25       +0.00%
¹ all samples are equal

Generated by the bench workflow. A + delta means the PR branch is slower.

@thegdsks
thegdsks force-pushed the feat/ciba-2026-06-22 branch from 5b3e5b0 to c098003 Compare June 22, 2026 20:34
thegdsks added 3 commits June 22, 2026 15:55
Implements CIBA (OpenID Client-Initiated Backchannel Authentication) for
the authorization server. Adds POST /oauth/bc-authorize, extends
POST /oauth/token with grant_type=urn:openid:params:grant-type:ciba,
and provides ApproveBackchannelAuth / DenyBackchannelAuth service methods.

Key additions:
- AuthenticationDevice interface for out-of-band push notifications (FCM/APNs/SMS)
- CIBAStorage optional interface; CIBA auto-disables when storage omits it
- CIBAConfig in AuthorizationServerConfig with sensible defaults
- Memory and Postgres storage implementations; migration 0016
- AS metadata advertising CIBA endpoints and delivery modes
- DCR updated: redirect_uris not required for CIBA-only clients
- 7 E2E tests covering happy path, slow_down, access_denied, expired_token,
  login_hint validation, binding_message propagation, and missing storage
@thegdsks
thegdsks force-pushed the feat/ciba-2026-06-22 branch from c098003 to cebfca0 Compare June 22, 2026 20:58
@thegdsks
thegdsks merged commit 20e3bd4 into main Jun 22, 2026
4 checks passed
@thegdsks
thegdsks deleted the feat/ciba-2026-06-22 branch June 22, 2026 21:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant