[{"data":1,"prerenderedAt":3114},["ShallowReactive",2],{"navigation":3,"\u002Fdocs\u002Fpackages\u002Fauth\u002Foauth2":176,"\u002Fdocs\u002Fpackages\u002Fauth\u002Foauth2-surround":3109},[4,21,86,158],{"title":5,"path":6,"stem":7,"children":8,"status":11,"icon":20},"Getting Started","\u002Fdocs\u002Fgetting-started","1.docs\u002F1.getting-started\u002F1.index",[9,12,16],{"title":10,"path":6,"stem":7,"status":11},"Introduction",null,{"title":13,"path":14,"stem":15,"status":11},"Installation","\u002Fdocs\u002Fgetting-started\u002Finstallation","1.docs\u002F1.getting-started\u002F2.installation",{"title":17,"path":18,"stem":19,"status":11},"Quick Start","\u002Fdocs\u002Fgetting-started\u002Fquick-start","1.docs\u002F1.getting-started\u002F3.quick-start",false,{"title":22,"path":23,"stem":24,"children":25,"status":11,"icon":20},"Core Concepts","\u002Fdocs\u002Fcore-concepts","1.docs\u002F2.core-concepts\u002F1.index",[26,28,32,36,46,50,54,58,62,66,70,74,78,82],{"title":27,"path":23,"stem":24,"status":11},"Overview",{"title":29,"path":30,"stem":31,"status":11},"Response","\u002Fdocs\u002Fcore-concepts\u002Fresponse","1.docs\u002F2.core-concepts\u002F10.response",{"title":33,"path":34,"stem":35,"status":11},"Testing","\u002Fdocs\u002Fcore-concepts\u002Ftesting","1.docs\u002F2.core-concepts\u002F12.testing",{"title":37,"path":38,"stem":39,"children":40,"status":11,"icon":20,"defaultOpen":20},"Decorators","\u002Fdocs\u002Fcore-concepts\u002Fdecorators","1.docs\u002F2.core-concepts\u002F13.decorators\u002F1.index",[41,42],{"title":27,"path":38,"stem":39,"status":11},{"title":43,"path":44,"stem":45,"status":11},"Custom","\u002Fdocs\u002Fcore-concepts\u002Fdecorators\u002Fcustom","1.docs\u002F2.core-concepts\u002F13.decorators\u002F2.custom",{"title":47,"path":48,"stem":49,"status":11},"Discovery Service","\u002Fdocs\u002Fcore-concepts\u002Fdiscovery","1.docs\u002F2.core-concepts\u002F14.discovery",{"title":51,"path":52,"stem":53,"status":11},"Application Lifecycle","\u002Fdocs\u002Fcore-concepts\u002Fapp-lifecycle","1.docs\u002F2.core-concepts\u002F15.app-lifecycle",{"title":55,"path":56,"stem":57,"status":11},"Controllers","\u002Fdocs\u002Fcore-concepts\u002Fcontrollers","1.docs\u002F2.core-concepts\u002F2.controllers",{"title":59,"path":60,"stem":61,"status":11},"Routing","\u002Fdocs\u002Fcore-concepts\u002Frouting","1.docs\u002F2.core-concepts\u002F3.routing",{"title":63,"path":64,"stem":65,"status":11},"Providers","\u002Fdocs\u002Fcore-concepts\u002Fproviders","1.docs\u002F2.core-concepts\u002F4.providers",{"title":67,"path":68,"stem":69,"status":11},"Modules","\u002Fdocs\u002Fcore-concepts\u002Fmodules","1.docs\u002F2.core-concepts\u002F5.modules",{"title":71,"path":72,"stem":73,"status":11},"Configuration","\u002Fdocs\u002Fcore-concepts\u002Fconfiguration","1.docs\u002F2.core-concepts\u002F6.configuration",{"title":75,"path":76,"stem":77,"status":11},"Middleware","\u002Fdocs\u002Fcore-concepts\u002Fmiddleware","1.docs\u002F2.core-concepts\u002F7.middleware",{"title":79,"path":80,"stem":81,"status":11},"Guards","\u002Fdocs\u002Fcore-concepts\u002Fguards","1.docs\u002F2.core-concepts\u002F8.guards",{"title":83,"path":84,"stem":85,"status":11},"Exceptions","\u002Fdocs\u002Fcore-concepts\u002Fexceptions","1.docs\u002F2.core-concepts\u002F9.exceptions",{"title":87,"path":88,"stem":89,"children":90,"status":11,"icon":20},"Packages","\u002Fdocs\u002Fpackages","1.docs\u002F3.packages\u002F1.index",[91,92,97,108,112,130,134,138,142,146,150,154],{"title":27,"path":88,"stem":89,"status":11},{"title":93,"path":94,"stem":95,"status":96},"CLI","\u002Fdocs\u002Fpackages\u002Fcli","1.docs\u002F3.packages\u002F10.cli","experimental",{"title":98,"path":99,"stem":100,"children":101,"status":11,"icon":20,"defaultOpen":20},"Events","\u002Fdocs\u002Fpackages\u002Fmessaging","1.docs\u002F3.packages\u002F11.messaging\u002F1.index",[102,104],{"title":27,"path":99,"stem":100,"status":103},"beta",{"title":105,"path":106,"stem":107,"status":96},"Redis","\u002Fdocs\u002Fpackages\u002Fmessaging\u002Fredis","1.docs\u002F3.packages\u002F11.messaging\u002F2.redis",{"title":109,"path":110,"stem":111,"status":103},"Serve Static","\u002Fdocs\u002Fpackages\u002Fserve-static","1.docs\u002F3.packages\u002F12.serve-static",{"title":113,"path":114,"stem":115,"children":116,"status":11,"icon":20,"defaultOpen":20},"Auth","\u002Fdocs\u002Fpackages\u002Fauth","1.docs\u002F3.packages\u002F2.auth\u002F1.index",[117,118,122,126],{"title":27,"path":114,"stem":115,"status":103},{"title":119,"path":120,"stem":121,"status":103},"JWT Provider","\u002Fdocs\u002Fpackages\u002Fauth\u002Fjwt","1.docs\u002F3.packages\u002F2.auth\u002F2.jwt",{"title":123,"path":124,"stem":125,"status":103},"Local Provider","\u002Fdocs\u002Fpackages\u002Fauth\u002Flocal","1.docs\u002F3.packages\u002F2.auth\u002F3.local",{"title":127,"path":128,"stem":129,"status":96},"OAuth2 Provider","\u002Fdocs\u002Fpackages\u002Fauth\u002Foauth2","1.docs\u002F3.packages\u002F2.auth\u002F4.oauth2",{"title":131,"path":132,"stem":133,"status":103},"JWT","\u002Fdocs\u002Fpackages\u002Fjwt","1.docs\u002F3.packages\u002F3.jwt",{"title":135,"path":136,"stem":137,"status":103},"Drizzle","\u002Fdocs\u002Fpackages\u002Fdrizzle","1.docs\u002F3.packages\u002F4.drizzle",{"title":139,"path":140,"stem":141,"status":103},"Papr","\u002Fdocs\u002Fpackages\u002Fpapr","1.docs\u002F3.packages\u002F5.papr",{"title":143,"path":144,"stem":145,"status":103},"Mongoose","\u002Fdocs\u002Fpackages\u002Fmongoose","1.docs\u002F3.packages\u002F6.mongoose",{"title":147,"path":148,"stem":149,"status":103},"Swagger","\u002Fdocs\u002Fpackages\u002Fswagger","1.docs\u002F3.packages\u002F7.swagger",{"title":151,"path":152,"stem":153,"status":103},"Node Server","\u002Fdocs\u002Fpackages\u002Fnode-server","1.docs\u002F3.packages\u002F8.node-server",{"title":155,"path":156,"stem":157,"status":103},"uWS Server","\u002Fdocs\u002Fpackages\u002Fuws-server","1.docs\u002F3.packages\u002F9.uws-server",{"title":159,"path":160,"stem":161,"children":162,"status":11,"icon":20},"Roadmap","\u002Fdocs\u002Froadmap","1.docs\u002F4.roadmap\u002F1.index",[163,164,168,172],{"title":27,"path":160,"stem":161,"status":11},{"title":165,"path":166,"stem":167,"status":11},"Short term (0-3 months)","\u002Fdocs\u002Froadmap\u002Fshort-term","1.docs\u002F4.roadmap\u002F2.short-term",{"title":169,"path":170,"stem":171,"status":11},"Mid term (3-9 months)","\u002Fdocs\u002Froadmap\u002Fmid-term","1.docs\u002F4.roadmap\u002F3.mid-term",{"title":173,"path":174,"stem":175,"status":11},"Long term (9-12+ months)","\u002Fdocs\u002Froadmap\u002Flong-term","1.docs\u002F4.roadmap\u002F4.long-term",{"id":177,"title":127,"body":178,"description":3104,"extension":3105,"meta":3106,"navigation":360,"path":128,"seo":3107,"status":96,"stem":129,"__hash__":3108},"docs\u002F1.docs\u002F3.packages\u002F2.auth\u002F4.oauth2.md",{"type":179,"value":180,"toc":3093},"minimark",[181,186,190,216,223,227,238,1022,1025,1129,1136,1159,1162,1165,1171,2472,2496,2500,2650,2654,3018,3029,3033,3038,3048,3054,3058,3089],[182,183,185],"h2",{"id":184},"problem","Problem",[187,188,189],"p",{},"You want users to sign in with GitHub. Two flavors come up in real apps:",[191,192,193,205],"ol",{},[194,195,196,200,201,204],"li",{},[197,198,199],"strong",{},"Server-side redirect flow"," - your app hosts a \"Sign in with GitHub\" button. User is redirected to GitHub, GitHub redirects back to your callback, you exchange the code for an access token, look up (or create) a local user, and issue ",[197,202,203],{},"your own"," JWT.",[194,206,207,210,211,215],{},[197,208,209],{},"External access-token verification"," - a native mobile or SPA client runs the OAuth flow itself and sends GitHub's access token to your backend in the ",[212,213,214],"code",{},"Authorization"," header. Your backend verifies it against GitHub's API on every request.",[187,217,218,219,222],{},"Both are useful. The first issues a long-lived session via your own JWT (cheap to verify); the second never stores GitHub tokens but pings GitHub on each call. This recipe shows both, structured so the ",[197,220,221],{},"same provider class"," powers both flows.",[182,224,226],{"id":225},"solution","Solution",[187,228,229,230,233,234,237],{},"This is the ",[212,231,232],{},"AuthProvider"," flavor - verifies an ",[212,235,236],{},"Authorization: token \u003Cgithub-access-token>"," header against the GitHub API and upserts the local user.",[239,240,245],"pre",{"className":241,"code":242,"language":243,"meta":244,"style":244},"language-typescript shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\u002F\u002F src\u002Fauth\u002Fproviders\u002Fgithub-auth.provider.ts\nimport { Injectable, inject, UnauthorizedException, type RequestContext } from '@miiajs\u002Fcore'\nimport { fromHeader, type AuthProvider } from '@miiajs\u002Fauth'\nimport { UsersService } from '..\u002F..\u002Fusers\u002Fusers.service.js'\n\ninterface GitHubProfile {\n  id: number\n  login: string\n  name: string | null\n  email: string | null\n  avatar_url: string\n}\n\n@Injectable()\nexport class GitHubAuth implements AuthProvider {\n  private usersService = inject(UsersService)\n  private extract = fromHeader('authorization', 'token')\n\n  async authenticate(ctx: RequestContext) {\n    const accessToken = this.extract(ctx)\n    if (!accessToken) throw new UnauthorizedException('Missing GitHub token')\n\n    const profile = await this.fetchProfile(accessToken)\n    return this.usersService.upsertFromOAuth({\n      provider: 'github',\n      externalId: String(profile.id),\n      email: profile.email,\n      name: profile.name ?? profile.login,\n      avatar: profile.avatar_url,\n    })\n  }\n\n  private async fetchProfile(token: string): Promise\u003CGitHubProfile> {\n    const res = await fetch('https:\u002F\u002Fapi.github.com\u002Fuser', {\n      headers: {\n        authorization: `token ${token}`,\n        'user-agent': 'miia-app',\n        accept: 'application\u002Fvnd.github+json',\n      },\n    })\n    if (!res.ok) throw new UnauthorizedException('Invalid GitHub token')\n    return res.json() as Promise\u003CGitHubProfile>\n  }\n}\n","typescript","",[212,246,247,256,306,334,355,362,376,389,400,417,431,441,447,452,465,484,501,536,541,565,588,625,630,654,676,694,719,736,763,780,788,794,799,835,863,873,897,919,936,942,949,985,1012,1017],{"__ignoreMap":244},[248,249,252],"span",{"class":250,"line":251},"line",1,[248,253,255],{"class":254},"sHwdD","\u002F\u002F src\u002Fauth\u002Fproviders\u002Fgithub-auth.provider.ts\n",[248,257,259,263,267,271,274,277,279,282,284,287,290,293,296,299,303],{"class":250,"line":258},2,[248,260,262],{"class":261},"s7zQu","import",[248,264,266],{"class":265},"sMK4o"," {",[248,268,270],{"class":269},"sTEyZ"," Injectable",[248,272,273],{"class":265},",",[248,275,276],{"class":269}," inject",[248,278,273],{"class":265},[248,280,281],{"class":269}," UnauthorizedException",[248,283,273],{"class":265},[248,285,286],{"class":261}," type",[248,288,289],{"class":269}," RequestContext",[248,291,292],{"class":265}," }",[248,294,295],{"class":261}," from",[248,297,298],{"class":265}," '",[248,300,302],{"class":301},"sfazB","@miiajs\u002Fcore",[248,304,305],{"class":265},"'\n",[248,307,309,311,313,316,318,320,323,325,327,329,332],{"class":250,"line":308},3,[248,310,262],{"class":261},[248,312,266],{"class":265},[248,314,315],{"class":269}," fromHeader",[248,317,273],{"class":265},[248,319,286],{"class":261},[248,321,322],{"class":269}," AuthProvider",[248,324,292],{"class":265},[248,326,295],{"class":261},[248,328,298],{"class":265},[248,330,331],{"class":301},"@miiajs\u002Fauth",[248,333,305],{"class":265},[248,335,337,339,341,344,346,348,350,353],{"class":250,"line":336},4,[248,338,262],{"class":261},[248,340,266],{"class":265},[248,342,343],{"class":269}," UsersService",[248,345,292],{"class":265},[248,347,295],{"class":261},[248,349,298],{"class":265},[248,351,352],{"class":301},"..\u002F..\u002Fusers\u002Fusers.service.js",[248,354,305],{"class":265},[248,356,358],{"class":250,"line":357},5,[248,359,361],{"emptyLinePlaceholder":360},true,"\n",[248,363,365,369,373],{"class":250,"line":364},6,[248,366,368],{"class":367},"spNyl","interface",[248,370,372],{"class":371},"sBMFI"," GitHubProfile",[248,374,375],{"class":265}," {\n",[248,377,379,383,386],{"class":250,"line":378},7,[248,380,382],{"class":381},"swJcz","  id",[248,384,385],{"class":265},":",[248,387,388],{"class":371}," number\n",[248,390,392,395,397],{"class":250,"line":391},8,[248,393,394],{"class":381},"  login",[248,396,385],{"class":265},[248,398,399],{"class":371}," string\n",[248,401,403,406,408,411,414],{"class":250,"line":402},9,[248,404,405],{"class":381},"  name",[248,407,385],{"class":265},[248,409,410],{"class":371}," string",[248,412,413],{"class":265}," |",[248,415,416],{"class":371}," null\n",[248,418,420,423,425,427,429],{"class":250,"line":419},10,[248,421,422],{"class":381},"  email",[248,424,385],{"class":265},[248,426,410],{"class":371},[248,428,413],{"class":265},[248,430,416],{"class":371},[248,432,434,437,439],{"class":250,"line":433},11,[248,435,436],{"class":381},"  avatar_url",[248,438,385],{"class":265},[248,440,399],{"class":371},[248,442,444],{"class":250,"line":443},12,[248,445,446],{"class":265},"}\n",[248,448,450],{"class":250,"line":449},13,[248,451,361],{"emptyLinePlaceholder":360},[248,453,455,458,462],{"class":250,"line":454},14,[248,456,457],{"class":265},"@",[248,459,461],{"class":460},"s2Zo4","Injectable",[248,463,464],{"class":269},"()\n",[248,466,468,471,474,477,480,482],{"class":250,"line":467},15,[248,469,470],{"class":261},"export",[248,472,473],{"class":367}," class",[248,475,476],{"class":371}," GitHubAuth",[248,478,479],{"class":367}," implements",[248,481,322],{"class":371},[248,483,375],{"class":265},[248,485,487,490,493,496,498],{"class":250,"line":486},16,[248,488,489],{"class":367},"  private",[248,491,492],{"class":381}," usersService",[248,494,495],{"class":265}," =",[248,497,276],{"class":381},[248,499,500],{"class":269},"(UsersService)\n",[248,502,504,506,509,511,513,516,519,522,524,526,528,531,533],{"class":250,"line":503},17,[248,505,489],{"class":367},[248,507,508],{"class":381}," extract",[248,510,495],{"class":265},[248,512,315],{"class":381},[248,514,515],{"class":269},"(",[248,517,518],{"class":265},"'",[248,520,521],{"class":301},"authorization",[248,523,518],{"class":265},[248,525,273],{"class":265},[248,527,298],{"class":265},[248,529,530],{"class":301},"token",[248,532,518],{"class":265},[248,534,535],{"class":269},")\n",[248,537,539],{"class":250,"line":538},18,[248,540,361],{"emptyLinePlaceholder":360},[248,542,544,547,550,552,556,558,560,563],{"class":250,"line":543},19,[248,545,546],{"class":367},"  async",[248,548,549],{"class":381}," authenticate",[248,551,515],{"class":265},[248,553,555],{"class":554},"sHdIc","ctx",[248,557,385],{"class":265},[248,559,289],{"class":371},[248,561,562],{"class":265},")",[248,564,375],{"class":265},[248,566,568,571,574,576,579,582,584,586],{"class":250,"line":567},20,[248,569,570],{"class":367},"    const",[248,572,573],{"class":269}," accessToken",[248,575,495],{"class":265},[248,577,578],{"class":265}," this.",[248,580,581],{"class":460},"extract",[248,583,515],{"class":381},[248,585,555],{"class":269},[248,587,535],{"class":381},[248,589,591,594,597,600,603,606,609,612,614,616,618,621,623],{"class":250,"line":590},21,[248,592,593],{"class":261},"    if",[248,595,596],{"class":381}," (",[248,598,599],{"class":265},"!",[248,601,602],{"class":269},"accessToken",[248,604,605],{"class":381},") ",[248,607,608],{"class":261},"throw",[248,610,611],{"class":265}," new",[248,613,281],{"class":460},[248,615,515],{"class":381},[248,617,518],{"class":265},[248,619,620],{"class":301},"Missing GitHub token",[248,622,518],{"class":265},[248,624,535],{"class":381},[248,626,628],{"class":250,"line":627},22,[248,629,361],{"emptyLinePlaceholder":360},[248,631,633,635,638,640,643,645,648,650,652],{"class":250,"line":632},23,[248,634,570],{"class":367},[248,636,637],{"class":269}," profile",[248,639,495],{"class":265},[248,641,642],{"class":261}," await",[248,644,578],{"class":265},[248,646,647],{"class":460},"fetchProfile",[248,649,515],{"class":381},[248,651,602],{"class":269},[248,653,535],{"class":381},[248,655,657,660,662,665,668,671,673],{"class":250,"line":656},24,[248,658,659],{"class":261},"    return",[248,661,578],{"class":265},[248,663,664],{"class":269},"usersService",[248,666,667],{"class":265},".",[248,669,670],{"class":460},"upsertFromOAuth",[248,672,515],{"class":381},[248,674,675],{"class":265},"{\n",[248,677,679,682,684,686,689,691],{"class":250,"line":678},25,[248,680,681],{"class":381},"      provider",[248,683,385],{"class":265},[248,685,298],{"class":265},[248,687,688],{"class":301},"github",[248,690,518],{"class":265},[248,692,693],{"class":265},",\n",[248,695,697,700,702,705,707,710,712,715,717],{"class":250,"line":696},26,[248,698,699],{"class":381},"      externalId",[248,701,385],{"class":265},[248,703,704],{"class":460}," String",[248,706,515],{"class":381},[248,708,709],{"class":269},"profile",[248,711,667],{"class":265},[248,713,714],{"class":269},"id",[248,716,562],{"class":381},[248,718,693],{"class":265},[248,720,722,725,727,729,731,734],{"class":250,"line":721},27,[248,723,724],{"class":381},"      email",[248,726,385],{"class":265},[248,728,637],{"class":269},[248,730,667],{"class":265},[248,732,733],{"class":269},"email",[248,735,693],{"class":265},[248,737,739,742,744,746,748,751,754,756,758,761],{"class":250,"line":738},28,[248,740,741],{"class":381},"      name",[248,743,385],{"class":265},[248,745,637],{"class":269},[248,747,667],{"class":265},[248,749,750],{"class":269},"name",[248,752,753],{"class":265}," ??",[248,755,637],{"class":269},[248,757,667],{"class":265},[248,759,760],{"class":269},"login",[248,762,693],{"class":265},[248,764,766,769,771,773,775,778],{"class":250,"line":765},29,[248,767,768],{"class":381},"      avatar",[248,770,385],{"class":265},[248,772,637],{"class":269},[248,774,667],{"class":265},[248,776,777],{"class":269},"avatar_url",[248,779,693],{"class":265},[248,781,783,786],{"class":250,"line":782},30,[248,784,785],{"class":265},"    }",[248,787,535],{"class":381},[248,789,791],{"class":250,"line":790},31,[248,792,793],{"class":265},"  }\n",[248,795,797],{"class":250,"line":796},32,[248,798,361],{"emptyLinePlaceholder":360},[248,800,802,804,807,810,812,814,816,818,821,824,827,830,833],{"class":250,"line":801},33,[248,803,489],{"class":367},[248,805,806],{"class":367}," async",[248,808,809],{"class":381}," fetchProfile",[248,811,515],{"class":265},[248,813,530],{"class":554},[248,815,385],{"class":265},[248,817,410],{"class":371},[248,819,820],{"class":265},"):",[248,822,823],{"class":371}," Promise",[248,825,826],{"class":265},"\u003C",[248,828,829],{"class":371},"GitHubProfile",[248,831,832],{"class":265},">",[248,834,375],{"class":265},[248,836,838,840,843,845,847,850,852,854,857,859,861],{"class":250,"line":837},34,[248,839,570],{"class":367},[248,841,842],{"class":269}," res",[248,844,495],{"class":265},[248,846,642],{"class":261},[248,848,849],{"class":460}," fetch",[248,851,515],{"class":381},[248,853,518],{"class":265},[248,855,856],{"class":301},"https:\u002F\u002Fapi.github.com\u002Fuser",[248,858,518],{"class":265},[248,860,273],{"class":265},[248,862,375],{"class":265},[248,864,866,869,871],{"class":250,"line":865},35,[248,867,868],{"class":381},"      headers",[248,870,385],{"class":265},[248,872,375],{"class":265},[248,874,876,879,881,884,887,890,892,895],{"class":250,"line":875},36,[248,877,878],{"class":381},"        authorization",[248,880,385],{"class":265},[248,882,883],{"class":265}," `",[248,885,886],{"class":301},"token ",[248,888,889],{"class":265},"${",[248,891,530],{"class":269},[248,893,894],{"class":265},"}`",[248,896,693],{"class":265},[248,898,900,903,906,908,910,912,915,917],{"class":250,"line":899},37,[248,901,902],{"class":265},"        '",[248,904,905],{"class":381},"user-agent",[248,907,518],{"class":265},[248,909,385],{"class":265},[248,911,298],{"class":265},[248,913,914],{"class":301},"miia-app",[248,916,518],{"class":265},[248,918,693],{"class":265},[248,920,922,925,927,929,932,934],{"class":250,"line":921},38,[248,923,924],{"class":381},"        accept",[248,926,385],{"class":265},[248,928,298],{"class":265},[248,930,931],{"class":301},"application\u002Fvnd.github+json",[248,933,518],{"class":265},[248,935,693],{"class":265},[248,937,939],{"class":250,"line":938},39,[248,940,941],{"class":265},"      },\n",[248,943,945,947],{"class":250,"line":944},40,[248,946,785],{"class":265},[248,948,535],{"class":381},[248,950,952,954,956,958,961,963,966,968,970,972,974,976,978,981,983],{"class":250,"line":951},41,[248,953,593],{"class":261},[248,955,596],{"class":381},[248,957,599],{"class":265},[248,959,960],{"class":269},"res",[248,962,667],{"class":265},[248,964,965],{"class":269},"ok",[248,967,605],{"class":381},[248,969,608],{"class":261},[248,971,611],{"class":265},[248,973,281],{"class":460},[248,975,515],{"class":381},[248,977,518],{"class":265},[248,979,980],{"class":301},"Invalid GitHub token",[248,982,518],{"class":265},[248,984,535],{"class":381},[248,986,988,990,992,994,997,1000,1003,1005,1007,1009],{"class":250,"line":987},42,[248,989,659],{"class":261},[248,991,842],{"class":269},[248,993,667],{"class":265},[248,995,996],{"class":460},"json",[248,998,999],{"class":381},"() ",[248,1001,1002],{"class":261},"as",[248,1004,823],{"class":371},[248,1006,826],{"class":265},[248,1008,829],{"class":371},[248,1010,1011],{"class":265},">\n",[248,1013,1015],{"class":250,"line":1014},43,[248,1016,793],{"class":265},[248,1018,1020],{"class":250,"line":1019},44,[248,1021,446],{"class":265},[187,1023,1024],{},"Use it on any route that accepts a GitHub token directly:",[239,1026,1028],{"className":241,"code":1027,"language":243,"meta":244,"style":244},"@Controller('\u002Fapi')\n@UseGuard(AuthGuard(GitHubAuth))\nclass ApiController {\n  @Get('\u002Fprofile')\n  profile(ctx: RequestContext) {\n    return ctx.user\n  }\n}\n",[212,1029,1030,1048,1063,1073,1092,1109,1121,1125],{"__ignoreMap":244},[248,1031,1032,1034,1037,1039,1041,1044,1046],{"class":250,"line":251},[248,1033,457],{"class":265},[248,1035,1036],{"class":460},"Controller",[248,1038,515],{"class":269},[248,1040,518],{"class":265},[248,1042,1043],{"class":301},"\u002Fapi",[248,1045,518],{"class":265},[248,1047,535],{"class":269},[248,1049,1050,1052,1055,1057,1060],{"class":250,"line":258},[248,1051,457],{"class":265},[248,1053,1054],{"class":460},"UseGuard",[248,1056,515],{"class":269},[248,1058,1059],{"class":460},"AuthGuard",[248,1061,1062],{"class":269},"(GitHubAuth))\n",[248,1064,1065,1068,1071],{"class":250,"line":308},[248,1066,1067],{"class":367},"class",[248,1069,1070],{"class":371}," ApiController",[248,1072,375],{"class":265},[248,1074,1075,1078,1081,1083,1085,1088,1090],{"class":250,"line":336},[248,1076,1077],{"class":265},"  @",[248,1079,1080],{"class":460},"Get",[248,1082,515],{"class":269},[248,1084,518],{"class":265},[248,1086,1087],{"class":301},"\u002Fprofile",[248,1089,518],{"class":265},[248,1091,535],{"class":269},[248,1093,1094,1097,1099,1101,1103,1105,1107],{"class":250,"line":357},[248,1095,1096],{"class":381},"  profile",[248,1098,515],{"class":265},[248,1100,555],{"class":554},[248,1102,385],{"class":265},[248,1104,289],{"class":371},[248,1106,562],{"class":265},[248,1108,375],{"class":265},[248,1110,1111,1113,1116,1118],{"class":250,"line":364},[248,1112,659],{"class":261},[248,1114,1115],{"class":269}," ctx",[248,1117,667],{"class":265},[248,1119,1120],{"class":269},"user\n",[248,1122,1123],{"class":250,"line":378},[248,1124,793],{"class":265},[248,1126,1127],{"class":250,"line":391},[248,1128,446],{"class":265},[187,1130,1131,1132,1135],{},"Or stack it with a JWT provider so a route accepts ",[197,1133,1134],{},"either"," flavor:",[239,1137,1139],{"className":241,"code":1138,"language":243,"meta":244,"style":244},"@UseGuard(AuthGuard(JwtAuth, GitHubAuth))\n",[212,1140,1141],{"__ignoreMap":244},[248,1142,1143,1145,1147,1149,1151,1154,1156],{"class":250,"line":251},[248,1144,457],{"class":265},[248,1146,1054],{"class":460},[248,1148,515],{"class":269},[248,1150,1059],{"class":460},[248,1152,1153],{"class":269},"(JwtAuth",[248,1155,273],{"class":265},[248,1157,1158],{"class":269}," GitHubAuth))\n",[187,1160,1161],{},"The JWT provider runs first (cheap local verification); the GitHub provider runs only as fallback (one network round-trip).",[182,1163,199],{"id":1164},"server-side-redirect-flow",[187,1166,1167,1168,1170],{},"For the \"Sign in with GitHub\" button on your own site, you want a redirect endpoint and a callback endpoint. The callback then issues ",[197,1169,203],{}," JWT so the rest of your app never has to touch GitHub.",[239,1172,1174],{"className":241,"code":1173,"language":243,"meta":244,"style":244},"\u002F\u002F src\u002Fauth\u002Foauth2\u002Fgithub-oauth.controller.ts\nimport { Controller, Get, inject, BadRequestException, type RequestContext } from '@miiajs\u002Fcore'\nimport { ConfigService } from '@miiajs\u002Fconfig'\nimport { JwtService } from '@miiajs\u002Fjwt'\nimport { GitHubAuth } from '..\u002Fproviders\u002Fgithub-auth.provider.js'\n\n@Controller('\u002Fauth\u002Fgithub')\nexport class GitHubOAuthController {\n  private configService = inject(ConfigService)\n  private jwtService = inject(JwtService)\n  private gitHubAuth = inject(GitHubAuth)\n\n  @Get('\u002F')\n  redirect(ctx: RequestContext) {\n    const url = new URL('https:\u002F\u002Fgithub.com\u002Flogin\u002Foauth\u002Fauthorize')\n    url.searchParams.set('client_id', this.configService.getOrThrow('GITHUB_CLIENT_ID'))\n    url.searchParams.set('redirect_uri', this.configService.getOrThrow('GITHUB_REDIRECT_URI'))\n    url.searchParams.set('scope', 'read:user user:email')\n    url.searchParams.set('state', this.issueState(ctx))\n    return ctx.res.redirect(url.toString())\n  }\n\n  @Get('\u002Fcallback')\n  async callback(ctx: RequestContext) {\n    const code = ctx.query.code\n    const state = ctx.query.state\n    if (!code) throw new BadRequestException('Missing code')\n    this.verifyState(ctx, state)\n\n    const accessToken = await this.exchangeCode(code)\n\n    \u002F\u002F Reuse the provider - synthesize a context with the GitHub token in the header.\n    const fakeCtx = {\n      req: new Request('http:\u002F\u002Finternal\u002F', {\n        headers: { authorization: `token ${accessToken}` },\n      }),\n    } as RequestContext\n\n    const user = await this.gitHubAuth.authenticate(fakeCtx)\n\n    \u002F\u002F Issue OUR jwt so subsequent requests don't hit GitHub.\n    const token = await this.jwtService.sign({ sub: (user as { id: number }).id })\n    return ctx.res.redirect(`\u002F?token=${encodeURIComponent(token)}`)\n  }\n\n  private async exchangeCode(code: string): Promise\u003Cstring> {\n    const res = await fetch('https:\u002F\u002Fgithub.com\u002Flogin\u002Foauth\u002Faccess_token', {\n      method: 'POST',\n      headers: { accept: 'application\u002Fjson', 'content-type': 'application\u002Fjson' },\n      body: JSON.stringify({\n        client_id: this.configService.getOrThrow('GITHUB_CLIENT_ID'),\n        client_secret: this.configService.getOrThrow('GITHUB_CLIENT_SECRET'),\n        code,\n      }),\n    })\n    if (!res.ok) throw new BadRequestException('GitHub code exchange failed')\n    const json = (await res.json()) as { access_token?: string; error?: string }\n    if (!json.access_token) throw new BadRequestException(json.error ?? 'No access token')\n    return json.access_token\n  }\n\n  \u002F** Issue a signed CSRF state, valid for 10 minutes. *\u002F\n  private issueState(_ctx: RequestContext): string {\n    \u002F\u002F Use JwtService for a short-lived signed nonce, or store in a cache keyed to session.\n    return crypto.randomUUID()\n  }\n\n  \u002F** Verify the state matches what we issued. *\u002F\n  private verifyState(_ctx: RequestContext, _state: string | undefined) {\n    \u002F\u002F Compare against the stored value (cookie, session, redis). Throw on mismatch.\n  }\n}\n",[212,1175,1176,1181,1220,1240,1260,1279,1283,1300,1311,1325,1339,1353,1357,1374,1391,1416,1464,1506,1538,1572,1600,1604,1608,1625,1644,1665,1685,1714,1732,1736,1757,1761,1766,1777,1802,1829,1838,1848,1852,1880,1884,1889,1949,1983,1987,1992,2023,2049,2066,2106,2126,2154,2183,2191,2200,2207,2241,2290,2334,2346,2351,2356,2362,2385,2391,2406,2411,2416,2422,2456,2462,2467],{"__ignoreMap":244},[248,1177,1178],{"class":250,"line":251},[248,1179,1180],{"class":254},"\u002F\u002F src\u002Fauth\u002Foauth2\u002Fgithub-oauth.controller.ts\n",[248,1182,1183,1185,1187,1190,1192,1195,1197,1199,1201,1204,1206,1208,1210,1212,1214,1216,1218],{"class":250,"line":258},[248,1184,262],{"class":261},[248,1186,266],{"class":265},[248,1188,1189],{"class":269}," Controller",[248,1191,273],{"class":265},[248,1193,1194],{"class":269}," Get",[248,1196,273],{"class":265},[248,1198,276],{"class":269},[248,1200,273],{"class":265},[248,1202,1203],{"class":269}," BadRequestException",[248,1205,273],{"class":265},[248,1207,286],{"class":261},[248,1209,289],{"class":269},[248,1211,292],{"class":265},[248,1213,295],{"class":261},[248,1215,298],{"class":265},[248,1217,302],{"class":301},[248,1219,305],{"class":265},[248,1221,1222,1224,1226,1229,1231,1233,1235,1238],{"class":250,"line":308},[248,1223,262],{"class":261},[248,1225,266],{"class":265},[248,1227,1228],{"class":269}," ConfigService",[248,1230,292],{"class":265},[248,1232,295],{"class":261},[248,1234,298],{"class":265},[248,1236,1237],{"class":301},"@miiajs\u002Fconfig",[248,1239,305],{"class":265},[248,1241,1242,1244,1246,1249,1251,1253,1255,1258],{"class":250,"line":336},[248,1243,262],{"class":261},[248,1245,266],{"class":265},[248,1247,1248],{"class":269}," JwtService",[248,1250,292],{"class":265},[248,1252,295],{"class":261},[248,1254,298],{"class":265},[248,1256,1257],{"class":301},"@miiajs\u002Fjwt",[248,1259,305],{"class":265},[248,1261,1262,1264,1266,1268,1270,1272,1274,1277],{"class":250,"line":357},[248,1263,262],{"class":261},[248,1265,266],{"class":265},[248,1267,476],{"class":269},[248,1269,292],{"class":265},[248,1271,295],{"class":261},[248,1273,298],{"class":265},[248,1275,1276],{"class":301},"..\u002Fproviders\u002Fgithub-auth.provider.js",[248,1278,305],{"class":265},[248,1280,1281],{"class":250,"line":364},[248,1282,361],{"emptyLinePlaceholder":360},[248,1284,1285,1287,1289,1291,1293,1296,1298],{"class":250,"line":378},[248,1286,457],{"class":265},[248,1288,1036],{"class":460},[248,1290,515],{"class":269},[248,1292,518],{"class":265},[248,1294,1295],{"class":301},"\u002Fauth\u002Fgithub",[248,1297,518],{"class":265},[248,1299,535],{"class":269},[248,1301,1302,1304,1306,1309],{"class":250,"line":391},[248,1303,470],{"class":261},[248,1305,473],{"class":367},[248,1307,1308],{"class":371}," GitHubOAuthController",[248,1310,375],{"class":265},[248,1312,1313,1315,1318,1320,1322],{"class":250,"line":402},[248,1314,489],{"class":367},[248,1316,1317],{"class":381}," configService",[248,1319,495],{"class":265},[248,1321,276],{"class":381},[248,1323,1324],{"class":269},"(ConfigService)\n",[248,1326,1327,1329,1332,1334,1336],{"class":250,"line":419},[248,1328,489],{"class":367},[248,1330,1331],{"class":381}," jwtService",[248,1333,495],{"class":265},[248,1335,276],{"class":381},[248,1337,1338],{"class":269},"(JwtService)\n",[248,1340,1341,1343,1346,1348,1350],{"class":250,"line":433},[248,1342,489],{"class":367},[248,1344,1345],{"class":381}," gitHubAuth",[248,1347,495],{"class":265},[248,1349,276],{"class":381},[248,1351,1352],{"class":269},"(GitHubAuth)\n",[248,1354,1355],{"class":250,"line":443},[248,1356,361],{"emptyLinePlaceholder":360},[248,1358,1359,1361,1363,1365,1367,1370,1372],{"class":250,"line":449},[248,1360,1077],{"class":265},[248,1362,1080],{"class":460},[248,1364,515],{"class":269},[248,1366,518],{"class":265},[248,1368,1369],{"class":301},"\u002F",[248,1371,518],{"class":265},[248,1373,535],{"class":269},[248,1375,1376,1379,1381,1383,1385,1387,1389],{"class":250,"line":454},[248,1377,1378],{"class":381},"  redirect",[248,1380,515],{"class":265},[248,1382,555],{"class":554},[248,1384,385],{"class":265},[248,1386,289],{"class":371},[248,1388,562],{"class":265},[248,1390,375],{"class":265},[248,1392,1393,1395,1398,1400,1402,1405,1407,1409,1412,1414],{"class":250,"line":467},[248,1394,570],{"class":367},[248,1396,1397],{"class":269}," url",[248,1399,495],{"class":265},[248,1401,611],{"class":265},[248,1403,1404],{"class":460}," URL",[248,1406,515],{"class":381},[248,1408,518],{"class":265},[248,1410,1411],{"class":301},"https:\u002F\u002Fgithub.com\u002Flogin\u002Foauth\u002Fauthorize",[248,1413,518],{"class":265},[248,1415,535],{"class":381},[248,1417,1418,1421,1423,1426,1428,1431,1433,1435,1438,1440,1442,1444,1447,1449,1452,1454,1456,1459,1461],{"class":250,"line":486},[248,1419,1420],{"class":269},"    url",[248,1422,667],{"class":265},[248,1424,1425],{"class":269},"searchParams",[248,1427,667],{"class":265},[248,1429,1430],{"class":460},"set",[248,1432,515],{"class":381},[248,1434,518],{"class":265},[248,1436,1437],{"class":301},"client_id",[248,1439,518],{"class":265},[248,1441,273],{"class":265},[248,1443,578],{"class":265},[248,1445,1446],{"class":269},"configService",[248,1448,667],{"class":265},[248,1450,1451],{"class":460},"getOrThrow",[248,1453,515],{"class":381},[248,1455,518],{"class":265},[248,1457,1458],{"class":301},"GITHUB_CLIENT_ID",[248,1460,518],{"class":265},[248,1462,1463],{"class":381},"))\n",[248,1465,1466,1468,1470,1472,1474,1476,1478,1480,1483,1485,1487,1489,1491,1493,1495,1497,1499,1502,1504],{"class":250,"line":503},[248,1467,1420],{"class":269},[248,1469,667],{"class":265},[248,1471,1425],{"class":269},[248,1473,667],{"class":265},[248,1475,1430],{"class":460},[248,1477,515],{"class":381},[248,1479,518],{"class":265},[248,1481,1482],{"class":301},"redirect_uri",[248,1484,518],{"class":265},[248,1486,273],{"class":265},[248,1488,578],{"class":265},[248,1490,1446],{"class":269},[248,1492,667],{"class":265},[248,1494,1451],{"class":460},[248,1496,515],{"class":381},[248,1498,518],{"class":265},[248,1500,1501],{"class":301},"GITHUB_REDIRECT_URI",[248,1503,518],{"class":265},[248,1505,1463],{"class":381},[248,1507,1508,1510,1512,1514,1516,1518,1520,1522,1525,1527,1529,1531,1534,1536],{"class":250,"line":538},[248,1509,1420],{"class":269},[248,1511,667],{"class":265},[248,1513,1425],{"class":269},[248,1515,667],{"class":265},[248,1517,1430],{"class":460},[248,1519,515],{"class":381},[248,1521,518],{"class":265},[248,1523,1524],{"class":301},"scope",[248,1526,518],{"class":265},[248,1528,273],{"class":265},[248,1530,298],{"class":265},[248,1532,1533],{"class":301},"read:user user:email",[248,1535,518],{"class":265},[248,1537,535],{"class":381},[248,1539,1540,1542,1544,1546,1548,1550,1552,1554,1557,1559,1561,1563,1566,1568,1570],{"class":250,"line":543},[248,1541,1420],{"class":269},[248,1543,667],{"class":265},[248,1545,1425],{"class":269},[248,1547,667],{"class":265},[248,1549,1430],{"class":460},[248,1551,515],{"class":381},[248,1553,518],{"class":265},[248,1555,1556],{"class":301},"state",[248,1558,518],{"class":265},[248,1560,273],{"class":265},[248,1562,578],{"class":265},[248,1564,1565],{"class":460},"issueState",[248,1567,515],{"class":381},[248,1569,555],{"class":269},[248,1571,1463],{"class":381},[248,1573,1574,1576,1578,1580,1582,1584,1587,1589,1592,1594,1597],{"class":250,"line":567},[248,1575,659],{"class":261},[248,1577,1115],{"class":269},[248,1579,667],{"class":265},[248,1581,960],{"class":269},[248,1583,667],{"class":265},[248,1585,1586],{"class":460},"redirect",[248,1588,515],{"class":381},[248,1590,1591],{"class":269},"url",[248,1593,667],{"class":265},[248,1595,1596],{"class":460},"toString",[248,1598,1599],{"class":381},"())\n",[248,1601,1602],{"class":250,"line":590},[248,1603,793],{"class":265},[248,1605,1606],{"class":250,"line":627},[248,1607,361],{"emptyLinePlaceholder":360},[248,1609,1610,1612,1614,1616,1618,1621,1623],{"class":250,"line":632},[248,1611,1077],{"class":265},[248,1613,1080],{"class":460},[248,1615,515],{"class":269},[248,1617,518],{"class":265},[248,1619,1620],{"class":301},"\u002Fcallback",[248,1622,518],{"class":265},[248,1624,535],{"class":269},[248,1626,1627,1629,1632,1634,1636,1638,1640,1642],{"class":250,"line":656},[248,1628,546],{"class":367},[248,1630,1631],{"class":381}," callback",[248,1633,515],{"class":265},[248,1635,555],{"class":554},[248,1637,385],{"class":265},[248,1639,289],{"class":371},[248,1641,562],{"class":265},[248,1643,375],{"class":265},[248,1645,1646,1648,1651,1653,1655,1657,1660,1662],{"class":250,"line":678},[248,1647,570],{"class":367},[248,1649,1650],{"class":269}," code",[248,1652,495],{"class":265},[248,1654,1115],{"class":269},[248,1656,667],{"class":265},[248,1658,1659],{"class":269},"query",[248,1661,667],{"class":265},[248,1663,1664],{"class":269},"code\n",[248,1666,1667,1669,1672,1674,1676,1678,1680,1682],{"class":250,"line":696},[248,1668,570],{"class":367},[248,1670,1671],{"class":269}," state",[248,1673,495],{"class":265},[248,1675,1115],{"class":269},[248,1677,667],{"class":265},[248,1679,1659],{"class":269},[248,1681,667],{"class":265},[248,1683,1684],{"class":269},"state\n",[248,1686,1687,1689,1691,1693,1695,1697,1699,1701,1703,1705,1707,1710,1712],{"class":250,"line":721},[248,1688,593],{"class":261},[248,1690,596],{"class":381},[248,1692,599],{"class":265},[248,1694,212],{"class":269},[248,1696,605],{"class":381},[248,1698,608],{"class":261},[248,1700,611],{"class":265},[248,1702,1203],{"class":460},[248,1704,515],{"class":381},[248,1706,518],{"class":265},[248,1708,1709],{"class":301},"Missing code",[248,1711,518],{"class":265},[248,1713,535],{"class":381},[248,1715,1716,1719,1722,1724,1726,1728,1730],{"class":250,"line":738},[248,1717,1718],{"class":265},"    this.",[248,1720,1721],{"class":460},"verifyState",[248,1723,515],{"class":381},[248,1725,555],{"class":269},[248,1727,273],{"class":265},[248,1729,1671],{"class":269},[248,1731,535],{"class":381},[248,1733,1734],{"class":250,"line":765},[248,1735,361],{"emptyLinePlaceholder":360},[248,1737,1738,1740,1742,1744,1746,1748,1751,1753,1755],{"class":250,"line":782},[248,1739,570],{"class":367},[248,1741,573],{"class":269},[248,1743,495],{"class":265},[248,1745,642],{"class":261},[248,1747,578],{"class":265},[248,1749,1750],{"class":460},"exchangeCode",[248,1752,515],{"class":381},[248,1754,212],{"class":269},[248,1756,535],{"class":381},[248,1758,1759],{"class":250,"line":790},[248,1760,361],{"emptyLinePlaceholder":360},[248,1762,1763],{"class":250,"line":796},[248,1764,1765],{"class":254},"    \u002F\u002F Reuse the provider - synthesize a context with the GitHub token in the header.\n",[248,1767,1768,1770,1773,1775],{"class":250,"line":801},[248,1769,570],{"class":367},[248,1771,1772],{"class":269}," fakeCtx",[248,1774,495],{"class":265},[248,1776,375],{"class":265},[248,1778,1779,1782,1784,1786,1789,1791,1793,1796,1798,1800],{"class":250,"line":837},[248,1780,1781],{"class":381},"      req",[248,1783,385],{"class":265},[248,1785,611],{"class":265},[248,1787,1788],{"class":460}," Request",[248,1790,515],{"class":381},[248,1792,518],{"class":265},[248,1794,1795],{"class":301},"http:\u002F\u002Finternal\u002F",[248,1797,518],{"class":265},[248,1799,273],{"class":265},[248,1801,375],{"class":265},[248,1803,1804,1807,1809,1811,1814,1816,1818,1820,1822,1824,1826],{"class":250,"line":865},[248,1805,1806],{"class":381},"        headers",[248,1808,385],{"class":265},[248,1810,266],{"class":265},[248,1812,1813],{"class":381}," authorization",[248,1815,385],{"class":265},[248,1817,883],{"class":265},[248,1819,886],{"class":301},[248,1821,889],{"class":265},[248,1823,602],{"class":269},[248,1825,894],{"class":265},[248,1827,1828],{"class":265}," },\n",[248,1830,1831,1834,1836],{"class":250,"line":875},[248,1832,1833],{"class":265},"      }",[248,1835,562],{"class":381},[248,1837,693],{"class":265},[248,1839,1840,1842,1845],{"class":250,"line":899},[248,1841,785],{"class":265},[248,1843,1844],{"class":261}," as",[248,1846,1847],{"class":371}," RequestContext\n",[248,1849,1850],{"class":250,"line":921},[248,1851,361],{"emptyLinePlaceholder":360},[248,1853,1854,1856,1859,1861,1863,1865,1868,1870,1873,1875,1878],{"class":250,"line":938},[248,1855,570],{"class":367},[248,1857,1858],{"class":269}," user",[248,1860,495],{"class":265},[248,1862,642],{"class":261},[248,1864,578],{"class":265},[248,1866,1867],{"class":269},"gitHubAuth",[248,1869,667],{"class":265},[248,1871,1872],{"class":460},"authenticate",[248,1874,515],{"class":381},[248,1876,1877],{"class":269},"fakeCtx",[248,1879,535],{"class":381},[248,1881,1882],{"class":250,"line":944},[248,1883,361],{"emptyLinePlaceholder":360},[248,1885,1886],{"class":250,"line":951},[248,1887,1888],{"class":254},"    \u002F\u002F Issue OUR jwt so subsequent requests don't hit GitHub.\n",[248,1890,1891,1893,1896,1898,1900,1902,1905,1907,1910,1912,1915,1918,1920,1922,1925,1927,1929,1932,1934,1937,1939,1941,1943,1945,1947],{"class":250,"line":987},[248,1892,570],{"class":367},[248,1894,1895],{"class":269}," token",[248,1897,495],{"class":265},[248,1899,642],{"class":261},[248,1901,578],{"class":265},[248,1903,1904],{"class":269},"jwtService",[248,1906,667],{"class":265},[248,1908,1909],{"class":460},"sign",[248,1911,515],{"class":381},[248,1913,1914],{"class":265},"{",[248,1916,1917],{"class":381}," sub",[248,1919,385],{"class":265},[248,1921,596],{"class":381},[248,1923,1924],{"class":269},"user",[248,1926,1844],{"class":261},[248,1928,266],{"class":265},[248,1930,1931],{"class":381}," id",[248,1933,385],{"class":265},[248,1935,1936],{"class":371}," number",[248,1938,292],{"class":265},[248,1940,562],{"class":381},[248,1942,667],{"class":265},[248,1944,714],{"class":269},[248,1946,292],{"class":265},[248,1948,535],{"class":381},[248,1950,1951,1953,1955,1957,1959,1961,1963,1965,1968,1971,1973,1976,1979,1981],{"class":250,"line":1014},[248,1952,659],{"class":261},[248,1954,1115],{"class":269},[248,1956,667],{"class":265},[248,1958,960],{"class":269},[248,1960,667],{"class":265},[248,1962,1586],{"class":460},[248,1964,515],{"class":381},[248,1966,1967],{"class":265},"`",[248,1969,1970],{"class":301},"\u002F?token=",[248,1972,889],{"class":265},[248,1974,1975],{"class":460},"encodeURIComponent",[248,1977,1978],{"class":269},"(token)",[248,1980,894],{"class":265},[248,1982,535],{"class":381},[248,1984,1985],{"class":250,"line":1019},[248,1986,793],{"class":265},[248,1988,1990],{"class":250,"line":1989},45,[248,1991,361],{"emptyLinePlaceholder":360},[248,1993,1995,1997,1999,2002,2004,2006,2008,2010,2012,2014,2016,2019,2021],{"class":250,"line":1994},46,[248,1996,489],{"class":367},[248,1998,806],{"class":367},[248,2000,2001],{"class":381}," exchangeCode",[248,2003,515],{"class":265},[248,2005,212],{"class":554},[248,2007,385],{"class":265},[248,2009,410],{"class":371},[248,2011,820],{"class":265},[248,2013,823],{"class":371},[248,2015,826],{"class":265},[248,2017,2018],{"class":371},"string",[248,2020,832],{"class":265},[248,2022,375],{"class":265},[248,2024,2026,2028,2030,2032,2034,2036,2038,2040,2043,2045,2047],{"class":250,"line":2025},47,[248,2027,570],{"class":367},[248,2029,842],{"class":269},[248,2031,495],{"class":265},[248,2033,642],{"class":261},[248,2035,849],{"class":460},[248,2037,515],{"class":381},[248,2039,518],{"class":265},[248,2041,2042],{"class":301},"https:\u002F\u002Fgithub.com\u002Flogin\u002Foauth\u002Faccess_token",[248,2044,518],{"class":265},[248,2046,273],{"class":265},[248,2048,375],{"class":265},[248,2050,2052,2055,2057,2059,2062,2064],{"class":250,"line":2051},48,[248,2053,2054],{"class":381},"      method",[248,2056,385],{"class":265},[248,2058,298],{"class":265},[248,2060,2061],{"class":301},"POST",[248,2063,518],{"class":265},[248,2065,693],{"class":265},[248,2067,2069,2071,2073,2075,2078,2080,2082,2085,2087,2089,2091,2094,2096,2098,2100,2102,2104],{"class":250,"line":2068},49,[248,2070,868],{"class":381},[248,2072,385],{"class":265},[248,2074,266],{"class":265},[248,2076,2077],{"class":381}," accept",[248,2079,385],{"class":265},[248,2081,298],{"class":265},[248,2083,2084],{"class":301},"application\u002Fjson",[248,2086,518],{"class":265},[248,2088,273],{"class":265},[248,2090,298],{"class":265},[248,2092,2093],{"class":381},"content-type",[248,2095,518],{"class":265},[248,2097,385],{"class":265},[248,2099,298],{"class":265},[248,2101,2084],{"class":301},[248,2103,518],{"class":265},[248,2105,1828],{"class":265},[248,2107,2109,2112,2114,2117,2119,2122,2124],{"class":250,"line":2108},50,[248,2110,2111],{"class":381},"      body",[248,2113,385],{"class":265},[248,2115,2116],{"class":269}," JSON",[248,2118,667],{"class":265},[248,2120,2121],{"class":460},"stringify",[248,2123,515],{"class":381},[248,2125,675],{"class":265},[248,2127,2129,2132,2134,2136,2138,2140,2142,2144,2146,2148,2150,2152],{"class":250,"line":2128},51,[248,2130,2131],{"class":381},"        client_id",[248,2133,385],{"class":265},[248,2135,578],{"class":265},[248,2137,1446],{"class":269},[248,2139,667],{"class":265},[248,2141,1451],{"class":460},[248,2143,515],{"class":381},[248,2145,518],{"class":265},[248,2147,1458],{"class":301},[248,2149,518],{"class":265},[248,2151,562],{"class":381},[248,2153,693],{"class":265},[248,2155,2157,2160,2162,2164,2166,2168,2170,2172,2174,2177,2179,2181],{"class":250,"line":2156},52,[248,2158,2159],{"class":381},"        client_secret",[248,2161,385],{"class":265},[248,2163,578],{"class":265},[248,2165,1446],{"class":269},[248,2167,667],{"class":265},[248,2169,1451],{"class":460},[248,2171,515],{"class":381},[248,2173,518],{"class":265},[248,2175,2176],{"class":301},"GITHUB_CLIENT_SECRET",[248,2178,518],{"class":265},[248,2180,562],{"class":381},[248,2182,693],{"class":265},[248,2184,2186,2189],{"class":250,"line":2185},53,[248,2187,2188],{"class":269},"        code",[248,2190,693],{"class":265},[248,2192,2194,2196,2198],{"class":250,"line":2193},54,[248,2195,1833],{"class":265},[248,2197,562],{"class":381},[248,2199,693],{"class":265},[248,2201,2203,2205],{"class":250,"line":2202},55,[248,2204,785],{"class":265},[248,2206,535],{"class":381},[248,2208,2210,2212,2214,2216,2218,2220,2222,2224,2226,2228,2230,2232,2234,2237,2239],{"class":250,"line":2209},56,[248,2211,593],{"class":261},[248,2213,596],{"class":381},[248,2215,599],{"class":265},[248,2217,960],{"class":269},[248,2219,667],{"class":265},[248,2221,965],{"class":269},[248,2223,605],{"class":381},[248,2225,608],{"class":261},[248,2227,611],{"class":265},[248,2229,1203],{"class":460},[248,2231,515],{"class":381},[248,2233,518],{"class":265},[248,2235,2236],{"class":301},"GitHub code exchange failed",[248,2238,518],{"class":265},[248,2240,535],{"class":381},[248,2242,2244,2246,2249,2251,2253,2256,2258,2260,2262,2265,2267,2269,2272,2275,2277,2280,2283,2285,2287],{"class":250,"line":2243},57,[248,2245,570],{"class":367},[248,2247,2248],{"class":269}," json",[248,2250,495],{"class":265},[248,2252,596],{"class":381},[248,2254,2255],{"class":261},"await",[248,2257,842],{"class":269},[248,2259,667],{"class":265},[248,2261,996],{"class":460},[248,2263,2264],{"class":381},"()) ",[248,2266,1002],{"class":261},[248,2268,266],{"class":265},[248,2270,2271],{"class":381}," access_token",[248,2273,2274],{"class":265},"?:",[248,2276,410],{"class":371},[248,2278,2279],{"class":265},";",[248,2281,2282],{"class":381}," error",[248,2284,2274],{"class":265},[248,2286,410],{"class":371},[248,2288,2289],{"class":265}," }\n",[248,2291,2293,2295,2297,2299,2301,2303,2306,2308,2310,2312,2314,2316,2318,2320,2323,2325,2327,2330,2332],{"class":250,"line":2292},58,[248,2294,593],{"class":261},[248,2296,596],{"class":381},[248,2298,599],{"class":265},[248,2300,996],{"class":269},[248,2302,667],{"class":265},[248,2304,2305],{"class":269},"access_token",[248,2307,605],{"class":381},[248,2309,608],{"class":261},[248,2311,611],{"class":265},[248,2313,1203],{"class":460},[248,2315,515],{"class":381},[248,2317,996],{"class":269},[248,2319,667],{"class":265},[248,2321,2322],{"class":269},"error",[248,2324,753],{"class":265},[248,2326,298],{"class":265},[248,2328,2329],{"class":301},"No access token",[248,2331,518],{"class":265},[248,2333,535],{"class":381},[248,2335,2337,2339,2341,2343],{"class":250,"line":2336},59,[248,2338,659],{"class":261},[248,2340,2248],{"class":269},[248,2342,667],{"class":265},[248,2344,2345],{"class":269},"access_token\n",[248,2347,2349],{"class":250,"line":2348},60,[248,2350,793],{"class":265},[248,2352,2354],{"class":250,"line":2353},61,[248,2355,361],{"emptyLinePlaceholder":360},[248,2357,2359],{"class":250,"line":2358},62,[248,2360,2361],{"class":254},"  \u002F** Issue a signed CSRF state, valid for 10 minutes. *\u002F\n",[248,2363,2365,2367,2370,2372,2375,2377,2379,2381,2383],{"class":250,"line":2364},63,[248,2366,489],{"class":367},[248,2368,2369],{"class":381}," issueState",[248,2371,515],{"class":265},[248,2373,2374],{"class":554},"_ctx",[248,2376,385],{"class":265},[248,2378,289],{"class":371},[248,2380,820],{"class":265},[248,2382,410],{"class":371},[248,2384,375],{"class":265},[248,2386,2388],{"class":250,"line":2387},64,[248,2389,2390],{"class":254},"    \u002F\u002F Use JwtService for a short-lived signed nonce, or store in a cache keyed to session.\n",[248,2392,2394,2396,2399,2401,2404],{"class":250,"line":2393},65,[248,2395,659],{"class":261},[248,2397,2398],{"class":269}," crypto",[248,2400,667],{"class":265},[248,2402,2403],{"class":460},"randomUUID",[248,2405,464],{"class":381},[248,2407,2409],{"class":250,"line":2408},66,[248,2410,793],{"class":265},[248,2412,2414],{"class":250,"line":2413},67,[248,2415,361],{"emptyLinePlaceholder":360},[248,2417,2419],{"class":250,"line":2418},68,[248,2420,2421],{"class":254},"  \u002F** Verify the state matches what we issued. *\u002F\n",[248,2423,2425,2427,2430,2432,2434,2436,2438,2440,2443,2445,2447,2449,2452,2454],{"class":250,"line":2424},69,[248,2426,489],{"class":367},[248,2428,2429],{"class":381}," verifyState",[248,2431,515],{"class":265},[248,2433,2374],{"class":554},[248,2435,385],{"class":265},[248,2437,289],{"class":371},[248,2439,273],{"class":265},[248,2441,2442],{"class":554}," _state",[248,2444,385],{"class":265},[248,2446,410],{"class":371},[248,2448,413],{"class":265},[248,2450,2451],{"class":371}," undefined",[248,2453,562],{"class":265},[248,2455,375],{"class":265},[248,2457,2459],{"class":250,"line":2458},70,[248,2460,2461],{"class":254},"    \u002F\u002F Compare against the stored value (cookie, session, redis). Throw on mismatch.\n",[248,2463,2465],{"class":250,"line":2464},71,[248,2466,793],{"class":265},[248,2468,2470],{"class":250,"line":2469},72,[248,2471,446],{"class":265},[2473,2474,2475],"blockquote",{},[187,2476,2477,2480,2481,2484,2485,2491,2492,2495],{},[197,2478,2479],{},"Two GitHub-related decisions worth calling out."," First, after the callback we issue ",[197,2482,2483],{},"our own"," JWT - we do not store or pass around GitHub's access token for normal API requests. That avoids hitting GitHub on every call and keeps token revocation in our hands. Second, the callback ",[197,2486,2487,2488],{},"reuses ",[212,2489,2490],{},"GitHubAuth.authenticate()"," by synthesizing a minimal ",[212,2493,2494],{},"RequestContext"," - the upsert logic lives in one place, not two.",[182,2497,2499],{"id":2498},"module-wiring","Module wiring",[239,2501,2503],{"className":241,"code":2502,"language":243,"meta":244,"style":244},"\u002F\u002F src\u002Fauth\u002Fauth.module.ts\nimport { Module } from '@miiajs\u002Fcore'\nimport { AuthController } from '.\u002Fauth.controller.js'\nimport { GitHubOAuthController } from '.\u002Foauth2\u002Fgithub-oauth.controller.js'\nimport { GitHubAuth } from '.\u002Fproviders\u002Fgithub-auth.provider.js'\n\n@Module({\n  controllers: [AuthController, GitHubOAuthController],\n  providers: [GitHubAuth],\n})\nexport class AuthModule {}\n",[212,2504,2505,2510,2529,2549,2568,2587,2591,2602,2619,2631,2638],{"__ignoreMap":244},[248,2506,2507],{"class":250,"line":251},[248,2508,2509],{"class":254},"\u002F\u002F src\u002Fauth\u002Fauth.module.ts\n",[248,2511,2512,2514,2516,2519,2521,2523,2525,2527],{"class":250,"line":258},[248,2513,262],{"class":261},[248,2515,266],{"class":265},[248,2517,2518],{"class":269}," Module",[248,2520,292],{"class":265},[248,2522,295],{"class":261},[248,2524,298],{"class":265},[248,2526,302],{"class":301},[248,2528,305],{"class":265},[248,2530,2531,2533,2535,2538,2540,2542,2544,2547],{"class":250,"line":308},[248,2532,262],{"class":261},[248,2534,266],{"class":265},[248,2536,2537],{"class":269}," AuthController",[248,2539,292],{"class":265},[248,2541,295],{"class":261},[248,2543,298],{"class":265},[248,2545,2546],{"class":301},".\u002Fauth.controller.js",[248,2548,305],{"class":265},[248,2550,2551,2553,2555,2557,2559,2561,2563,2566],{"class":250,"line":336},[248,2552,262],{"class":261},[248,2554,266],{"class":265},[248,2556,1308],{"class":269},[248,2558,292],{"class":265},[248,2560,295],{"class":261},[248,2562,298],{"class":265},[248,2564,2565],{"class":301},".\u002Foauth2\u002Fgithub-oauth.controller.js",[248,2567,305],{"class":265},[248,2569,2570,2572,2574,2576,2578,2580,2582,2585],{"class":250,"line":357},[248,2571,262],{"class":261},[248,2573,266],{"class":265},[248,2575,476],{"class":269},[248,2577,292],{"class":265},[248,2579,295],{"class":261},[248,2581,298],{"class":265},[248,2583,2584],{"class":301},".\u002Fproviders\u002Fgithub-auth.provider.js",[248,2586,305],{"class":265},[248,2588,2589],{"class":250,"line":364},[248,2590,361],{"emptyLinePlaceholder":360},[248,2592,2593,2595,2598,2600],{"class":250,"line":378},[248,2594,457],{"class":265},[248,2596,2597],{"class":460},"Module",[248,2599,515],{"class":269},[248,2601,675],{"class":265},[248,2603,2604,2607,2609,2612,2614,2617],{"class":250,"line":391},[248,2605,2606],{"class":381},"  controllers",[248,2608,385],{"class":265},[248,2610,2611],{"class":269}," [AuthController",[248,2613,273],{"class":265},[248,2615,2616],{"class":269}," GitHubOAuthController]",[248,2618,693],{"class":265},[248,2620,2621,2624,2626,2629],{"class":250,"line":402},[248,2622,2623],{"class":381},"  providers",[248,2625,385],{"class":265},[248,2627,2628],{"class":269}," [GitHubAuth]",[248,2630,693],{"class":265},[248,2632,2633,2636],{"class":250,"line":419},[248,2634,2635],{"class":265},"}",[248,2637,535],{"class":269},[248,2639,2640,2642,2644,2647],{"class":250,"line":433},[248,2641,470],{"class":261},[248,2643,473],{"class":367},[248,2645,2646],{"class":371}," AuthModule",[248,2648,2649],{"class":265}," {}\n",[182,2651,2653],{"id":2652},"usersserviceupsertfromoauth","UsersService.upsertFromOAuth",[239,2655,2657],{"className":241,"code":2656,"language":243,"meta":244,"style":244},"async upsertFromOAuth(input: {\n  provider: 'github' | 'google' | 'apple'\n  externalId: string\n  email: string | null\n  name: string\n  avatar: string | null\n}) {\n  const [existing] = await this.db\n    .select()\n    .from(users)\n    .where(and(eq(users.provider, input.provider), eq(users.externalId, input.externalId)))\n\n  if (existing) return existing\n\n  const [created] = await this.db\n    .insert(users)\n    .values({\n      provider: input.provider,\n      externalId: input.externalId,\n      email: input.email,\n      name: input.name,\n      avatar: input.avatar,\n    })\n    .returning()\n\n  return created\n}\n",[212,2658,2659,2671,2702,2707,2717,2722,2731,2739,2762,2772,2786,2848,2852,2869,2873,2892,2905,2916,2930,2944,2958,2972,2987,2993,3002,3006,3014],{"__ignoreMap":244},[248,2660,2661,2664,2666,2669],{"class":250,"line":251},[248,2662,2663],{"class":269},"async ",[248,2665,670],{"class":460},[248,2667,2668],{"class":269},"(input: ",[248,2670,675],{"class":265},[248,2672,2673,2676,2678,2680,2682,2684,2686,2688,2691,2693,2695,2697,2700],{"class":250,"line":258},[248,2674,2675],{"class":381},"  provider",[248,2677,385],{"class":265},[248,2679,298],{"class":265},[248,2681,688],{"class":301},[248,2683,518],{"class":265},[248,2685,413],{"class":265},[248,2687,298],{"class":265},[248,2689,2690],{"class":301},"google",[248,2692,518],{"class":265},[248,2694,413],{"class":265},[248,2696,298],{"class":265},[248,2698,2699],{"class":301},"apple",[248,2701,305],{"class":265},[248,2703,2704],{"class":250,"line":308},[248,2705,2706],{"class":269},"  externalId: string\n",[248,2708,2709,2712,2715],{"class":250,"line":336},[248,2710,2711],{"class":269},"  email: string ",[248,2713,2714],{"class":265},"|",[248,2716,416],{"class":265},[248,2718,2719],{"class":250,"line":357},[248,2720,2721],{"class":269},"  name: string\n",[248,2723,2724,2727,2729],{"class":250,"line":364},[248,2725,2726],{"class":269},"  avatar: string ",[248,2728,2714],{"class":265},[248,2730,416],{"class":265},[248,2732,2733,2735,2737],{"class":250,"line":378},[248,2734,2635],{"class":265},[248,2736,605],{"class":269},[248,2738,675],{"class":265},[248,2740,2741,2744,2747,2750,2753,2755,2757,2759],{"class":250,"line":391},[248,2742,2743],{"class":367},"  const",[248,2745,2746],{"class":265}," [",[248,2748,2749],{"class":269},"existing",[248,2751,2752],{"class":265},"]",[248,2754,495],{"class":265},[248,2756,642],{"class":261},[248,2758,578],{"class":265},[248,2760,2761],{"class":269},"db\n",[248,2763,2764,2767,2770],{"class":250,"line":402},[248,2765,2766],{"class":265},"    .",[248,2768,2769],{"class":460},"select",[248,2771,464],{"class":381},[248,2773,2774,2776,2779,2781,2784],{"class":250,"line":419},[248,2775,2766],{"class":265},[248,2777,2778],{"class":460},"from",[248,2780,515],{"class":381},[248,2782,2783],{"class":269},"users",[248,2785,535],{"class":381},[248,2787,2788,2790,2793,2795,2798,2800,2803,2805,2807,2809,2812,2814,2817,2819,2821,2823,2825,2828,2830,2832,2834,2837,2839,2841,2843,2845],{"class":250,"line":433},[248,2789,2766],{"class":265},[248,2791,2792],{"class":460},"where",[248,2794,515],{"class":381},[248,2796,2797],{"class":460},"and",[248,2799,515],{"class":381},[248,2801,2802],{"class":460},"eq",[248,2804,515],{"class":381},[248,2806,2783],{"class":269},[248,2808,667],{"class":265},[248,2810,2811],{"class":269},"provider",[248,2813,273],{"class":265},[248,2815,2816],{"class":269}," input",[248,2818,667],{"class":265},[248,2820,2811],{"class":269},[248,2822,562],{"class":381},[248,2824,273],{"class":265},[248,2826,2827],{"class":460}," eq",[248,2829,515],{"class":381},[248,2831,2783],{"class":269},[248,2833,667],{"class":265},[248,2835,2836],{"class":269},"externalId",[248,2838,273],{"class":265},[248,2840,2816],{"class":269},[248,2842,667],{"class":265},[248,2844,2836],{"class":269},[248,2846,2847],{"class":381},")))\n",[248,2849,2850],{"class":250,"line":443},[248,2851,361],{"emptyLinePlaceholder":360},[248,2853,2854,2857,2859,2861,2863,2866],{"class":250,"line":449},[248,2855,2856],{"class":261},"  if",[248,2858,596],{"class":381},[248,2860,2749],{"class":269},[248,2862,605],{"class":381},[248,2864,2865],{"class":261},"return",[248,2867,2868],{"class":269}," existing\n",[248,2870,2871],{"class":250,"line":454},[248,2872,361],{"emptyLinePlaceholder":360},[248,2874,2875,2877,2879,2882,2884,2886,2888,2890],{"class":250,"line":467},[248,2876,2743],{"class":367},[248,2878,2746],{"class":265},[248,2880,2881],{"class":269},"created",[248,2883,2752],{"class":265},[248,2885,495],{"class":265},[248,2887,642],{"class":261},[248,2889,578],{"class":265},[248,2891,2761],{"class":269},[248,2893,2894,2896,2899,2901,2903],{"class":250,"line":486},[248,2895,2766],{"class":265},[248,2897,2898],{"class":460},"insert",[248,2900,515],{"class":381},[248,2902,2783],{"class":269},[248,2904,535],{"class":381},[248,2906,2907,2909,2912,2914],{"class":250,"line":503},[248,2908,2766],{"class":265},[248,2910,2911],{"class":460},"values",[248,2913,515],{"class":381},[248,2915,675],{"class":265},[248,2917,2918,2920,2922,2924,2926,2928],{"class":250,"line":538},[248,2919,681],{"class":381},[248,2921,385],{"class":265},[248,2923,2816],{"class":269},[248,2925,667],{"class":265},[248,2927,2811],{"class":269},[248,2929,693],{"class":265},[248,2931,2932,2934,2936,2938,2940,2942],{"class":250,"line":543},[248,2933,699],{"class":381},[248,2935,385],{"class":265},[248,2937,2816],{"class":269},[248,2939,667],{"class":265},[248,2941,2836],{"class":269},[248,2943,693],{"class":265},[248,2945,2946,2948,2950,2952,2954,2956],{"class":250,"line":567},[248,2947,724],{"class":381},[248,2949,385],{"class":265},[248,2951,2816],{"class":269},[248,2953,667],{"class":265},[248,2955,733],{"class":269},[248,2957,693],{"class":265},[248,2959,2960,2962,2964,2966,2968,2970],{"class":250,"line":590},[248,2961,741],{"class":381},[248,2963,385],{"class":265},[248,2965,2816],{"class":269},[248,2967,667],{"class":265},[248,2969,750],{"class":269},[248,2971,693],{"class":265},[248,2973,2974,2976,2978,2980,2982,2985],{"class":250,"line":627},[248,2975,768],{"class":381},[248,2977,385],{"class":265},[248,2979,2816],{"class":269},[248,2981,667],{"class":265},[248,2983,2984],{"class":269},"avatar",[248,2986,693],{"class":265},[248,2988,2989,2991],{"class":250,"line":632},[248,2990,785],{"class":265},[248,2992,535],{"class":381},[248,2994,2995,2997,3000],{"class":250,"line":656},[248,2996,2766],{"class":265},[248,2998,2999],{"class":460},"returning",[248,3001,464],{"class":381},[248,3003,3004],{"class":250,"line":678},[248,3005,361],{"emptyLinePlaceholder":360},[248,3007,3008,3011],{"class":250,"line":696},[248,3009,3010],{"class":261},"  return",[248,3012,3013],{"class":269}," created\n",[248,3015,3016],{"class":250,"line":721},[248,3017,446],{"class":265},[187,3019,3020,3021,3024,3025,3028],{},"For OAuth users you don't store a ",[212,3022,3023],{},"passwordHash"," - make the column nullable, or use a discriminator column (",[212,3026,3027],{},"auth_type: 'local' | 'oauth'",").",[182,3030,3032],{"id":3031},"security-notes","Security notes",[3034,3035,3037],"h3",{"id":3036},"csrf-via-state-parameter","CSRF via state parameter",[187,3039,3040,3041,3043,3044,3047],{},"The OAuth ",[212,3042,1556],{}," parameter is your CSRF protection for the callback. The simplest approach: generate a UUID, set it in an ",[212,3045,3046],{},"httpOnly"," cookie, and compare on callback. For stateless apps, sign a short-lived JWT containing a nonce and store the nonce in a cache.",[187,3049,3050,3051,3053],{},"Don't skip this. Without ",[212,3052,1556],{},", a malicious site can trick a logged-in user's browser into completing an OAuth flow against an attacker-controlled GitHub account.",[182,3055,3057],{"id":3056},"see-also","See also",[3059,3060,3061,3072,3079,3084],"ul",{},[194,3062,3063,3068,3069,3071],{},[3064,3065,3066],"a",{"href":114},[212,3067,331],{}," - ",[212,3070,232],{}," interface",[194,3073,3074,3078],{},[3064,3075,3076],{"href":132},[212,3077,1257],{}," - issuing your own session JWT after callback",[194,3080,3081,3083],{},[3064,3082,119],{"href":120}," - verify the JWT issued by the callback",[194,3085,3086,3088],{},[3064,3087,123],{"href":124}," - username\u002Fpassword login alongside OAuth",[3090,3091,3092],"style",{},"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 .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}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 .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);}",{"title":244,"searchDepth":258,"depth":258,"links":3094},[3095,3096,3097,3098,3099,3100,3103],{"id":184,"depth":258,"text":185},{"id":225,"depth":258,"text":226},{"id":1164,"depth":258,"text":199},{"id":2498,"depth":258,"text":2499},{"id":2652,"depth":258,"text":2653},{"id":3031,"depth":258,"text":3032,"children":3101},[3102],{"id":3036,"depth":308,"text":3037},{"id":3056,"depth":258,"text":3057},"GitHub login as both a server-side redirect flow and an AuthProvider that verifies external access tokens.","md",{},{"title":127,"description":3104},"PZ2J5K_eULaeFzcqjCMMVYyn2rs7PrzXtm_msfv1iTU",[3110,3112],{"title":123,"path":124,"stem":125,"description":3111,"children":-1},"Username\u002Fpassword login as an AuthProvider - argon2 password verification, Zod body validation inside the provider.",{"title":131,"path":132,"stem":133,"description":3113,"children":-1},"Injectable JwtService for signing and verifying JSON Web Tokens - a thin wrapper around jose.",1778575278127]