diff --git a/auth/logo.go b/auth/logo.go index 924511c9..e9a71e0b 100644 --- a/auth/logo.go +++ b/auth/logo.go @@ -1,6 +1,7 @@ package main import ( + "context" "crypto/md5" "encoding/hex" "errors" @@ -81,6 +82,37 @@ func (h *Handler) writeManualLogo(id uuid.UUID, data []byte) error { return nil } +func (h *Handler) downloadLogo(ctx context.Context, id uuid.UUID, logoURL string) error { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, logoURL, nil) + if err != nil { + return err + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected logo response status: %d", resp.StatusCode) + } + + data, err := io.ReadAll(io.LimitReader(resp.Body, maxLogoSize+1)) + if err != nil { + return err + } + if len(data) > maxLogoSize { + return fmt.Errorf("logo file too large") + } + + if !slices.Contains(allowedLogoTypes, http.DetectContentType(data)) { + return fmt.Errorf("unsupported logo content type") + } + + return h.writeManualLogo(id, data) +} + func (h *Handler) streamGravatar(c *echo.Context, email string) error { hash := md5.Sum([]byte(strings.TrimSpace(strings.ToLower(email)))) url := fmt.Sprintf("https://www.gravatar.com/avatar/%s?d=404", hex.EncodeToString(hash[:])) diff --git a/auth/oidc.go b/auth/oidc.go index a6b6aa17..c22a3c4e 100644 --- a/auth/oidc.go +++ b/auth/oidc.go @@ -265,6 +265,9 @@ type RawProfile struct { Uid *string `json:"uid"` Id *string `json:"id"` Guid *string `json:"guid"` + Picture *string `json:"picture"` + AvatarURL *string `json:"avatar_url"` + Avatar *string `json:"avatar"` Username *string `json:"username"` PreferredUsername *string `json:"preferred_username"` Login *string `json:"login"` @@ -276,9 +279,10 @@ type RawProfile struct { } type Profile struct { - Sub string `json:"sub,omitempty"` - Username string `json:"username,omitempty"` - Email string `json:"email,omitempty"` + Sub string `json:"sub,omitempty"` + Username string `json:"username,omitempty"` + Email string `json:"email,omitempty"` + PictureURL string `json:"pictureUrl,omitempty"` } func (h *Handler) fetchOidcProfile(c *echo.Context, provider OidcProviderConfig, accessToken string) (Profile, error) { @@ -316,6 +320,25 @@ func (h *Handler) fetchOidcProfile(c *echo.Context, provider OidcProviderConfig, if sub == nil { return Profile{}, echo.NewHTTPError(http.StatusInternalServerError, "Missing sub or username") } + picture := cmp.Or(profile.Picture, profile.AvatarURL, profile.Avatar) + if picture == nil { + if rawPicture, ok := profile.Account["picture"]; ok { + if pictureURL, ok := rawPicture.(string); ok { + picture = &pictureURL + } + } + } + if picture == nil { + if rawPicture, ok := profile.User["picture"]; ok { + if pictureURL, ok := rawPicture.(string); ok { + picture = &pictureURL + } + } + } + pictureURL := "" + if picture != nil { + pictureURL = *picture + } return Profile{ Sub: *sub, Username: *cmp.Or( @@ -331,6 +354,7 @@ func (h *Handler) fetchOidcProfile(c *echo.Context, provider OidcProviderConfig, *sub, provider, ))), + PictureURL: pictureURL, }, nil } @@ -421,6 +445,24 @@ func (h *Handler) CreateUserByOidc( if ErrIs(err, pgerrcode.UniqueViolation) { return echo.NewHTTPError(http.StatusConflict, "A user already exists with the same username or email. If this is you, login via username and then link your account.") } + if err != nil { + return err + } + + if profile.PictureURL != "" { + if err := h.downloadLogo(ctx, user.Id, profile.PictureURL); err != nil { + slog.Warn( + "Could not download OIDC profile picture", + "provider", + provider.Id, + "sub", + profile.Sub, + "err", + err, + ) + } + } + } var expireAt *time.Time