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 thecode) 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.ListEntriesAsyncreturnsVaultEntryInfo(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
ConnectionXML 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 |
|---|---|---|---|---|
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 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 |
Stub — UnconfiguredGoogleTokenProvider 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:
- Identity (OIDC).
LinkedInAuthClientperforms the OpenID Connect token exchange and calls the userinfo endpoint. This proves who the user is. - Token URL:
https://www.linkedin.com/oauth/v2/accessToken - UserInfo URL:
https://api.linkedin.com/v2/userinfo ExchangeCodeAsync(clientId, clientSecret, code, redirectUri)→TokenResponse(Access_Token, Expires_In, Scope)GetUserInfoAsync(accessToken)→UserInfo(Sub, Name, Given_Name, Family_Name, Email, Picture)- Saved-jobs tracker (scraping). There is no public LinkedIn jobs API. Job data comes from a Playwright browser session (
LinkedInJobsScraper) driven againstlinkedin.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(grantauthorization_code)RefreshAsync(clientId, clientSecretJwt, refreshToken)→POST /token(grantrefresh_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 = trueand seeds entries.ListEntriesAsync()— returnsVaultEntryInfometadata only (name, category, username, URL, modified). No secrets.CreateAtsEntryAsync(tenantHost, userName)— auto-creates aworkday-<sub>entry; real impl applies theats-defaultpassword 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
Related docs¶
- 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