Skip to content

feat: [LC-1663] Streamline Auto-verify email #1122

Merged
goblincore merged 8 commits intomainfrom
lc-1663-streamlined-jwt
Mar 31, 2026
Merged

feat: [LC-1663] Streamline Auto-verify email #1122
goblincore merged 8 commits intomainfrom
lc-1663-streamlined-jwt

Conversation

@goblincore
Copy link
Copy Markdown
Collaborator

@goblincore goblincore commented Mar 25, 2026

Overview

🎟 Relevant Jira Issues

LC-1663

πŸ“š What is the context and goal of this PR?

Implement provider-agnostic OIDC JWT verification so that email ContactMethods can be auto-verified at profile creation time, eliminating the separate email verification round-trip and enabling immediate finalization of inbox credentials (e.g. from embed SDK claims) when users sign up.

Previously, email verification required the useAutoVerifyContactMethodWithProofOfLogin client hook to fire after login, followed by useFinalizeInboxCredentials to sign and store pending credentials. This multi-step async chain was fragile and slow - users who received credentials before signing up had to wait for two separate hook cycles before the credential appeared in their wallet.

Now the brain-service verifies the auth provider's JWT inline during createProfile, verifies the email ContactMethod immediately (so that eliminates that round trip via useAutoVerifyContactMethodWithProofOfLogin), and the existing client finalization hook picks up pending credentials on first login(this part is same as before).

πŸ₯΄ TL; RL:

  • OIDC JWT verification helper - jose-based verification using standard OIDC discovery (/.well-known/openid-configuration). Supports Firebase, Keycloak, Okta, Auth0 - adding a provider is a config change, not a code change
  • createProfile route enhancement - accepts optional authToken, verifies JWT, creates/links/verifies ContactMethod in the same request
  • Finalization refactor - extracted inbox credential finalization from inbox.ts into reusable finalizeInboxCredentialsForProfile helper
  • Client-side token forwarding - OnboardingNetworkForm retrieves Firebase ID token and passes it during signup

πŸ’‘ Feature Breakdown (screenshots & videos encouraged!)

This pretty much exactly follows the spec outlined in the JIRA:

https://www.loom.com/share/0767c3df46524477a9f4c411169058e4

(In the video I forgot to mention the added env variables you need though if you read the testing instructions in the QA instructions further down you will see it, but just to make sure - you also need to set the brain service ENV variables:

OIDC_TRUSTED_ISSUERS=https://securetoken.google.com/learncard
OIDC_EXPECTED_AUDIENCE=learncard

)

1. OIDC JWT Verification (oidc-jwt.helpers.ts)
(SEE JIRA for a more detailed explanation of what OIDC is): "Every major provider (Firebase, Keycloak, Okta, Auth0, etc.) supports OpenID Connect Discovery. They all publish a /.well-known/openid-configuration document that contains a jwks_uri pointing to their public keys."

  • Uses jose library (~45KB, zero deps) for JWT verification via OIDC discovery
  • Provider config via env vars: OIDC_TRUSTED_ISSUERS (comma-separated issuer URLs) and OIDC_EXPECTED_AUDIENCE
  • JWKS caching per issuer (respects Cache-Control headers)
  • Configurable claim extraction (emailClaim, emailVerifiedClaim) per provider
  • Returns null on any failure - never throws

2. createProfile route (profiles.ts)

  • New optional authToken field on input (backwards compatible)
  • On valid JWT with verified email:
    • Finds or creates ContactMethod node
    • Links to new profile via HAS_CONTACT_METHOD
    • Cleans up stale relationships (deleteAllProfileContactMethodRelationshipsExceptForProfileId)
    • Marks ContactMethod as verified
  • Verification failure is non-fatal - profile still created, warning logged
  • Does NOT call finalizeInboxCredentialsForProfile for now and uses existing hook (was causing race condition)

