Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ node_modules/*
.claude/worktrees
docs/superpowers
.worktrees/
local.mk
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ trivy-scan:
--severity HIGH,CRITICAL --ignore-unfixed --exit-code 1
clean:
rm -rf build
# Local-only dev overrides. Create local.mk (gitignored) and set DEV_FLAGS to
# extra `make dev` flags — real OAuth client IDs/secrets, SMTP creds, etc.
# Example local.mk:
# DEV_FLAGS = --github-client-id=xxx --github-client-secret=yyy
-include local.mk

dev:
@PRIVATE_KEY=$$(printf '%s\n' \
"-----BEGIN RSA PRIVATE KEY-----" \
Expand Down Expand Up @@ -88,7 +94,8 @@ dev:
--admin-secret=admin \
--client-id=kbyuFDidLLm280LIwVFiazOqjO3ty8KH \
--client-secret=60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa \
--allowed-origins=localhost:8080,localhost:8090,localhost:9091,localhost:5173,localhost:5174
--allowed-origins=localhost:8080,localhost:8090,localhost:9091,localhost:5173,localhost:5174 \
$(DEV_FLAGS)

test:
go clean --testcache && TEST_DBS="sqlite" $(GO_TEST_ALL)
Expand Down
13 changes: 8 additions & 5 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,14 @@ var (
defaultFacebookScopes = []string{"public_profile", "email"}
defaultMicrosoftScopes = []string{"openid", "profile", "email"}
defaultTwitchScopes = []string{"openid", "user:read:email"}
defaultLinkedinScopes = []string{"r_liteprofile", "r_emailaddress"}
defaultAppleScopes = []string{"email", "name"}
defaultDiscordScopes = []string{"identify", "email"}
defaultTwitterScopes = []string{"tweet.read", "users.read"}
defaultRobloxScopes = []string{"openid", "profile"}
// LinkedIn's current product is "Sign In with LinkedIn using OpenID
// Connect"; the legacy r_liteprofile/r_emailaddress scopes are not
// provisioned for apps onboarded to it.
defaultLinkedinScopes = []string{"openid", "profile", "email"}
defaultAppleScopes = []string{"email", "name"}
defaultDiscordScopes = []string{"identify", "email"}
defaultTwitterScopes = []string{"tweet.read", "users.read"}
defaultRobloxScopes = []string{"openid", "profile"}
// Default RPS cap per IP; raised from 10 to reduce false positives on busy UIs.
defaultRateLimitRPS = 30
defaultRateLimitBurst = 20
Expand Down
15 changes: 7 additions & 8 deletions e2e-playground/mocks/mock-oauth/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,15 @@ function defaultProfile(provider: string): Record<string, unknown> {
const email = `mock-user@${provider}.example.com`;
switch (provider) {
case 'github':
return { name: 'Mock User', email, avatar_url: 'https://example.com/avatar.png' };
// Mixed types on purpose: GitHub's real GET /user carries a numeric id
// and boolean flags alongside the strings.
return { id: 583231, login: 'mockuser', name: 'Mock User', email, avatar_url: 'https://example.com/avatar.png', public_repos: 8, site_admin: false, company: null };
case 'facebook':
return { first_name: 'Mock', last_name: 'User', email, picture: { data: { url: 'https://example.com/avatar.png' } } };
case 'linkedin':
return { localizedFirstName: 'Mock', localizedLastName: 'User' };
// OIDC userinfo shape (api.linkedin.com/v2/userinfo), which replaced the
// legacy /v2/me + /v2/emailAddress pair.
return { sub: 'mock-linkedin-sub', name: 'Mock User', given_name: 'Mock', family_name: 'User', picture: 'https://example.com/a.png', email, email_verified: true };
case 'discord':
// Flat shape matching Discord's real GET /users/@me response
// (processDiscordUserInfo, internal/http_handlers/oauth_callback.go,
Expand Down Expand Up @@ -153,12 +157,7 @@ app.get(['/:provider/userinfo', '/:provider/user', '/:provider/@me', '/:provider

app.get('/:provider/user/emails', (req, res) => {
const profile = (profiles[req.params.provider] || defaultProfile(req.params.provider)) as { email?: string };
res.json([{ email: profile.email || 'mock-user@github.example.com', primary: true }]);
});

app.get('/:provider/emailAddress', (req, res) => {
const profile = (profiles[req.params.provider] || defaultProfile(req.params.provider)) as { email?: string };
res.json({ elements: [{ 'handle~': { emailAddress: profile.email || 'mock-user@linkedin.example.com' } }] });
res.json([{ email: profile.email || 'mock-user@github.example.com', primary: true, verified: true }]);
});

if (require.main === module) {
Expand Down
13 changes: 10 additions & 3 deletions internal/constants/oauth_info_urls.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,16 @@ const (
// Get github user emails when user info email is empty Ref: https://stackoverflow.com/a/35387123
GithubUserEmails = "https://api.github.com/user/emails"

// Ref: https://docs.microsoft.com/en-us/linkedin/shared/integrations/people/profile-api
LinkedInUserInfoURL = "https://api.linkedin.com/v2/me?projection=(id,localizedFirstName,localizedLastName,emailAddress,profilePicture(displayImage~:playableStreams))"
LinkedInEmailURL = "https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))"
// LinkedInUserInfoURL is the OpenID Connect userinfo endpoint published in
// LinkedIn's own discovery document
// (https://www.linkedin.com/oauth/.well-known/openid-configuration). It
// returns sub/name/given_name/family_name/picture/locale/email/
// email_verified in one call, replacing the legacy /v2/me +
// /v2/emailAddress pair, whose r_liteprofile/r_emailaddress scopes are not
// provisioned for apps onboarded via "Sign In with LinkedIn using OpenID
// Connect".
// Ref: https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin-v2
LinkedInUserInfoURL = "https://api.linkedin.com/v2/userinfo"

// TwitterUserInfoURL requests confirmed_email as a sparse-fieldset field
// alongside the always-present id/name/profile_image_url/username. Per
Expand Down
Loading
Loading