@@ -22,6 +22,7 @@ import { ConnectError, create } from "@zitadel/client";
2222import { AutoLinkingOption } from "@zitadel/proto/zitadel/idp/v2/idp_pb" ;
2323import { OrganizationSchema } from "@zitadel/proto/zitadel/object/v2/object_pb" ;
2424import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb" ;
25+ import { SetHumanEmailSchema } from "@zitadel/proto/zitadel/user/v2/email_pb" ;
2526import {
2627 AddHumanUserRequest ,
2728 AddHumanUserRequestSchema ,
@@ -66,6 +67,47 @@ async function resolveOrganizationForUser({
6667 return undefined ;
6768}
6869
70+ type GithubEmailInfo = { email : string ; verified : boolean } | undefined ;
71+
72+ // getGithunPrimaryEmail fetches the primary email, even if the user has hidden it
73+ // user:email scope is required for private emails
74+ async function getGithubPrimaryEmail ( token : string ) : Promise < GithubEmailInfo > {
75+ const fetchEmails = async ( ) => {
76+ return fetch ( "https://api.github.com/user/emails" , {
77+ headers : {
78+ Accept : "application/vnd.github+json" ,
79+ Authorization : `Bearer ${ token } ` ,
80+ "X-GitHub-Api-Version" : "2022-11-28" ,
81+ } ,
82+ cache : "no-store" ,
83+ } ) ;
84+ } ;
85+
86+ try {
87+ let res = await fetchEmails ( ) ;
88+ if ( ! res . ok ) {
89+ console . warn ( "GitHub email fetch failed" , res . status ) ;
90+ return undefined ;
91+ }
92+
93+ const data : Array < { email : string ; primary : boolean ; verified : boolean } > =
94+ await res . json ( ) ;
95+
96+ const primary =
97+ data . find ( ( e ) => e . primary && e . verified ) ||
98+ data . find ( ( e ) => e . primary ) ||
99+ data [ 0 ] ;
100+ if ( primary ) {
101+ return { email : primary . email , verified : primary . verified } ;
102+ }
103+
104+ return undefined ;
105+ } catch ( err ) {
106+ console . warn ( "GitHub email fetch error" , err ) ;
107+ return undefined ;
108+ }
109+ }
110+
69111export const metadata = generateRouteMetadata ( "idp" ) ;
70112export default async function Page ( props : {
71113 searchParams : Promise < Record < string | number | symbol , string | undefined > > ;
@@ -101,6 +143,57 @@ export default async function Page(props: {
101143 const { idpInformation, userId } = intent ;
102144 let { addHumanUser } = intent ;
103145
146+ // ensure mandatory profile fields are populated to satisfy ZITADEL validation, as GitHub does not provide givenName
147+ // and familyName
148+ if ( addHumanUser ) {
149+ const profile : any = addHumanUser . profile ?? { } ;
150+ let { givenName, familyName } = profile ;
151+
152+ const fallback = idpInformation ?. userName ?? "user" ;
153+
154+ if ( ! givenName || givenName . trim ( ) . length === 0 ) {
155+ givenName = fallback . slice ( 0 , 200 ) ;
156+ }
157+
158+ if ( ! familyName || familyName . trim ( ) . length === 0 ) {
159+ familyName = fallback . slice ( 0 , 200 ) ;
160+ }
161+
162+ addHumanUser = {
163+ ...addHumanUser ,
164+ profile : {
165+ ...profile ,
166+ givenName,
167+ familyName,
168+ } ,
169+ } as typeof addHumanUser ;
170+
171+ // ensure email exists as well (GitHub may not return an address if user hides it)
172+ if (
173+ ! addHumanUser . email ?. email ||
174+ addHumanUser . email . email . trim ( ) . length === 0
175+ ) {
176+ let newEmailInfo : GithubEmailInfo ;
177+ const githubAccessToken : string | undefined = ( idpInformation as any )
178+ ?. access ?. value ?. accessToken ;
179+ if ( provider === "github" ) {
180+ if ( githubAccessToken ) {
181+ newEmailInfo = await getGithubPrimaryEmail ( githubAccessToken ) ;
182+ }
183+ }
184+ let finalEmail = newEmailInfo ?. email ?? `${ fallback } @idp.local` ;
185+ const isVerified = newEmailInfo ?. verified ?? false ;
186+
187+ addHumanUser = {
188+ ...addHumanUser ,
189+ email : create ( SetHumanEmailSchema , {
190+ email : finalEmail . slice ( 0 , 200 ) ,
191+ verification : { case : "isVerified" , value : isVerified } ,
192+ } ) ,
193+ } ;
194+ }
195+ }
196+
104197 if ( ! idpInformation ) {
105198 return loginFailed ( "IDP information missing" ) ;
106199 }
0 commit comments