Summary
Add PKCE and state parameter validation to OAuth callback — The OAuth callback endpoint accepts code without verifying the state anti-CSRF parameter, enabling authorization code injection.
Social Media Link
Let's collaborate on Discord. And ensure to star our repo.
Problem Statement
Confirmed in project-portal/project-portal-backend/internal/integration/handler.go and project-portal/project-portal-backend/internal/integration/service.go:
-
InitiateOAuth2 never generates a state value: Service.InitiateOAuth2 returns a hardcoded string "https://" + provider + ".com/oauth/authorize?client_id=..." with no state query parameter appended at all.
-
OAuth2Callback never validates state: Handler.OAuth2Callback reads only provider and code from the query string (c.Query("code")) — it never reads or checks a state parameter.
-
No PKCE code_verifier/code_challenge support: Neither InitiateOAuth2 nor HandleOAuth2Callback generates a code_verifier, derives a code_challenge, or sends it as part of the authorization/token exchange, leaving public-client flows vulnerable to authorization code interception.
-
HandleOAuth2Callback only checks code is non-empty: The entire validation in service.go is if code == "" { return errors.New("invalid code") } — no actual token exchange, no signature or origin validation of the callback request.
-
No server-side state storage: There is no repository method or in-memory store for issued state values, so even if a state were generated, there would be nowhere to look it up and mark it consumed.
-
No expiry on state/code_verifier values: Because none exist, there's naturally no TTL enforcement, meaning a leaked or replayed authorization flow could not be time-bound even if state tracking were added.
-
No single-use enforcement: A state value (once implemented) needs to be invalidated after first use to prevent replay — this logic is entirely absent.
-
OAuth2Authorize builds the authorization URL client-side agnostic of PKCE: Handler.OAuth2Authorize calls h.service.InitiateOAuth2(ctx, provider) and redirects — no code_challenge_method=S256 parameter is ever added to the outbound URL.
-
No token exchange implementation: HandleOAuth2Callback has a comment // Exchange code for token and save and // Mock saving token — the actual HTTP call to the provider's token endpoint is never made.
-
No origin/redirect_uri validation: There's no check that the callback's redirect_uri matches what was registered for the connection, which is required to prevent redirect_uri manipulation attacks.
-
IncomingWebhook has a similar placeholder comment for signature verification: Handler.IncomingWebhook comment says // Verify signature logic would go here and unconditionally returns 200 — related to, but distinct from, the OAuth flow gap (tracked here since it sits in the same handler file).
-
No test coverage for the OAuth flow: There is no test file for internal/integration covering the authorize/callback state or PKCE behavior.
Required Changes
-
Generate a cryptographically random state value in InitiateOAuth2, persist it (with an expiry) keyed by provider/user, and append it to the authorization URL.
-
Add state parameter reading and validation to OAuth2Callback/HandleOAuth2Callback, rejecting the request if state is missing, unknown, expired, or already consumed.
-
Generate a PKCE code_verifier and derive code_challenge (S256) in InitiateOAuth2; store the verifier server-side keyed by state.
-
Add code_challenge and code_challenge_method=S256 query parameters to the outbound authorization URL.
-
Implement the actual token exchange HTTP call in HandleOAuth2Callback, including the code_verifier in the request body per PKCE spec.
-
Add single-use enforcement: mark a state value consumed immediately after validation so it cannot be replayed.
-
Add a TTL (e.g. 10 minutes) on stored state/code_verifier pairs with cleanup of expired entries.
-
Add redirect_uri validation against the value registered for the integration connection.
-
Persist the exchanged access/refresh token via the connection repository instead of the current no-op comment.
-
Add structured error responses distinguishing "invalid state", "expired state", and "token exchange failed".
-
Add unit tests for the authorize → callback → token-exchange flow, covering valid, missing, expired, and replayed state.
-
Implement webhook signature verification in IncomingWebhook to close the adjacent placeholder in the same file.
Acceptance Criteria
InitiateOAuth2 generates and persists a unique state and PKCE code_verifier/code_challenge pair.
- The outbound authorization URL includes
state and code_challenge/code_challenge_method=S256.
OAuth2Callback rejects requests with missing, unknown, or expired state.
- A
state value cannot be successfully reused after its first valid callback.
- Token exchange sends
code_verifier and completes a real HTTP call to the provider's token endpoint.
- Exchanged tokens are persisted, not just logged in a comment.
redirect_uri mismatches are rejected.
- Expired
state/code_verifier entries are cleaned up.
- Unit tests cover valid, missing, expired, and replayed state scenarios.
IncomingWebhook verifies signatures instead of unconditionally returning success.
Directory to Work on:
project-portal/project-portal-backend/
Summary
Add PKCE and state parameter validation to OAuth callback — The OAuth callback endpoint accepts
codewithout verifying thestateanti-CSRF parameter, enabling authorization code injection.Social Media Link
Let's collaborate on Discord. And ensure to star our repo.
Problem Statement
Confirmed in
project-portal/project-portal-backend/internal/integration/handler.goandproject-portal/project-portal-backend/internal/integration/service.go:InitiateOAuth2never generates astatevalue:Service.InitiateOAuth2returns a hardcoded string"https://" + provider + ".com/oauth/authorize?client_id=..."with nostatequery parameter appended at all.OAuth2Callbacknever validatesstate:Handler.OAuth2Callbackreads onlyproviderandcodefrom the query string (c.Query("code")) — it never reads or checks astateparameter.No PKCE
code_verifier/code_challengesupport: NeitherInitiateOAuth2norHandleOAuth2Callbackgenerates acode_verifier, derives acode_challenge, or sends it as part of the authorization/token exchange, leaving public-client flows vulnerable to authorization code interception.HandleOAuth2Callbackonly checkscodeis non-empty: The entire validation inservice.goisif code == "" { return errors.New("invalid code") }— no actual token exchange, no signature or origin validation of the callback request.No server-side state storage: There is no repository method or in-memory store for issued
statevalues, so even if astatewere generated, there would be nowhere to look it up and mark it consumed.No expiry on
state/code_verifiervalues: Because none exist, there's naturally no TTL enforcement, meaning a leaked or replayed authorization flow could not be time-bound even if state tracking were added.No single-use enforcement: A
statevalue (once implemented) needs to be invalidated after first use to prevent replay — this logic is entirely absent.OAuth2Authorizebuilds the authorization URL client-side agnostic of PKCE:Handler.OAuth2Authorizecallsh.service.InitiateOAuth2(ctx, provider)and redirects — nocode_challenge_method=S256parameter is ever added to the outbound URL.No token exchange implementation:
HandleOAuth2Callbackhas a comment// Exchange code for token and saveand// Mock saving token— the actual HTTP call to the provider's token endpoint is never made.No origin/redirect_uri validation: There's no check that the callback's
redirect_urimatches what was registered for the connection, which is required to prevent redirect_uri manipulation attacks.IncomingWebhookhas a similar placeholder comment for signature verification:Handler.IncomingWebhookcomment says// Verify signature logic would go hereand unconditionally returns 200 — related to, but distinct from, the OAuth flow gap (tracked here since it sits in the same handler file).No test coverage for the OAuth flow: There is no test file for
internal/integrationcovering the authorize/callback state or PKCE behavior.Required Changes
Generate a cryptographically random
statevalue inInitiateOAuth2, persist it (with an expiry) keyed by provider/user, and append it to the authorization URL.Add
stateparameter reading and validation toOAuth2Callback/HandleOAuth2Callback, rejecting the request ifstateis missing, unknown, expired, or already consumed.Generate a PKCE
code_verifierand derivecode_challenge(S256) inInitiateOAuth2; store the verifier server-side keyed bystate.Add
code_challengeandcode_challenge_method=S256query parameters to the outbound authorization URL.Implement the actual token exchange HTTP call in
HandleOAuth2Callback, including thecode_verifierin the request body per PKCE spec.Add single-use enforcement: mark a
statevalue consumed immediately after validation so it cannot be replayed.Add a TTL (e.g. 10 minutes) on stored
state/code_verifierpairs with cleanup of expired entries.Add
redirect_urivalidation against the value registered for the integration connection.Persist the exchanged access/refresh token via the connection repository instead of the current no-op comment.
Add structured error responses distinguishing "invalid state", "expired state", and "token exchange failed".
Add unit tests for the authorize → callback → token-exchange flow, covering valid, missing, expired, and replayed
state.Implement webhook signature verification in
IncomingWebhookto close the adjacent placeholder in the same file.Acceptance Criteria
InitiateOAuth2generates and persists a uniquestateand PKCEcode_verifier/code_challengepair.stateandcode_challenge/code_challenge_method=S256.OAuth2Callbackrejects requests with missing, unknown, or expiredstate.statevalue cannot be successfully reused after its first valid callback.code_verifierand completes a real HTTP call to the provider's token endpoint.redirect_urimismatches are rejected.state/code_verifierentries are cleaned up.IncomingWebhookverifies signatures instead of unconditionally returning success.Directory to Work on:
project-portal/project-portal-backend/