Skip to content

Connections & SSO — Technical Reference

This document describes every external account WorkWingman links to, the protocol and client class behind each, the OAuth/redirect details a developer must configure, and where credentials live.

Honesty note. WorkWingman's OAuth flows are not yet operational. The Google token provider throws by design (UnconfiguredGoogleTokenProvider), and the Apple/LinkedIn clients implement only the server-side token legs — the interactive authorization leg (the browser/loopback redirect that produces the code) is not wired up. The KeePass vault is currently an in-memory seed, not a real .kdbx. Each provider below carries a Setup status note stating exactly what exists and what is planned wiring. Do not treat any of these as working SSO today.

Secrets boundary

  • The HTTP API never returns secrets. IVaultService.ListEntriesAsync returns VaultEntryInfo (name, category, username, URL, modified timestamp) — metadata only.
  • The only path to a raw secret is IVaultService.ResolveSecretAsync(entryName), called server-side/in-process by automation code that needs to log in. It is never exposed as an API response shape.
  • Everything runs on the user's machine (see Connection XML doc: "Everything runs on this machine."). Refresh tokens and logins are stored in the local KeePass vault (%USERPROFILE%\Wingman\vault.kdbx).

Connection matrix

Provider Protocol / Class Scopes / Access Credential location Setup status
LinkedIn Sign In with LinkedIn v2 (OIDC) — LinkedInAuthClient; plus Playwright scraper for jobs openid, profile, email (identity). Jobs via scraping, no API scope Vault entry sso-linkedin; refresh/session material in vault Stub — server token/userinfo legs exist; interactive auth leg not wired
Google Google OAuth 2.0 (desktop loopback) — IGoogleTokenProvider / GoogleScopes spreadsheets, forms.body, forms.responses.readonly, calendar.events, gmail.readonly, drive.file Vault entry sso-google; refresh tokens in vault StubUnconfiguredGoogleTokenProvider throws on every call
Apple Sign in with Apple REST — AppleAuthClient Identity (id_token) + Apple Calendar (CalDAV downstream); optional Hide My Email Vault entry sso-apple; refresh tokens in vault Stub — token exchange/refresh exist; ES256 client-secret JWT builder + interactive auth leg not wired
KeePass vault Local .kdbx unlock — KeePassVaultService Master key unlock; stores all secrets The vault is the credential store In-memory seed — real .kdbx via KeePassLib is roadmap
Claude account App account (drafting/study), quota shown in UI N/A (account link) N/A Not wired — surfaced as a Connection tile only
Learning platforms Username/password logins (Udemy, Google Cloud Skills Boost, AWS Skill Builder, Microsoft Learn) Site logins Vault entries edu-* Seeded metadata — automation not wired
GitHub + gitfront Public portfolio links Read-only public URLs N/A (public links) Link-only
Workday ATS tenants Per-tenant login; Playwright automation (no public apply API) Site login per tenant Vault entries workday-* (+ ats-default) Seeded metadata — entry auto-create exists; apply automation not wired
WhisperFlow AI Pro Optional dictation app (recommended, not required) N/A Optional; user's own login Not modeled in code — external tooling only

LinkedIn

Class: src/WorkWingman.Infrastructure/Clients/LinkedInAuthClient.cs Protocol: Sign In with LinkedIn v2 (OpenID Connect).

The identity / jobs split

LinkedIn is used two ways, and they are not the same integration:

  1. Identity (OIDC). LinkedInAuthClient performs the OpenID Connect token exchange and calls the userinfo endpoint. This proves who the user is.
  2. Token URL: https://www.linkedin.com/oauth/v2/accessToken
  3. UserInfo URL: https://api.linkedin.com/v2/userinfo
  4. ExchangeCodeAsync(clientId, clientSecret, code, redirectUri)TokenResponse(Access_Token, Expires_In, Scope)
  5. GetUserInfoAsync(accessToken)UserInfo(Sub, Name, Given_Name, Family_Name, Email, Picture)
  6. Saved-jobs tracker (scraping). There is no public LinkedIn jobs API. Job data comes from a Playwright browser session (LinkedInJobsScraper) driven against linkedin.com/jobs-tracker. The OIDC access token does not grant job data — the two paths are independent.