3. Finalization helper extraction (finalize-inbox.helpers.ts)

  • Moved ~220 lines of finalization logic from inbox.ts route into standalone helper
  • Handles: unsigned credential signing, subject DID injection, SA resolution, ISSUED/CLAIMED status updates, webhook dispatch, activity logging (both success and failure paths)
  • Called by /inbox/finalize route (existing)

4. Client-side token forwarding (OnboardingNetworkForm.tsx)

  • Gets Firebase ID token via FirebaseAuthentication.getIdToken() (Capacitor native) or auth().currentUser.getIdToken() (web)
  • Passes as authToken in createProfile call
  • Graceful fallback, if token retrieval fails, signup proceeds without auto-verify

5. Plugin type update (learn-card-network/src/types.ts)

  • createProfile method signature updated with optional authToken?: string

πŸ›  Important tradeoffs made:

  • jose over Firebase Admin SDK - Provider-agnostic OIDC verification means no vendor SDK dependency. Adding Keycloak/Okta/Auth0 is a config entry, not a dependency.
  • Env var config over DB config - OIDC_TRUSTED_ISSUERS and OIDC_EXPECTED_AUDIENCE are env vars rather than stored in Neo4j. Simpler to manage per-environment and doesn't require a migration.

πŸ” Types of Changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Chore (refactor, documentation update, etc)

πŸ’³ Does This Create Any New Technical Debt? ( If yes, please describe and add JIRA TODOs )

  • No
  • Yes

Testing

πŸ”¬ How Can Someone QA This?

Prerequisites: Set env vars on brain-service:

OIDC_TRUSTED_ISSUERS=https://securetoken.google.com/learncard
OIDC_EXPECTED_AUDIENCE=learncard
  1. Happy path - new signup with auto-verified email:

    • Sign up with a new email via Firebase auth
    • Complete onboarding (profileId, displayName, etc.)
    • After signup, verify the email shows as verified immediately (no email verification step)
  2. Embed claim β†’ signup β†’ credential appears:

    • From the developer portal embed SDK, send a credential to an email that does NOT have an account
    • Sign up with that email
    • After signup + first login, the credential should appear in the wallet (finalized by the client hook)

πŸ“± πŸ–₯ Which devices would you like help testing on?

Chromium / Safari (desktop web), iOS (Capacitor native)

πŸ§ͺ Code Coverage

OIDC JWT Unit Tests (test/oidc-jwt.spec.ts) β€” 11 tests, run via npx vitest run --config vitest.unit.config.ts test/oidc-jwt.spec.ts:

  • Valid JWT β†’ extracts email, emailVerified, uid, provider
  • Empty/missing OIDC_TRUSTED_ISSUERS β†’ returns null (no-op)
  • Unknown issuer, malformed token, empty token β†’ returns null
  • Wrong signing key β†’ signature verification fails β†’ returns null
  • Expired token β†’ returns null
  • Wrong audience β†’ returns null
  • emailVerified: false β†’ returned as-is (not suppressed)
  • Multiple comma-separated issuers β†’ matches correct provider
  • Missing email claim β†’ returns { email: '', emailVerified: false }

createProfile + authToken Integration Tests (test/profiles.spec.ts) β€” 6 tests added (require Neo4j via testcontainers):

  • Valid authToken β†’ ContactMethod created, linked, marked verified
  • No authToken β†’ profile created normally, verifyAuthToken never called
  • Token verification returns null β†’ profile still created, no ContactMethod
  • Token verification throws β†’ profile still created (non-fatal)
  • emailVerified: false β†’ no ContactMethod created
  • Existing ContactMethod β†’ reused, not duplicated

The finalizeInboxCredentialsForProfile helper is covered by the existing finalize route tests (refactor only, no logic change).

Documentation

πŸ“ Documentation Checklist

User-Facing Docs (docs/ β†’ docs.learncard.com)

  • Tutorial β€” New capability that users need to learn (docs/tutorials/)
  • How-To Guide β€” New workflow or integration (docs/how-to-guides/)
  • Reference β€” New/changed API, config, or SDK method (docs/sdks/)
  • Concept β€” New mental model or architecture explanation (docs/core-concepts/)
  • App Flows β€” Changes to LearnCard App or ScoutPass user flows (docs/apps/)

