[{"data":1,"prerenderedAt":1602},["ShallowReactive",2],{"navigation_docs_en":3,"-en-security-social-login":347,"-en-security-social-login-surround":1597},[4,45,61,96,132,166,196,222,246,270],{"title":5,"icon":6,"path":7,"stem":8,"children":9,"page":44},"Getting Started","i-lucide-rocket","\u002Fen\u002Fgetting-started","en\u002F1.getting-started",[10,15,20,24,29,34,39],{"title":11,"path":12,"stem":13,"icon":14},"Introduction","\u002Fen\u002Fgetting-started\u002Fintroduction","en\u002F1.getting-started\u002F1.introduction","i-lucide-house",{"title":16,"path":17,"stem":18,"icon":19},"Coming from Laravel","\u002Fen\u002Fgetting-started\u002Fcoming-from-laravel","en\u002F1.getting-started\u002F2.coming-from-laravel","i-lucide-arrow-right-left",{"title":21,"path":22,"stem":23,"icon":19},"Coming from Symfony","\u002Fen\u002Fgetting-started\u002Fcoming-from-symfony","en\u002F1.getting-started\u002F3.coming-from-symfony",{"title":25,"path":26,"stem":27,"icon":28},"Installation","\u002Fen\u002Fgetting-started\u002Finstallation","en\u002F1.getting-started\u002F4.installation","i-lucide-download",{"title":30,"path":31,"stem":32,"icon":33},"Your first module","\u002Fen\u002Fgetting-started\u002Ffirst-module","en\u002F1.getting-started\u002F5.first-module","i-lucide-package-plus",{"title":35,"path":36,"stem":37,"icon":38},"Project structure","\u002Fen\u002Fgetting-started\u002Fproject-structure","en\u002F1.getting-started\u002F6.project-structure","i-lucide-folder-tree",{"title":40,"path":41,"stem":42,"icon":43},"The shop sample application","\u002Fen\u002Fgetting-started\u002Fsample-app","en\u002F1.getting-started\u002F7.sample-app","i-lucide-shopping-cart",false,{"title":46,"icon":47,"path":48,"stem":49,"children":50,"page":44},"Reference","i-lucide-book-open","\u002Fen\u002Freference","en\u002F10.reference",[51,56],{"title":52,"path":53,"stem":54,"icon":55},"Configuration","\u002Fen\u002Freference\u002Fconfiguration","en\u002F10.reference\u002F1.configuration","i-lucide-settings",{"title":57,"path":58,"stem":59,"icon":60},"External dependencies","\u002Fen\u002Freference\u002Fdependencies","en\u002F10.reference\u002F2.dependencies","i-lucide-package",{"title":62,"icon":63,"path":64,"stem":65,"children":66,"page":44},"Concepts","i-lucide-lightbulb","\u002Fen\u002Fconcepts","en\u002F2.concepts",[67,72,77,82,86,91],{"title":68,"path":69,"stem":70,"icon":71},"Architecture","\u002Fen\u002Fconcepts\u002Farchitecture","en\u002F2.concepts\u002F1.architecture","i-lucide-layers",{"title":73,"path":74,"stem":75,"icon":76},"Application lifecycle","\u002Fen\u002Fconcepts\u002Fapplication-lifecycle","en\u002F2.concepts\u002F2.application-lifecycle","i-lucide-power",{"title":78,"path":79,"stem":80,"icon":81},"Error model","\u002Fen\u002Fconcepts\u002Ferror-model","en\u002F2.concepts\u002F3.error-model","i-lucide-shield-alert",{"title":52,"path":83,"stem":84,"icon":85},"\u002Fen\u002Fconcepts\u002Fconfiguration","en\u002F2.concepts\u002F4.configuration","i-lucide-settings-2",{"title":87,"path":88,"stem":89,"icon":90},"Codegen pipeline","\u002Fen\u002Fconcepts\u002Fcodegen-pipeline","en\u002F2.concepts\u002F5.codegen-pipeline","i-lucide-file-json",{"title":92,"path":93,"stem":94,"icon":95},"Design patterns","\u002Fen\u002Fconcepts\u002Fdesign-patterns","en\u002F2.concepts\u002F6.design-patterns","i-lucide-puzzle",{"title":97,"icon":98,"path":99,"stem":100,"children":101,"page":44},"HTTP & Routing","i-lucide-globe","\u002Fen\u002Fhttp","en\u002F3.http",[102,107,112,117,122,127],{"title":103,"path":104,"stem":105,"icon":106},"The server core","\u002Fen\u002Fhttp\u002Fserver","en\u002F3.http\u002F1.server","i-lucide-server",{"title":108,"path":109,"stem":110,"icon":111},"The Fiber engine","\u002Fen\u002Fhttp\u002Ffiber","en\u002F3.http\u002F2.fiber","i-lucide-zap",{"title":113,"path":114,"stem":115,"icon":116},"The chi engine","\u002Fen\u002Fhttp\u002Fchi","en\u002F3.http\u002F3.chi","i-lucide-route",{"title":118,"path":119,"stem":120,"icon":121},"Error responses","\u002Fen\u002Fhttp\u002Ferror-responses","en\u002F3.http\u002F4.error-responses","i-lucide-octagon-alert",{"title":123,"path":124,"stem":125,"icon":126},"Validation","\u002Fen\u002Fhttp\u002Fvalidation","en\u002F3.http\u002F5.validation","i-lucide-badge-check",{"title":128,"path":129,"stem":130,"icon":131},"Pagination","\u002Fen\u002Fhttp\u002Fpagination","en\u002F3.http\u002F6.pagination","i-lucide-list-ordered",{"title":133,"icon":134,"path":135,"stem":136,"children":137,"page":44},"Database","i-lucide-database","\u002Fen\u002Fdatabase","en\u002F4.database",[138,142,147,151,156,161],{"title":139,"path":140,"stem":141,"icon":134},"Overview","\u002Fen\u002Fdatabase\u002Foverview","en\u002F4.database\u002F1.overview",{"title":143,"path":144,"stem":145,"icon":146},"pgx","\u002Fen\u002Fdatabase\u002Fpgx","en\u002F4.database\u002F2.pgx","i-lucide-plug",{"title":148,"path":149,"stem":150,"icon":71},"bun","\u002Fen\u002Fdatabase\u002Fbun","en\u002F4.database\u002F3.bun",{"title":152,"path":153,"stem":154,"icon":155},"Transactions","\u002Fen\u002Fdatabase\u002Ftransactions","en\u002F4.database\u002F4.transactions","i-lucide-git-merge",{"title":157,"path":158,"stem":159,"icon":160},"Migrations","\u002Fen\u002Fdatabase\u002Fmigrations","en\u002F4.database\u002F5.migrations","i-lucide-file-stack",{"title":162,"path":163,"stem":164,"icon":165},"Seeders","\u002Fen\u002Fdatabase\u002Fseeding","en\u002F4.database\u002F6.seeding","i-lucide-sprout",{"title":167,"icon":168,"path":169,"stem":170,"children":171,"page":44},"Async & Background","i-lucide-workflow","\u002Fen\u002Fasync","en\u002F5.async",[172,176,181,186,191],{"title":173,"path":174,"stem":175,"icon":131},"Queue","\u002Fen\u002Fasync\u002Fqueue","en\u002F5.async\u002F1.queue",{"title":177,"path":178,"stem":179,"icon":180},"Events","\u002Fen\u002Fasync\u002Fevents","en\u002F5.async\u002F2.events","i-lucide-radio",{"title":182,"path":183,"stem":184,"icon":185},"Outbox","\u002Fen\u002Fasync\u002Foutbox","en\u002F5.async\u002F3.outbox","i-lucide-inbox",{"title":187,"path":188,"stem":189,"icon":190},"Scheduling","\u002Fen\u002Fasync\u002Fscheduler","en\u002F5.async\u002F4.scheduler","i-lucide-calendar-clock",{"title":192,"path":193,"stem":194,"icon":195},"Worker","\u002Fen\u002Fasync\u002Fworker","en\u002F5.async\u002F5.worker","i-lucide-cog",{"title":197,"icon":198,"path":199,"stem":200,"children":201,"page":44},"Security","i-lucide-shield-check","\u002Fen\u002Fsecurity","en\u002F6.security",[202,207,212,217],{"title":203,"path":204,"stem":205,"icon":206},"Authentication","\u002Fen\u002Fsecurity\u002Fauthentication","en\u002F6.security\u002F1.authentication","i-lucide-key-round",{"title":208,"path":209,"stem":210,"icon":211},"RBAC","\u002Fen\u002Fsecurity\u002Frbac","en\u002F6.security\u002F2.rbac","i-lucide-users",{"title":213,"path":214,"stem":215,"icon":216},"Policies","\u002Fen\u002Fsecurity\u002Fpolicies","en\u002F6.security\u002F3.policies","i-lucide-gavel",{"title":218,"path":219,"stem":220,"icon":221},"Social login","\u002Fen\u002Fsecurity\u002Fsocial-login","en\u002F6.security\u002F4.social-login","i-lucide-log-in",{"title":223,"icon":224,"path":225,"stem":226,"children":227,"page":44},"Observability","i-lucide-activity","\u002Fen\u002Fobservability","en\u002F7.observability",[228,231,236,241],{"title":139,"path":229,"stem":230,"icon":224},"\u002Fen\u002Fobservability\u002Foverview","en\u002F7.observability\u002F1.overview",{"title":232,"path":233,"stem":234,"icon":235},"Logging","\u002Fen\u002Fobservability\u002Flogging","en\u002F7.observability\u002F2.logging","i-lucide-scroll-text",{"title":237,"path":238,"stem":239,"icon":240},"OpenTelemetry","\u002Fen\u002Fobservability\u002Fopentelemetry","en\u002F7.observability\u002F3.opentelemetry","i-lucide-radar",{"title":242,"path":243,"stem":244,"icon":245},"Sentry","\u002Fen\u002Fobservability\u002Fsentry","en\u002F7.observability\u002F4.sentry","i-lucide-bug",{"title":247,"icon":248,"path":249,"stem":250,"children":251,"page":44},"Storage & Mail","i-lucide-hard-drive","\u002Fen\u002Fstorage-mail","en\u002F8.storage-mail",[252,256,261,266],{"title":253,"path":254,"stem":255,"icon":248},"Storage","\u002Fen\u002Fstorage-mail\u002Fstorage","en\u002F8.storage-mail\u002F1.storage",{"title":257,"path":258,"stem":259,"icon":260},"Email","\u002Fen\u002Fstorage-mail\u002Fmail","en\u002F8.storage-mail\u002F2.mail","i-lucide-mail",{"title":262,"path":263,"stem":264,"icon":265},"Notifications","\u002Fen\u002Fstorage-mail\u002Fnotifications","en\u002F8.storage-mail\u002F3.notifications","i-lucide-bell",{"title":267,"path":268,"stem":269,"icon":180},"Realtime","\u002Fen\u002Fstorage-mail\u002Frealtime","en\u002F8.storage-mail\u002F4.realtime",{"title":271,"icon":272,"path":273,"stem":274,"children":275,"page":44},"CLI Reference","i-lucide-terminal","\u002Fen\u002Fcli","en\u002F9.cli",[276,279,284,289,294,299,304,308,313,318,323,328,333,338,342],{"title":139,"path":277,"stem":278,"icon":272},"\u002Fen\u002Fcli\u002Foverview","en\u002F9.cli\u002F1.overview",{"title":280,"path":281,"stem":282,"icon":283},"add db","\u002Fen\u002Fcli\u002Fadd-db","en\u002F9.cli\u002F10.add-db","i-lucide-database-zap",{"title":285,"path":286,"stem":287,"icon":288},"add compose \u002F add docker","\u002Fen\u002Fcli\u002Fadd-compose","en\u002F9.cli\u002F11.add-compose","i-lucide-container",{"title":290,"path":291,"stem":292,"icon":293},"add mail","\u002Fen\u002Fcli\u002Fadd-mail","en\u002F9.cli\u002F12.add-mail","i-lucide-mail-plus",{"title":295,"path":296,"stem":297,"icon":298},"add notification","\u002Fen\u002Fcli\u002Fadd-notification","en\u002F9.cli\u002F13.add-notification","i-lucide-bell-plus",{"title":300,"path":301,"stem":302,"icon":303},"add realtime","\u002Fen\u002Fcli\u002Fadd-realtime","en\u002F9.cli\u002F14.add-realtime","i-lucide-radio-tower",{"title":305,"path":306,"stem":307,"icon":165},"add seeder","\u002Fen\u002Fcli\u002Fadd-seeder","en\u002F9.cli\u002F15.add-seeder",{"title":309,"path":310,"stem":311,"icon":312},"new project","\u002Fen\u002Fcli\u002Fnew-project","en\u002F9.cli\u002F2.new-project","i-lucide-folder-plus",{"title":314,"path":315,"stem":316,"icon":317},"new module","\u002Fen\u002Fcli\u002Fnew-module","en\u002F9.cli\u002F3.new-module","i-lucide-blocks",{"title":319,"path":320,"stem":321,"icon":322},"add surface","\u002Fen\u002Fcli\u002Fadd-surface","en\u002F9.cli\u002F4.add-surface","i-lucide-layers-2",{"title":324,"path":325,"stem":326,"icon":327},"add core","\u002Fen\u002Fcli\u002Fadd-core","en\u002F9.cli\u002F5.add-core","i-lucide-box",{"title":329,"path":330,"stem":331,"icon":332},"add handler","\u002Fen\u002Fcli\u002Fadd-handler","en\u002F9.cli\u002F6.add-handler","i-lucide-webhook",{"title":334,"path":335,"stem":336,"icon":337},"new migration","\u002Fen\u002Fcli\u002Fnew-migration","en\u002F9.cli\u002F7.new-migration","i-lucide-file-plus",{"title":339,"path":340,"stem":341,"icon":195},"worker generators","\u002Fen\u002Fcli\u002Fworker-generators","en\u002F9.cli\u002F8.worker-generators",{"title":343,"path":344,"stem":345,"icon":346},"upgrade templates","\u002Fen\u002Fcli\u002Fupgrade-templates","en\u002F9.cli\u002F9.upgrade-templates","i-lucide-refresh-cw",{"id":348,"title":218,"body":349,"description":1590,"extension":1591,"links":1592,"meta":1593,"navigation":1594,"path":219,"seo":1595,"stem":220,"__hash__":1596},"docs_en\u002Fen\u002F6.security\u002F4.social-login.md",{"type":350,"value":351,"toc":1577},"minimark",[352,390,442,464,469,505,519,523,626,760,765,829,833,933,937,947,1049,1062,1096,1099,1172,1176,1190,1202,1398,1443,1483,1487,1537,1541,1573],[353,354,359],"pre",{"className":355,"code":356,"language":357,"meta":358,"style":358},"language-sh shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","go tool gpsystem add auth --social discord,facebook,apple,google\n","sh","",[360,361,362],"code",{"__ignoreMap":358},[363,364,367,371,375,378,381,384,387],"span",{"class":365,"line":366},"line",1,[363,368,370],{"class":369},"sBMFI","go",[363,372,374],{"class":373},"sfazB"," tool",[363,376,377],{"class":373}," gpsystem",[363,379,380],{"class":373}," add",[363,382,383],{"class":373}," auth",[363,385,386],{"class":373}," --social",[363,388,389],{"class":373}," discord,facebook,apple,google\n",[391,392,393,396,397,401,402,406,407,413,414,417,418,421,422,425,426,429,430,433,434,437,438,441],"p",{},[360,394,395],{},"add auth"," (see the ",[398,399,400],"a",{"href":277},"CLI overview",") always generates a ",[403,404,405],"strong",{},"provider-agnostic social login layer"," in the module: a ",[398,408,412],{"href":409,"rel":410},"https:\u002F\u002Fgithub.com\u002Fmarkbates\u002Fgoth",[411],"nofollow","markbates\u002Fgoth","-based provider registry, two endpoints (",[360,415,416],{},"GET \u002Fsocial\u002F{provider}"," and ",[360,419,420],{},"GET","\u002F",[360,423,424],{},"POST \u002Fsocial\u002F{provider}\u002Fcallback","), and a ",[360,427,428],{},"LoginWithProvider"," service method that issues tokens the same way the email+password login does. The ",[360,431,432],{},"--social"," flag only decides which of the four known providers (Discord, Facebook, Apple, Google) get pre-wired; any other goth provider (or a hand-written ",[360,435,436],{},"goth.Provider",") is a one-line manual addition in the generated ",[360,439,440],{},"social\u002Fproviders.go",".",[443,444,445,448,449,452,453,455,456,459,460,463],"note",{},[403,446,447],{},"goth is a dependency of the generated project",", not the kit's: the kit's ",[360,450,451],{},"auth\u002F"," package is deliberately stateless (JWT sign\u002Fparse + middleware only), and the code ",[360,454,432],{}," turns on lands in the project's own ",[360,457,458],{},"go.mod"," (pulled in by ",[360,461,462],{},"go mod tidy","). This follows from the same research that shaped the kit's shape: the kit is one module, and an OAuth library has no place in it when the generated auth module already lives in the project anyway (bcrypt, refresh rotation, email flows, all of it).",[465,466,468],"h2",{"id":467},"why-this-shape","Why this shape",[391,470,471,472,421,475,478,479,482,483,486,487,490,491,494,495,421,498,501,502,504],{},"The ",[360,473,474],{},"users",[360,476,477],{},"auth_credentials"," schema was designed for multiple providers from day one: the ",[360,480,481],{},"auth_credentials.provider"," + ",[360,484,485],{},"provider_key"," pair (",[360,488,489],{},"UNIQUE (user_id, provider)",") stores a ",[360,492,493],{},"local"," row (bcrypt hash) for email+password registration; a social login writes a ",[360,496,497],{},"discord",[360,499,500],{},"google","\u002F... row there with the provider's stable external user id. ",[360,503,428],{}," builds directly on this schema: there's no separate \"social user\" table, one user can log in through several providers.",[391,506,507,508,511,512,515,516,518],{},"Because the generated strict-server layer (TypeSpec → OpenAPI → oapi-codegen) gives handler methods the signature ",[360,509,510],{},"func(ctx context.Context, req ...) (Resp, error)",", there's no direct access to the underlying Fiber\u002Fchi request, so no convenient way to set a cookie between the begin and callback requests either. That's why the state parameter is a ",[403,513,514],{},"self-verifying, signed token"," (see below) rather than a server-side session: the whole flow stays stateless, in the same spirit as the kit's own ",[360,517,451],{}," package.",[465,520,522],{"id":521},"the-login-flow","The login flow",[353,524,528],{"className":525,"code":526,"language":527,"meta":358,"style":358},"language-mermaid shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","sequenceDiagram\n    participant B as Browser\n    participant A as API (generated auth module)\n    participant P as Provider (e.g. Discord)\n\n    B->>A: GET \u002Fapi\u002Fv1\u002Fauth\u002Fsocial\u002Fdiscord\n    A->>A: build signed state (HMAC, JWT_SECRET)\n    A-->>B: 302 Location: provider consent URL (state=...)\n    B->>P: consent screen\n    P-->>B: redirect \u002Fcallback?code=...&state=...\n    B->>A: GET \u002Fapi\u002Fv1\u002Fauth\u002Fsocial\u002Fdiscord\u002Fcallback\n    A->>A: verify state, exchange code for access token\n    A->>P: GET \u002Fusers\u002F@me (Bearer access token)\n    P-->>A: profile (email, name, avatar, user id)\n    A->>A: find-or-create\u002Flink + IssueTokens\n    A-->>B: 200 TokenPair (accessToken, refreshToken, user)\n","mermaid",[360,529,530,535,541,547,553,560,566,572,578,584,590,596,602,608,614,620],{"__ignoreMap":358},[363,531,532],{"class":365,"line":366},[363,533,534],{},"sequenceDiagram\n",[363,536,538],{"class":365,"line":537},2,[363,539,540],{},"    participant B as Browser\n",[363,542,544],{"class":365,"line":543},3,[363,545,546],{},"    participant A as API (generated auth module)\n",[363,548,550],{"class":365,"line":549},4,[363,551,552],{},"    participant P as Provider (e.g. Discord)\n",[363,554,556],{"class":365,"line":555},5,[363,557,559],{"emptyLinePlaceholder":558},true,"\n",[363,561,563],{"class":365,"line":562},6,[363,564,565],{},"    B->>A: GET \u002Fapi\u002Fv1\u002Fauth\u002Fsocial\u002Fdiscord\n",[363,567,569],{"class":365,"line":568},7,[363,570,571],{},"    A->>A: build signed state (HMAC, JWT_SECRET)\n",[363,573,575],{"class":365,"line":574},8,[363,576,577],{},"    A-->>B: 302 Location: provider consent URL (state=...)\n",[363,579,581],{"class":365,"line":580},9,[363,582,583],{},"    B->>P: consent screen\n",[363,585,587],{"class":365,"line":586},10,[363,588,589],{},"    P-->>B: redirect \u002Fcallback?code=...&state=...\n",[363,591,593],{"class":365,"line":592},11,[363,594,595],{},"    B->>A: GET \u002Fapi\u002Fv1\u002Fauth\u002Fsocial\u002Fdiscord\u002Fcallback\n",[363,597,599],{"class":365,"line":598},12,[363,600,601],{},"    A->>A: verify state, exchange code for access token\n",[363,603,605],{"class":365,"line":604},13,[363,606,607],{},"    A->>P: GET \u002Fusers\u002F@me (Bearer access token)\n",[363,609,611],{"class":365,"line":610},14,[363,612,613],{},"    P-->>A: profile (email, name, avatar, user id)\n",[363,615,617],{"class":365,"line":616},15,[363,618,619],{},"    A->>A: find-or-create\u002Flink + IssueTokens\n",[363,621,623],{"class":365,"line":622},16,[363,624,625],{},"    A-->>B: 200 TokenPair (accessToken, refreshToken, user)\n",[627,628,629,656,662,715,746],"ol",{},[630,631,632,635,636,639,640,643,644,647,648,651,652,655],"li",{},[403,633,634],{},"Begin."," ",[360,637,638],{},"GET \u002Fapi\u002Fv1\u002F{module}\u002Fsocial\u002F{provider}"," builds the provider's consent URL with that provider's goth client, embedding a signed ",[360,641,642],{},"state"," parameter, and ",[360,645,646],{},"302","s the browser there (",[360,649,650],{},"gen.SocialBegin302Response"," + a ",[360,653,654],{},"Location"," header).",[630,657,658,661],{},[403,659,660],{},"Consent."," The user approves access on the provider's own page.",[630,663,664,667,668,671,672],{},[403,665,666],{},"Callback."," The provider redirects back to ",[360,669,670],{},"\u002Fsocial\u002F{provider}\u002Fcallback",":\n",[673,674,675,693],"ul",{},[630,676,677,678,681,682,685,686,688,689,692],{},"most providers (Discord, Google, Facebook) use the ",[403,679,680],{},"query string"," (",[360,683,684],{},"?code=...&state=...","), a ",[360,687,420],{}," with ",[360,690,691],{},"SocialCallbackQuery",";",[630,694,695,696,681,699,702,703,688,706,709,710,714],{},"Apple uses ",[403,697,698],{},"form_post",[360,700,701],{},"application\u002Fx-www-form-urlencoded"," body), a ",[360,704,705],{},"POST",[360,707,708],{},"SocialCallbackForm"," (see ",[398,711,713],{"href":712},"#apple-specifics","Apple specifics",").",[630,716,717,718,720,721,723,724,727,728,681,731,734,735,734,738,734,740,734,743,714],{},"The handler verifies ",[360,719,642],{},", exchanges ",[360,722,360],{}," for an access token, fetches the provider's profile, and normalizes the resulting ",[360,725,726],{},"goth.User"," into a ",[360,729,730],{},"social.ExternalUser",[360,732,733],{},"Provider",", ",[360,736,737],{},"ProviderKey",[360,739,257],{},[360,741,742],{},"Name",[360,744,745],{},"AvatarURL",[630,747,748,749,751,752,755,756,759],{},"The service's ",[360,750,428],{}," runs the find-or-create\u002Flink logic (below), then calls the module's existing ",[360,753,754],{},"core.Service.IssueTokens",": the same JWT\u002Frefresh pair comes out as an email+password login would produce, and the ",[360,757,758],{},"rbac","\u002Fmiddleware layer is untouched.",[761,762,764],"h3",{"id":763},"account-linking-rule","Account-linking rule",[673,766,767,786,792,802,812],{},[630,768,769,681,772,482,775,778,779,781,782,785],{},[403,770,771],{},"Provider identity already known",[360,773,774],{},"provider",[360,776,777],{},"providerKey"," matches an existing ",[360,780,477],{}," row): straight login, ",[360,783,784],{},"IssueTokens"," for the existing user.",[630,787,788,791],{},[403,789,790],{},"First social login for this identity, but the email belongs to an existing, verified user",": the new provider credential gets linked to that user (who can now log in with email+password or with the social provider).",[630,793,794,797,798,801],{},[403,795,796],{},"The email belongs to an existing user whose email is NOT verified",": rejected (",[360,799,800],{},"409 Conflict","). An unverified email is not proof of ownership; auto-linking here would let an attacker who registered a social account with the same email string take over the victim's still-unverified local registration.",[630,803,804,807,808,811],{},[403,805,806],{},"The email is not known at all",": a brand new, ",[403,809,810],{},"pre-verified"," user is created (the provider already vouched for the address, and the kit accepts that).",[630,813,814,681,817,820,821,824,825,828],{},[403,815,816],{},"The provider returns no email",[360,818,819],{},"SocialEmailRequired"," error, ",[360,822,823],{},"422","): the kit requires an email, since ",[360,826,827],{},"users.email"," is the account's key. Every built-in provider registration therefore requests an explicit email scope (below).",[465,830,832],{"id":831},"the-four-built-in-providers","The four built-in providers",[834,835,836,854],"table",{},[837,838,839],"thead",{},[840,841,842,845,848,851],"tr",{},[843,844,733],"th",{},[843,846,847],{},"goth package",[843,849,850],{},"Default scopes",[843,852,853],{},"Callback",[855,856,857,879,895,914],"tbody",{},[840,858,859,863,868,876],{},[860,861,862],"td",{},"Discord",[860,864,865],{},[360,866,867],{},"providers\u002Fdiscord",[860,869,870,734,873],{},[360,871,872],{},"identify",[360,874,875],{},"email",[860,877,878],{},"GET (query string)",[840,880,881,884,889,893],{},[860,882,883],{},"Facebook",[860,885,886],{},[360,887,888],{},"providers\u002Ffacebook",[860,890,891],{},[360,892,875],{},[860,894,878],{},[840,896,897,900,905,912],{},[860,898,899],{},"Google",[860,901,902],{},[360,903,904],{},"providers\u002Fgoogle",[860,906,907,734,909],{},[360,908,875],{},[360,910,911],{},"profile",[860,913,878],{},[840,915,916,919,924,928],{},[860,917,918],{},"Apple",[860,920,921],{},[360,922,923],{},"providers\u002Fapple",[860,925,926],{},[360,927,875],{},[860,929,930],{},[403,931,932],{},"POST (form_post)",[761,934,936],{"id":935},"provider-side-setup-and-env-vars","Provider-side setup and env vars",[391,938,939,940,943,944,714],{},"Every non-Apple provider needs an OAuth2 app registered with it, with the redirect\u002Fcallback URL set to ",[360,941,942],{},"{APP_BASE_URL}\u002Fapi\u002Fv1\u002F{module}\u002Fsocial\u002F{provider}\u002Fcallback"," (e.g. ",[360,945,946],{},"https:\u002F\u002Fapp.example.com\u002Fapi\u002Fv1\u002Fauth\u002Fsocial\u002Fdiscord\u002Fcallback",[834,948,949,961],{},[837,950,951],{},[840,952,953,955,958],{},[843,954,733],{},[843,956,957],{},"Env vars",[843,959,960],{},"Where to get them",[855,962,963,983,1003,1023],{},[840,964,965,967,975],{},[860,966,862],{},[860,968,969,734,972],{},[360,970,971],{},"OAUTH_DISCORD_CLIENT_ID",[360,973,974],{},"OAUTH_DISCORD_CLIENT_SECRET",[860,976,977,982],{},[398,978,981],{"href":979,"rel":980},"https:\u002F\u002Fdiscord.com\u002Fdevelopers\u002Fapplications",[411],"Discord Developer Portal",", OAuth2 tab",[840,984,985,987,995],{},[860,986,883],{},[860,988,989,734,992],{},[360,990,991],{},"OAUTH_FACEBOOK_CLIENT_ID",[360,993,994],{},"OAUTH_FACEBOOK_CLIENT_SECRET",[860,996,997,1002],{},[398,998,1001],{"href":999,"rel":1000},"https:\u002F\u002Fdevelopers.facebook.com\u002F",[411],"Meta for Developers",", Facebook Login product",[840,1004,1005,1007,1015],{},[860,1006,899],{},[860,1008,1009,734,1012],{},[360,1010,1011],{},"OAUTH_GOOGLE_CLIENT_ID",[360,1013,1014],{},"OAUTH_GOOGLE_CLIENT_SECRET",[860,1016,1017,1022],{},[398,1018,1021],{"href":1019,"rel":1020},"https:\u002F\u002Fconsole.cloud.google.com\u002Fapis\u002Fcredentials",[411],"Google Cloud Console",", OAuth 2.0 Client ID",[840,1024,1025,1027,1041],{},[860,1026,918],{},[860,1028,1029,734,1032,734,1035,734,1038],{},[360,1030,1031],{},"OAUTH_APPLE_CLIENT_ID",[360,1033,1034],{},"OAUTH_APPLE_TEAM_ID",[360,1036,1037],{},"OAUTH_APPLE_KEY_ID",[360,1039,1040],{},"OAUTH_APPLE_PRIVATE_KEY",[860,1042,1043,1048],{},[398,1044,1047],{"href":1045,"rel":1046},"https:\u002F\u002Fdeveloper.apple.com\u002Faccount\u002Fresources\u002F",[411],"Apple Developer",", Sign in with Apple + Keys",[391,1050,1051,1052,1055,1056,1058,1059,1061],{},"These are only required (",[360,1053,1054],{},"env:\"...,required\"",") when the matching ",[360,1057,432],{}," flag was passed to ",[360,1060,395],{},": the platform config only gets fields for the providers actually turned on.",[1063,1064,1065,1067,1068,1071,1072,1075,1076,1079,1080,1083,1084,1087,1088,1091,1092,1095],"warning",{},[360,1066,1040],{}," is a PKCS8 PEM private key (the contents of the downloaded ",[360,1069,1070],{},".p8"," file for your Sign in with Apple key). The generated ",[360,1073,1074],{},"providers.go"," mints a ",[403,1077,1078],{},"JWT"," from it, the team id and the key id on every startup (",[360,1081,1082],{},"apple.MakeSecret","): Apple has no static client secret, only a signed token valid for at most 6 months. A malformed key makes ",[360,1085,1086],{},"RegisterProviders"," return an error, which the generated ",[360,1089,1090],{},"Register{{Surface}}"," turns into a startup ",[360,1093,1094],{},"panic"," (deliberately: this is a configuration mistake, not a runtime condition).",[465,1097,713],{"id":1098},"apple-specifics",[673,1100,1101,1141,1159],{},[630,1102,1103,1106,1107,421,1109,1112,1113,1116,1117,1120,1121,421,1123,1125,1126,1128,1129,1131,1132,1134,1135,1137,1138,441],{},[403,1104,1105],{},"form_post callback."," Requesting the ",[360,1108,875],{},[360,1110,1111],{},"name"," scope makes goth's Apple provider switch on ",[360,1114,1115],{},"response_mode=form_post",": Apple ",[403,1118,1119],{},"POSTs"," the ",[360,1122,360],{},[360,1124,642],{}," pair as an ",[360,1127,701],{}," body to the callback URL instead of a query string. That's why there's a separate ",[360,1130,424],{}," operation (",[360,1133,708],{},") alongside the usual ",[360,1136,420],{},"; both call the same ",[360,1139,1140],{},"social.Registry.Complete",[630,1142,1143,1146,1147,1150,1151,1154,1155,1158],{},[403,1144,1145],{},"First consent only."," Apple sends the name only on the ",[403,1148,1149],{},"first"," approval (in a ",[360,1152,1153],{},"user"," field, as JSON); a repeat login only carries ",[360,1156,1157],{},"sub"," (the stable user id) and, if enabled, the email. The generated code falls back to the email as the name when the profile doesn't provide one.",[630,1160,1161,1164,1165,1167,1168,1171],{},[403,1162,1163],{},"Private relay email."," If the user picks \"Hide My Email\", ",[360,1166,257],{}," is an Apple-generated ",[360,1169,1170],{},"@privaterelay.appleid.com"," address, stable for that user but not their real one.",[465,1173,1175],{"id":1174},"adding-a-provider-by-hand","Adding a provider by hand",[391,1177,1178,1179,1182,1183,1185,1186,1189],{},"The generic layer (",[360,1180,1181],{},"internal\u002Fmodules\u002F{module}\u002Fsocial\u002Fsocial.go",") never changes; every extension happens in ",[360,1184,1074],{},", below the ",[360,1187,1188],{},"\u002F\u002F gpsystem:social-providers"," marker.",[391,1191,1192,1195,1196,1201],{},[403,1193,1194],{},"Another goth provider"," (one of the ",[398,1197,1200],{"href":1198,"rel":1199},"https:\u002F\u002Fgithub.com\u002Fmarkbates\u002Fgoth\u002Ftree\u002Fmaster\u002Fproviders",[411],"~60 built in",", e.g. GitLab):",[353,1203,1206],{"className":1204,"code":1205,"language":370,"meta":358,"style":358},"language-go shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\u002F\u002F providers.go\nimport \"github.com\u002Fmarkbates\u002Fgoth\u002Fproviders\u002Fgitlab\"\n\nfunc RegisterProviders(reg *Registry, cfg Config) error {\n    \u002F\u002F gpsystem:social-providers\n    if cfg.GitLab.ClientID != \"\" {\n        reg.Register(\"gitlab\", gitlab.New(cfg.GitLab.ClientID, cfg.GitLab.ClientSecret, callbackURL(cfg, \"gitlab\")))\n    }\n    \u002F\u002F ...\n}\n",[360,1207,1208,1214,1230,1234,1275,1280,1306,1383,1388,1393],{"__ignoreMap":358},[363,1209,1210],{"class":365,"line":366},[363,1211,1213],{"class":1212},"sHwdD","\u002F\u002F providers.go\n",[363,1215,1216,1220,1224,1227],{"class":365,"line":537},[363,1217,1219],{"class":1218},"s7zQu","import",[363,1221,1223],{"class":1222},"sMK4o"," \"",[363,1225,1226],{"class":369},"github.com\u002Fmarkbates\u002Fgoth\u002Fproviders\u002Fgitlab",[363,1228,1229],{"class":1222},"\"\n",[363,1231,1232],{"class":365,"line":543},[363,1233,559],{"emptyLinePlaceholder":558},[363,1235,1236,1239,1243,1246,1250,1253,1256,1259,1262,1265,1268,1272],{"class":365,"line":549},[363,1237,1238],{"class":1222},"func",[363,1240,1242],{"class":1241},"s2Zo4"," RegisterProviders",[363,1244,1245],{"class":1222},"(",[363,1247,1249],{"class":1248},"sHdIc","reg",[363,1251,1252],{"class":1222}," *",[363,1254,1255],{"class":369},"Registry",[363,1257,1258],{"class":1222},",",[363,1260,1261],{"class":1248}," cfg",[363,1263,1264],{"class":369}," Config",[363,1266,1267],{"class":1222},")",[363,1269,1271],{"class":1270},"spNyl"," error",[363,1273,1274],{"class":1222}," {\n",[363,1276,1277],{"class":365,"line":555},[363,1278,1279],{"class":1212},"    \u002F\u002F gpsystem:social-providers\n",[363,1281,1282,1285,1288,1290,1293,1295,1298,1301,1304],{"class":365,"line":562},[363,1283,1284],{"class":1218},"    if",[363,1286,1261],{"class":1287},"sTEyZ",[363,1289,441],{"class":1222},[363,1291,1292],{"class":1287},"GitLab",[363,1294,441],{"class":1222},[363,1296,1297],{"class":1287},"ClientID ",[363,1299,1300],{"class":1222},"!=",[363,1302,1303],{"class":1222}," \"\"",[363,1305,1274],{"class":1222},[363,1307,1308,1311,1313,1316,1318,1321,1324,1326,1328,1331,1333,1336,1338,1341,1343,1345,1347,1350,1352,1354,1356,1358,1360,1363,1365,1368,1370,1372,1374,1376,1378,1380],{"class":365,"line":568},[363,1309,1310],{"class":1287},"        reg",[363,1312,441],{"class":1222},[363,1314,1315],{"class":1241},"Register",[363,1317,1245],{"class":1222},[363,1319,1320],{"class":1222},"\"",[363,1322,1323],{"class":373},"gitlab",[363,1325,1320],{"class":1222},[363,1327,1258],{"class":1222},[363,1329,1330],{"class":1287}," gitlab",[363,1332,441],{"class":1222},[363,1334,1335],{"class":1241},"New",[363,1337,1245],{"class":1222},[363,1339,1340],{"class":1287},"cfg",[363,1342,441],{"class":1222},[363,1344,1292],{"class":1287},[363,1346,441],{"class":1222},[363,1348,1349],{"class":1287},"ClientID",[363,1351,1258],{"class":1222},[363,1353,1261],{"class":1287},[363,1355,441],{"class":1222},[363,1357,1292],{"class":1287},[363,1359,441],{"class":1222},[363,1361,1362],{"class":1287},"ClientSecret",[363,1364,1258],{"class":1222},[363,1366,1367],{"class":1241}," callbackURL",[363,1369,1245],{"class":1222},[363,1371,1340],{"class":1287},[363,1373,1258],{"class":1222},[363,1375,1223],{"class":1222},[363,1377,1323],{"class":373},[363,1379,1320],{"class":1222},[363,1381,1382],{"class":1222},")))\n",[363,1384,1385],{"class":365,"line":574},[363,1386,1387],{"class":1222},"    }\n",[363,1389,1390],{"class":365,"line":580},[363,1391,1392],{"class":1212},"    \u002F\u002F ...\n",[363,1394,1395],{"class":365,"line":586},[363,1396,1397],{"class":1222},"}\n",[391,1399,1400,1401,1404,1405,681,1408,1411,1412,421,1415,1418,1419,1421,1422,1425,1426,681,1429,1432,1433,1436,1437,1439,1440,1442],{},"That needs a ",[360,1402,1403],{},"GitLab ProviderConfig"," field on ",[360,1406,1407],{},"Config",[360,1409,1410],{},"config.go","), an ",[360,1413,1414],{},"OAUTH_GITLAB_CLIENT_ID",[360,1416,1417],{},"_SECRET"," pair on the platform config (",[360,1420,1410],{},", the ",[360,1423,1424],{},"\u002F\u002F gpsystem:config"," anchor), and the matching fields on ",[360,1427,1428],{},"Dependencies",[360,1430,1431],{},"register.go",") and the ",[360,1434,1435],{},"social.Config{...}"," literal in ",[360,1438,1090],{},": exactly the same spots ",[360,1441,432],{}," generates for the built-in four.",[391,1444,1445,1448,1449,681,1455,734,1458,734,1461,734,1464,421,1466,734,1469,734,1472,421,1475,1478,1479,1482],{},[403,1446,1447],{},"A provider with no goth package",": implement ",[398,1450,1453],{"href":1451,"rel":1452},"https:\u002F\u002Fpkg.go.dev\u002Fgithub.com\u002Fmarkbates\u002Fgoth#Provider",[411],[360,1454,436],{},[360,1456,1457],{},"BeginAuth",[360,1459,1460],{},"UnmarshalSession",[360,1462,1463],{},"FetchUser",[360,1465,742],{},[360,1467,1468],{},"SetName",[360,1470,1471],{},"Debug",[360,1473,1474],{},"RefreshToken",[360,1476,1477],{},"RefreshTokenAvailable",") on your own type, then ",[360,1480,1481],{},"reg.Register(\"name\", &MyProvider{...})"," the same way.",[465,1484,1486],{"id":1485},"troubleshooting","Troubleshooting",[673,1488,1489,1504,1518,1528],{},[630,1490,1491,1496,1497,1500,1501,1503],{},[403,1492,1493],{},[360,1494,1495],{},"404 unknown social provider",": the ",[360,1498,1499],{},"{provider}"," path segment isn't registered (neither via ",[360,1502,432],{}," nor by hand), or the name is misspelled.",[630,1505,1506,1511,1512,1514,1515,714],{},[403,1507,1508],{},[360,1509,1510],{},"401 social login failed",": ",[360,1513,642],{}," is invalid (expired, older than 10 minutes, or meant for a different provider), or the code exchange failed at the provider (most often: a redirect URI mismatch between the provider's app settings and the one computed from ",[360,1516,1517],{},"APP_BASE_URL",[630,1519,1520,1527],{},[403,1521,1522,1524,1525],{},[360,1523,823],{}," at the callback, ",[360,1526,819],{},": the provider returned no email; check that the email scope is included (it is by default for all four built-ins).",[630,1529,1530,1534,1535,714],{},[403,1531,1532],{},[360,1533,800],{},": the email belongs to an existing but unverified user; that user needs to verify their own email+password registration first (see ",[398,1536,203],{"href":204},[465,1538,1540],{"id":1539},"related-pages","Related pages",[673,1542,1543,1551,1560],{},[630,1544,1545,1547,1548,1550],{},[398,1546,203],{"href":204},": the kit's ",[360,1549,451],{}," package, JWT issuing, middleware.",[630,1552,1553,1555,1556,1559],{},[398,1554,208],{"href":209},": role\u002Fpermission checks over the ",[360,1557,1558],{},"Identity"," a token turns into, unchanged for social logins.",[630,1561,1562,1564,1565,1568,1569,1572],{},[398,1563,52],{"href":53},": the kit's own ",[360,1566,1567],{},"envPrefix","-composable configs (the ",[360,1570,1571],{},"OAUTH_*"," vars are the generated project's own config, not covered there).",[1574,1575,1576],"style",{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}",{"title":358,"searchDepth":537,"depth":537,"links":1578},[1579,1580,1583,1586,1587,1588,1589],{"id":467,"depth":537,"text":468},{"id":521,"depth":537,"text":522,"children":1581},[1582],{"id":763,"depth":543,"text":764},{"id":831,"depth":537,"text":832,"children":1584},[1585],{"id":935,"depth":543,"text":936},{"id":1098,"depth":537,"text":713},{"id":1174,"depth":537,"text":1175},{"id":1485,"depth":537,"text":1486},{"id":1539,"depth":537,"text":1540},"Wiring Discord, Facebook, Apple, Google, or any other OAuth2 provider into the add auth module.","md",null,{},{"icon":221},{"title":218,"description":1590},"1nL8aSqHy8uVIg83sfkN6_v-RnBpgWDjqJpD8q8B0Jg",[1598,1600],{"title":213,"path":214,"stem":215,"description":1599,"icon":216,"children":-1},"Per-request, per-user authorization above roles: declared in the contract, enforced from generated code.",{"title":139,"path":229,"stem":230,"description":1601,"icon":224,"children":-1},"The telemetry facade: logging, OpenTelemetry and Sentry in a single call, with one shutdown guarantee.",1785011040494]