@@ -2,11 +2,14 @@ package httpactionsserver
22
33import (
44 "crypto/tls"
5+ "encoding/base64"
56 "encoding/json"
7+ "errors"
68 "fmt"
79 "io"
810 "net/http"
911 "slices"
12+ "strings"
1013
1114 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1215 "sigs.k8s.io/controller-runtime/pkg/client"
@@ -94,6 +97,7 @@ func (s *Server) Start() error {
9497 mux := http .NewServeMux ()
9598 mux .HandleFunc ("/v1/actions/create-user-account" , s .createUserAccountHandler )
9699 mux .HandleFunc ("/v1/actions/customize-jwt" , s .customizeJwtHandler )
100+ mux .HandleFunc ("/v1/actions/idp-intent-succeeded" , s .idpIntentSucceededHandler )
97101
98102 srv := & http.Server {
99103 Addr : s .config .Addr ,
@@ -218,6 +222,33 @@ type CustomizeJwtHandlerRequest struct {
218222 } `json:"user"`
219223}
220224
225+ // IdpIntentSucceededRequest models JUST the fields we need from an idpintent.succeeded Zitadel payload.
226+ type IdpIntentSucceededRequest struct {
227+ EventType string `json:"event_type"`
228+ UserID string `json:"userId"`
229+ EventPayload struct {
230+ IDPUser string `json:"idpUser"`
231+ IDPIdToken string `json:"idpIdToken,omitempty"`
232+ UserID string `json:"userId,omitempty"`
233+ } `json:"event_payload"`
234+ }
235+
236+ // IDPUserData represents the decoded idpUser data from identity providers
237+ type IDPUserData struct {
238+ User struct {
239+ Email string `json:"email"`
240+ EmailVerified bool `json:"email_verified"`
241+ FamilyName string `json:"family_name"`
242+ GivenName string `json:"given_name"`
243+ Name string `json:"name"`
244+ Picture string `json:"picture"`
245+ Sub string `json:"sub"`
246+ // GitHub specific fields
247+ AvatarURL string `json:"avatar_url,omitempty"`
248+ Login string `json:"login,omitempty"`
249+ } `json:"User"`
250+ }
251+
221252type Metadata struct {
222253 Key string `json:"key"`
223254 Value []byte `json:"value"`
@@ -298,3 +329,105 @@ func (s *Server) customizeJwtHandler(w http.ResponseWriter, r *http.Request) {
298329 }
299330 log .Info ("Successfully wrote response" )
300331}
332+
333+ // idpIntentSucceededHandler processes the idpintent.succeeded action to capture the IDP provider and avatar URL.
334+ func (s * Server ) idpIntentSucceededHandler (w http.ResponseWriter , r * http.Request ) {
335+ log := logf .FromContext (r .Context ()).WithName ("idpIntentSucceededHandler" )
336+
337+ bodyBytes , err := io .ReadAll (r .Body )
338+ if err != nil {
339+ log .Error (err , "Failed to read request body" )
340+ http .Error (w , "failed to read request body" , http .StatusBadRequest )
341+ return
342+ }
343+
344+ if err := s .validateSignature (bodyBytes , r .Header .Get ("Zitadel-Signature" ), s .config .SigningKey ); err != nil {
345+ log .Error (err , "Signature validation failed" )
346+ http .Error (w , fmt .Sprintf ("signature validation failed: %v" , err ), http .StatusUnauthorized )
347+ return
348+ }
349+
350+ var req IdpIntentSucceededRequest
351+ if err := json .Unmarshal (bodyBytes , & req ); err != nil {
352+ log .Error (err , "Failed to parse body" )
353+ http .Error (w , "invalid body" , http .StatusBadRequest )
354+ return
355+ }
356+
357+ if req .EventType != "idpintent.succeeded" || req .EventPayload .IDPUser == "" {
358+ log .Error (nil , "Unexpected event type" , "eventType" , req .EventType )
359+ http .Error (w , "unexpected event" , http .StatusBadRequest )
360+ return
361+ }
362+
363+ // Decode idpUser JSON
364+ raw , err := base64 .StdEncoding .DecodeString (req .EventPayload .IDPUser )
365+ if err != nil {
366+ log .Error (err , "Failed to decode idpUser" )
367+ http .Error (w , "invalid idpUser" , http .StatusBadRequest )
368+ return
369+ }
370+
371+ // Detect provider & avatar generically (supports Google & GitHub)
372+ idpProvider , avatarURL , perr := parseIDPUserData (raw )
373+ if perr != nil {
374+ log .Error (perr , "Failed to parse idpUser JSON" )
375+ http .Error (w , "invalid idpUser data" , http .StatusBadRequest )
376+ return
377+ }
378+
379+ // Update user avatar URL and last login provider
380+ ctx := r .Context ()
381+ current := & iammiloapiscomv1alpha1.User {}
382+ if err := s .k8sClient .Get (ctx , client.ObjectKey {Name : req .EventPayload .UserID }, current ); err != nil {
383+ log .Error (err , "Failed to get User resource" , "userId" , req .EventPayload .UserID )
384+ http .Error (w , "user not found" , http .StatusNotFound )
385+ return
386+ }
387+ original := current .DeepCopy ()
388+
389+ current .Status .AvatarURL = avatarURL
390+ current .Status .LastLoginProvider = idpProvider
391+
392+ fieldManagerName := "idp-intent-succeeded"
393+ if err := s .k8sClient .Status ().Patch (ctx , current , client .MergeFrom (original ), client .FieldOwner (fieldManagerName )); err != nil {
394+ log .Error (err , "Failed to patch User status" )
395+ http .Error (w , "failed to patch user" , http .StatusInternalServerError )
396+ return
397+ }
398+
399+ log .Info ("Processed idpintent.succeeded" , "idpProvider" , idpProvider , "avatarURL" , avatarURL , "userId" , req .EventPayload .UserID )
400+ w .WriteHeader (http .StatusOK )
401+ _ , _ = w .Write ([]byte ("success" ))
402+ }
403+
404+ // parseIDPUserData inspects the raw json of idpUser (base64 decoded) and
405+ // returns provider and avatar URL best effort without relying on rigid structs.
406+ func parseIDPUserData (raw []byte ) (iammiloapiscomv1alpha1.AuthProvider , string , error ) {
407+ var m map [string ]interface {}
408+ if err := json .Unmarshal (raw , & m ); err != nil {
409+ return "" , "" , err
410+ }
411+
412+ // Google format: {"User": {"picture": "..."}}
413+ if user , ok := m ["User" ].(map [string ]interface {}); ok {
414+ if pic , ok := user ["picture" ].(string ); ok && pic != "" {
415+ return iammiloapiscomv1alpha1 .AuthProviderGoogle , pic , nil
416+ }
417+ }
418+
419+ // GitHub format: top-level avatar_url
420+ if avatar , ok := m ["avatar_url" ].(string ); ok && avatar != "" {
421+ return iammiloapiscomv1alpha1 .AuthProviderGitHub , avatar , nil
422+ }
423+
424+ // Also google picture could be top-level
425+ if pic , ok := m ["picture" ].(string ); ok && pic != "" {
426+ if strings .Contains (pic , "googleusercontent.com" ) {
427+ return iammiloapiscomv1alpha1 .AuthProviderGoogle , pic , nil
428+ }
429+
430+ }
431+
432+ return "" , "" , errors .New ("unknown idp provider" )
433+ }
0 commit comments