Internal/AI Docs

  • AGENTS.md β€” New pattern, flow, or context that AI assistants need
  • Code comments/JSDoc β€” Complex logic that needs inline explanation

Visual Documentation

  • Mermaid diagram β€” Complex flow, state machine, or architecture

πŸ’­ Documentation Notes

Internal infrastructure change, no user-facing doc changes needed. The OIDC_TRUSTED_ISSUERS / OIDC_EXPECTED_AUDIENCE env vars need to be documented in deployment runbooks for staging/production.

βœ… PR Checklist

  • Related to a Jira issue (create one if not)
  • My code follows style guidelines (eslint / prettier)
  • I have manually tested common end-2-end cases
  • I have reviewed my code
  • I have commented my code, particularly where ambiguous
  • New and existing unit tests pass locally with my changes
  • I have completed the Documentation Checklist above (or explained why N/A)
  • I have considered product analytics for user-facing features (use @analytics in learn-card-app)

πŸš€ Ready to squash-and-merge?:

  • Code is backwards compatible
  • There is not a "Do Not Merge" label on this PR
  • I have thoughtfully considered the security implications of this change.
  • This change does not expose new public facing endpoints that do not have authentication

✨ PR Description

Purpose: Add OpenID Connect (OIDC) JWT-based email auto-verification during profile creation to streamline user onboarding by eliminating manual verification steps.

Main changes:

  • Implemented OIDC JWT verification module with multi-provider support and JWKS caching for Firebase/Keycloak authentication tokens
  • Enhanced createProfile endpoint to accept authToken parameter and automatically verify email addresses via OIDC claims
  • Refactored inbox credential finalization logic into separate helper module to prevent race conditions with client-side wallet uploads

Generated by LinearB AI and added by gitStream.
AI-generated content may contain inaccuracies. Please verify before using.
πŸ’‘ Tip: You can customize your AI Description using Guidelines Learn how

…ation

Add provider-agnostic OIDC JWT verification to the createProfile route
so email ContactMethod is verified during onboarding instead of
requiring a 3-hop VP-JWT chain. Pending inbox credentials are also
finalized inline (fire-and-forget) for faster embed SDK claim resolution.

- Add `jose` for OIDC discovery-based JWT verification
- New `oidc-jwt.helpers.ts`: verifyAuthToken using JWKS + issuer matching
- New `finalize-inbox.helpers.ts`: extracted from inbox.ts finalize route
- Extend createProfile input with optional `authToken` field
- Client passes Firebase ID token during onboarding
- Env vars: OIDC_TRUSTED_ISSUERS, OIDC_EXPECTED_AUDIENCE

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Mar 25, 2026

πŸ¦‹ Changeset detected

Latest commit: 3a12ae1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 18 packages
Name Type
learn-card-app Patch
@learncard/network-plugin Patch
@learncard/network-brain-service Patch
@learncard/init Patch
@learncard/network-brain-client Patch
@learncard/chapi-example Patch
@learncard/app-store-demo-basic-launchpad Patch
@learncard/app-store-demo-lore-card Patch
@learncard/app-store-demo-mozilla-social-badges Patch
@learncard/create-http-bridge Patch
@learncard/cli Patch
@learncard/react Patch
@learncard/lca-api-plugin Patch
learn-card-discord-bot Patch
@learncard/lca-api-service Patch
scoutpass-app Patch
@learncard/snap-chapi-example Patch
learn-card-base Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@netlify
Copy link
Copy Markdown

netlify Bot commented Mar 25, 2026

βœ… Deploy Preview for app-store-1-basic-launchpad-app canceled.

Name Link
πŸ”¨ Latest commit 3a12ae1
πŸ” Latest deploy log https://app.netlify.com/projects/app-store-1-basic-launchpad-app/deploys/69cc26e18a4943000810fbde