Scopes

OIDC identity only: openid, profile, email. No API scope requests job data — that is why the scraper exists.

Developer configuration (placeholders)

// appsettings / env — PLACEHOLDERS ONLY, never commit real values
"LinkedIn": {
  "ClientId":     "<LINKEDIN_CLIENT_ID>",
  "ClientSecret": "<LINKEDIN_CLIENT_SECRET>",
  "RedirectUri":  "http://localhost:<PORT>/callback/linkedin"
}

Redirect URI must be registered in the LinkedIn app's "Authorized redirect URLs for your app."

Setup status

Stub. The server-side token exchange and userinfo calls are implemented. The interactive authorization leg (opening the consent page in the Electron shell, capturing the code at the loopback redirect) is not wired. The Playwright jobs scraper is referenced but its session is not driven end-to-end. Planned wiring: Electron-shell auth window → loopback capture → ExchangeCodeAsync → persist refresh material to the sso-linkedin vault entry.

  • Official docs: https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin-v2
  • Playwright: https://playwright.dev · https://github.com/microsoft/playwright

Google

Class / scopes: src/WorkWingman.Infrastructure/Clients/GoogleTokenProvider.cs (IGoogleTokenProvider, GoogleScopes, UnconfiguredGoogleTokenProvider). Consuming clients: GoogleSheetsClient, GoogleFormsClient, GoogleCalendarClient. Protocol: Google OAuth 2.0, desktop loopback (loopback redirect) flow. Per the class doc, the flow "runs in the Electron shell; refresh tokens are stored in the KeePass vault."

Scopes (from GoogleScopes)

Constant Scope Used for
Sheets https://www.googleapis.com/auth/spreadsheets Results sheet (Sheets API)
Forms https://www.googleapis.com/auth/forms.body Intake form body (Forms API)
FormsResponses https://www.googleapis.com/auth/forms.responses.readonly Read intake responses
Calendar https://www.googleapis.com/auth/calendar.events Interview events
GmailRead https://www.googleapis.com/auth/gmail.readonly Detect interview invites
DriveFile https://www.googleapis.com/auth/drive.file App-created Drive folder (form + sheet backup)

gmail.readonly and drive.file are deliberately narrow: read-only Gmail, and Drive access limited to files the app itself creates.

Developer configuration (placeholders)

// PLACEHOLDERS ONLY
"Google": {
  "ClientId":     "<GOOGLE_OAUTH_CLIENT_ID>.apps.googleusercontent.com",
  "ClientSecret": "<GOOGLE_OAUTH_CLIENT_SECRET>",
  "RedirectUri":  "http://127.0.0.1:<PORT>/callback/google"  // loopback
}

Register the loopback redirect in the Google Cloud console OAuth client (type: Desktop app). Enable the Sheets, Forms, Calendar, Gmail, and Drive APIs on the project.

Setup status

Stub. The DI-registered provider is UnconfiguredGoogleTokenProvider, whose GetAccessToken(scope) throws InvalidOperationException with a clear message ("Google OAuth is not configured yet … Connect Google on the Connections screen first."). No real token is ever returned today. Planned wiring: a concrete IGoogleTokenProvider performing the loopback OAuth flow in the Electron shell, caching/refreshing tokens against the sso-google vault entry.

  • Sheets API: https://developers.google.com/workspace/sheets/api
  • Forms API: https://developers.google.com/workspace/forms/api
  • Calendar API: https://developers.google.com/calendar
  • Google APIs .NET client: https://github.com/googleapis/google-api-dotnet-client

Apple

Class: src/WorkWingman.Infrastructure/Clients/AppleAuthClient.cs Protocol: Sign in with Apple REST. Downstream: Apple Calendar via CalDAV; optional Hide My Email for ATS accounts.

What the client does

  • Base: https://appleid.apple.com/auth
  • ExchangeCodeAsync(clientId, clientSecretJwt, code, redirectUri)POST /token (grant authorization_code)
  • RefreshAsync(clientId, clientSecretJwt, refreshToken)POST /token (grant refresh_token)
  • TokenResponse(Access_Token, Refresh_Token?, Id_Token, Expires_In, Token_Type)

The Apple client_secret is a short-lived ES256 JWT, signed with the developer's private key (Key ID + Team ID + Service ID). The client accepts that JWT as clientSecretJwt — it does not build it.

Developer configuration (placeholders)

// PLACEHOLDERS ONLY
"Apple": {
  "ServiceId":   "<APPLE_SERVICE_ID>",          // client_id
  "TeamId":      "<APPLE_TEAM_ID>",
  "KeyId":       "<APPLE_KEY_ID>",
  "PrivateKeyP8":"<PATH_TO_AuthKey_XXXX.p8>",    // ES256 signing key
  "RedirectUri": "https://<YOUR_HOST>/callback/apple"  // Apple requires HTTPS
}

Apple requires an HTTPS return URL; a plain loopback is not accepted. Configure the Service ID's "Return URLs" in the Apple Developer portal.

Setup status

Stub. The server-side token exchange and refresh calls exist and are correct in shape. Not wired: (1) the ES256 client-secret JWT builder that produces clientSecretJwt from the .p8 key, and (2) the interactive authorization leg in the Electron shell that yields the code. CalDAV calendar sync and Hide My Email are downstream and not implemented. Planned wiring: JWT signer → Electron auth window → ExchangeCodeAsync → persist refresh token to the sso-apple vault entry → CalDAV sync.

  • Official docs: https://developer.apple.com/documentation/signinwithapplerestapi
  • Sign in with Apple: https://developer.apple.com/sign-in-with-apple/

KeePass vault

Class: src/WorkWingman.Infrastructure/Vault/KeePassVaultService.cs (IVaultService) Vault path: %USERPROFILE%\Wingman\vault.kdbx (VaultPath).

Surface

  • UnlockAsync() — real impl prompts for master key / key file via the Electron shell and opens the .kdbx. Current impl sets _unlocked = true and seeds entries.
  • ListEntriesAsync() — returns VaultEntryInfo metadata only (name, category, username, URL, modified). No secrets.
  • CreateAtsEntryAsync(tenantHost, userName) — auto-creates a workday-<sub> entry; real impl applies the ats-default password pattern.
  • ResolveSecretAsync(entryName) — the only secret-returning call; in-process automation use only.
  • EnsureUnlocked() — throws if locked ("Vault is locked. Unlock it from the Connections screen first.").

Seeded entries (in-memory)

Categories from VaultEntryCategory: Sso, Ats, LearningPlatform. Seed includes sso-linkedin, sso-google, sso-apple, ats-default, workday-{bah,mastercard,uhg,leidos,ejones,spectrum}, and edu-{udemy,google-skills,aws-skillbuilder,ms-learn}.

Setup status

In-memory seed. No real .kdbx is read or written; secrets are random Guid strings generated at seed time. The class doc states the intent explicitly: production should read/write the kdbx via KeePassLib, or via the keepassxc-proxy protocol if the user runs KeePassXC. The seam is deliberate — the "secrets never cross the HTTP API" rule is real from day one even though storage is in-memory. Planned wiring: replace the ConcurrentDictionary store with a KeePassLib-backed reader/writer over VaultPath, and wire UnlockAsync to a real master-key prompt.

  • KeePassXC: https://keepassxc.org
  • KeePassXC source: https://github.com/keepassxreboot/keepassxc
  • KeePassLib (managed): https://github.com/dlech/KeePassLib2Android

Claude account

Surfaced as a Connection tile (ConnectionKind.Claude): "Reads Workday questions, drafts answers, tailors documents, builds per-job study projects." Quota is shown in the UI (Connection.Badge, e.g. "62% quota").

Setup status

Not wired as an OAuth/SSO integration in the reviewed code — it is a Connection tile. No client class handles Claude auth in the files above.

  • Docs: https://docs.anthropic.com
  • Account/quota help: https://support.claude.com/en/articles/9517075
  • SDK: https://github.com/anthropics/anthropic-sdk-typescript · https://github.com/anthropics/anthropic-sdk-python

Learning platforms

ConnectionKind.LearningPlatforms"Udemy Personal Plan · Google Cloud Skills Boost · AWS Skill Builder · Microsoft Learn — credentials in KeePass." Stored as edu-* vault entries (VaultEntryCategory.LearningPlatform).

Platform Vault entry URL
Udemy Personal Plan edu-udemy https://www.udemy.com
Google Cloud Skills Boost edu-google-skills https://www.skills.google · https://www.cloudskillsboost.google
AWS Skill Builder edu-aws-skillbuilder https://skillbuilder.aws
Microsoft Learn edu-ms-learn https://learn.microsoft.com

Setup status

Seeded metadata only. Entries exist; no login automation is wired.

  • Microsoft Learn: https://learn.microsoft.com · https://github.com/MicrosoftDocs/learn

GitHub + gitfront

ConnectionKind.GitHubPortfolio"Portfolio links on applications." Public portfolio URLs attached to applications.

Setup status

Link-only. No auth; public links.

  • https://github.com · GitHub REST API: https://docs.github.com/rest
  • https://gitfront.io

Workday ATS tenants

Per-tenant Workday accounts, auto-created via CreateAtsEntryAsync(tenantHost, userName), stored as workday-<sub> vault entries (VaultEntryCategory.Ats) with URL https://<tenantHost> (e.g. cvshealth.wd1.myworkdayjobs.com). A shared ats-default entry holds the default password pattern applied to new tenants.

No public apply API. Application submission is automated via Playwright (same tooling as the LinkedIn scraper). AutomationSettings enforces AlwaysRequireReviewBeforeSubmit (locked on) and StartFromBlank (do not trust ATS resume parsing).

The per-tenant account-wall pause. When a tenant has no vault entry yet, the run pauses (RunStatus.AwaitingAccount) instead of guessing — WorkWingman pre-fills the non-secret contact fields and offers a freshly vault-generated password for the user to copy, but never creates the account or authenticates itself. See Per-tenant ATS accounts for the full pause model, vault provisioning (GetOrCreateAtsCredentialAsync), and the KeePassXC hand-off seam for return visits.

Setup status

Seeded metadata + entry auto-create. CreateAtsEntryAsync builds the entry; the real impl (per code comment) should apply the ats-default password. Apply automation is not wired end-to-end. The account-wall pause and per-tenant credential provisioning described above ARE wired and tested.

  • https://myworkdayjobs.com
  • Playwright: https://playwright.dev · https://github.com/microsoft/playwright

WhisperFlow AI Pro

Recommended, not required. Used to dictate the life-story / about-me content that feeds cover letters. Login is optional and personal to the user.

Setup status

Not modeled in code. External tooling; no client class or Connection tile in the reviewed files.

  • https://wisprflow.ai

  • Plain-language: Connections & SSO
  • Data & Backup — reuses this Google connection (and its no-op-when-unconnected behavior) for opt-in Drive/Sheets sync.
  • Source: src/WorkWingman.Infrastructure/Clients/GoogleTokenProvider.cs
  • Source: src/WorkWingman.Infrastructure/Clients/AppleAuthClient.cs
  • Source: src/WorkWingman.Infrastructure/Clients/LinkedInAuthClient.cs
  • Source: src/WorkWingman.Infrastructure/Vault/KeePassVaultService.cs
  • Source: src/WorkWingman.Infrastructure/Services/ConnectionsService.cs
  • Source: src/WorkWingman.Core/Models/Connection.cs