@netlify
Copy link
Copy Markdown

netlify Bot commented Mar 25, 2026

βœ… Deploy Preview for learncarddocs canceled.

Name Link
πŸ”¨ Latest commit 3a12ae1
πŸ” Latest deploy log https://app.netlify.com/projects/learncarddocs/deploys/69cc26e1469d9f0008d5ebd5

@netlify
Copy link
Copy Markdown

netlify Bot commented Mar 25, 2026

βœ… Deploy Preview for staging-learncardapp ready!

Name Link
πŸ”¨ Latest commit 3a12ae1
πŸ” Latest deploy log https://app.netlify.com/projects/staging-learncardapp/deploys/69cc26e173f8df000811ef13
😎 Deploy Preview https://deploy-preview-1122--staging-learncardapp.netlify.app
πŸ“± Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions
Copy link
Copy Markdown
Contributor

πŸ‘‹ Hey there! It looks like you modified code, but didn't update the documentation in /docs.

If this PR introduces new features, changes APIs, or modifies behavior that users or developers need to know about, please consider updating the docs.


πŸ„ Windsurf Tip

You can ask Windsurf to help:

"Analyze the changes in this PR and update the gitbook docs in /docs accordingly."

Windsurf will review your changes and suggest appropriate documentation updates based on what was modified.


πŸ“š Documentation Guide
Change Type Doc Location
New feature/API docs/tutorials/ or docs/how-to-guides/
SDK/API changes docs/sdks/
New concepts docs/core-concepts/
App UI/UX flows docs/apps/ (LearnCard App, ScoutPass)
Internal patterns AGENTS.md

This is an automated reminder. If no docs are needed, feel free to ignore this message.

…ofile authToken

- 11 unit tests for verifyAuthToken: valid JWT, unknown issuer, malformed/expired/wrong-key tokens, wrong audience, multiple issuers, missing claims
- 6 integration tests for createProfile + authToken: auto-verify email, backwards compat, non-fatal failures, emailVerified=false handling
- Add oidc-jwt.spec.ts to vitest.unit.config.ts (no Neo4j dependency)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add changeset for email auto-verification feature.
@goblincore goblincore marked this pull request as ready for review March 30, 2026 15:44
@goblincore
Copy link
Copy Markdown
Collaborator Author

@claude please review

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 30, 2026

Claude finished @goblincore's task β€”β€” View job


PR Review Complete

I've conducted a comprehensive review of PR #1122: feat: LC-1663 Streamline Auto-verify email

πŸ” Review Summary

This PR implements provider-agnostic OIDC JWT verification for automatic email verification during profile creation. The implementation is well-designed, secure, and follows best practices.

βœ… Recommendation: Cannot Auto-Approve - Manual Review Required

Reason: This PR touches high-risk areas (services/ backend code, auth-related functionality) and contains significant authentication logic changes that require human review per our guidelines.

πŸ“‹ Detailed Analysis

Security Assessment

βœ… Strong Security Implementation

  • JWT verification uses industry-standard jose library with proper signature validation
  • OIDC discovery follows OAuth 2.0/OIDC specifications correctly
  • Environment-based config prevents hardcoded secrets (OIDC_TRUSTED_ISSUERS, OIDC_EXPECTED_AUDIENCE)
  • Graceful failure handling - invalid tokens return null, don't crash the system
  • Non-fatal approach - profile creation succeeds even if email verification fails
  • No credential exposure - tokens are validated server-side, never logged

Code Quality Assessment

βœ… Excellent Implementation Quality

OIDC JWT Helper (services/learn-card-network/brain-service/src/helpers/oidc-jwt.helpers.ts:1-71):

  • Clean provider-agnostic design supporting multiple OIDC providers
  • Proper JWKS caching with issuer-based keys
  • Configurable claim extraction for different provider schemas
  • Comprehensive error handling with logging

Profiles Route Enhancement (services/learn-card-network/brain-service/src/routes/profiles.ts:103-130):

  • Backward compatible - authToken is optional
  • Proper ContactMethod lifecycle: find-or-create β†’ link β†’ verify
  • Race condition prevention noted in comments
  • Non-blocking implementation preserves existing signup flow

Client Integration (apps/learn-card-app/src/components/onboarding/onboardingNetworkForm/OnboardingNetworkForm.tsx:292-324):

  • Cross-platform token retrieval (Capacitor native + web)
  • Graceful degradation if token retrieval fails
  • Non-fatal error handling with appropriate logging

Architecture Assessment

βœ… Well-Architected Solution

Refactoring Excellence:

  • Extracted 220+ lines of inbox finalization logic into reusable finalizeInboxCredentialsForProfile helper
  • Clean separation of concerns between verification, profile creation, and credential finalization
  • Maintains existing client-side finalization hook workflow

Provider Agnostic Design:

  • Adding new OIDC providers requires only environment variable changes
  • No vendor lock-in (Firebase, Keycloak, Okta, Auth0 all supported)
  • Standard OIDC discovery endpoint usage

Testing Assessment

βœ… Comprehensive Test Coverage

Unit Tests (test/oidc-jwt.spec.ts):

  • 11 test cases covering happy path, edge cases, and security scenarios
  • Proper mocking of OIDC endpoints and JWT verification
  • Tests for multiple issuers, malformed tokens, expired tokens, wrong signatures
  • Coverage for emailVerified: false handling

Integration Tests (test/profiles.spec.ts):

  • 6 new tests for createProfile with authToken
  • Tests both success and failure paths
  • Validates Neo4j ContactMethod creation and relationships
  • Non-fatal error handling verification

πŸ” Specific Code Observations

Positive Highlights:

  1. Security-first approach - JWT verification happens before any database operations
  2. Fail-safe design - profile creation never fails due to email verification issues
  3. Clean error boundaries - verification errors are logged but don't propagate
  4. Performance conscious - JWKS caching reduces repeated network calls
  5. Backwards compatible - existing clients without authToken work unchanged

Minor Suggestions (non-blocking):

  1. Environment validation: Consider validating OIDC_TRUSTED_ISSUERS format on startup
  2. Metrics/observability: Could add metrics for verification success/failure rates
  3. Documentation: The env vars (OIDC_TRUSTED_ISSUERS, OIDC_EXPECTED_AUDIENCE) need deployment runbook updates

πŸ—οΈ Change Impact Assessment

  • Breaking Changes: None - fully backward compatible
  • Risk Level: Low-Medium (server-side verification is safer than client-side)
  • Rollback Plan: Environment variables can disable feature instantly
  • Performance Impact: Minimal - one additional HTTP request for OIDC discovery (cached)

πŸ“Š Auto-Approval Analysis

Cannot auto-approve due to:

  • Changes in services/ (high-risk backend code)
  • Authentication/security-related functionality
  • New dependencies (jose library)
  • Changes to core user onboarding flow

This is not a reflection of code quality - the implementation is excellent and secure.

🎯 Final Verdict

βœ… APPROVE - This is a well-implemented, secure, and valuable feature that streamlines user onboarding while maintaining strong security practices. The code quality is high, test coverage is comprehensive, and the architecture is sound.

The feature eliminates the email verification round-trip during signup, significantly improving UX while using industry-standard OIDC verification methods.


Copy link
Copy Markdown
Contributor

@gitstream-cm gitstream-cm Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✨ PR Review

LGTM

Generated by LinearB AI and added by gitStream.
AI-generated content may contain inaccuracies. Please verify before using.
πŸ’‘ Tip: You can customize your AI Review using Guidelines Learn how

Copy link
Copy Markdown
Collaborator

@gerardopar gerardopar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! πŸ‘πŸ½

@goblincore goblincore merged commit 8b5325d into main Mar 31, 2026
33 checks passed
@goblincore goblincore deleted the lc-1663-streamlined-jwt branch March 31, 2026 20:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants