Skip to content

Feat: Pull users' Profile Picture from the OIDC claims#2704

Open
Guibi1 wants to merge 1 commit intoopencloud-eu:mainfrom
Guibi1:oidc-profile-picture
Open

Feat: Pull users' Profile Picture from the OIDC claims#2704
Guibi1 wants to merge 1 commit intoopencloud-eu:mainfrom
Guibi1:oidc-profile-picture

Conversation

@Guibi1
Copy link
Copy Markdown

@Guibi1 Guibi1 commented May 2, 2026

Description

This PR adds two settings:

  • PROXY_OIDC_PROFILE_PICTURE_CLAIM: Sets which oidc claim to use as the profile picture.
  • PROXY_OIDC_PROFILE_PICTURE_DISABLE_LOCAL_CHANGES: Stops the user from updating their profile pictures by disabling the graph endpoint.

Related Issue

Motivation and Context

See linked issue.

How Has This Been Tested?

  • manual testing

Screenshots (if appropriate):

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Technical debt
  • Tests only (no source changes)

Checklist:

  • Code changes
  • Unit tests added
  • Acceptance tests added
  • Documentation added

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented May 2, 2026

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds support in the proxy service to (a) sync a user’s profile photo from a configurable OIDC claim on login, and (b) optionally forbid local profile-photo updates via the Graph “me/photo/$value” endpoint for OIDC-authenticated requests.

Changes:

  • Add PROXY_OIDC_PROFILE_PICTURE_CLAIM to fetch a profile-photo URL from OIDC claims and sync it on new sessions.
  • Add PROXY_OIDC_PROFILE_PICTURE_DISABLE_LOCAL_CHANGES to block PUT/PATCH/DELETE to /graph/v1.0/me/photo/$value for OIDC-authenticated requests.
  • Introduce proxy wiring for internal Graph calls (service discovery + separate backend HTTP client).

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
services/proxy/pkg/middleware/options.go Extends middleware options with backend HTTP client, internal service selector, and OIDC profile-picture config.
services/proxy/pkg/middleware/account_resolver.go Implements profile-picture fetch + Graph update on new session; optionally blocks local photo mutations.
services/proxy/pkg/config/defaults/defaultconfig.go Adds default values for the new oidc_profile_picture config section.
services/proxy/pkg/config/config.go Introduces OIDCProfilePicture config and env vars for claim + local-change disable.
services/proxy/pkg/command/server.go Wires new config into middleware and creates a dedicated backend HTTP client for internal calls.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +190 to +206
parsedURL, err := url.Parse(pictureURL)
if err != nil {
return fmt.Errorf("invalid profile picture URL: %w", err)
}
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
return fmt.Errorf("unsupported profile picture URL scheme: %s", parsedURL.Scheme)
}
if parsedURL.Host == "" {
return fmt.Errorf("profile picture URL is missing a host")
}

authHeader := ""
if req != nil {
authHeader = req.Header.Get("Authorization")
}

photo, err := m.fetchProfilePicture(ctx, parsedURL, authHeader)
Comment on lines +258 to +266
func (m accountResolver) shouldAttachOIDCToken(pictureURL *url.URL) bool {
if m.oidcIssuer == "" || pictureURL == nil {
return false
}
issuerURL, err := url.Parse(m.oidcIssuer)
if err != nil || issuerURL.Host == "" {
return false
}
return strings.EqualFold(issuerURL.Host, pictureURL.Host)
Comment on lines +340 to +344
if m.disableLocalProfilePictureChange && claims != nil && m.isProfilePhotoMutation(req) {
m.logger.Debug().Str("path", req.URL.Path).Msg("profile photo updates disabled for OIDC users")
w.WriteHeader(http.StatusForbidden)
return
}
Comment on lines +425 to +429
if m.profilePictureClaim != "" && oidc.NewSessionFlagFromContext(ctx) {
if err := m.syncProfilePicture(ctx, req, user, token, claims); err != nil {
m.logger.Warn().Err(err).Str("userid", user.GetId().GetOpaqueId()).Msg("Failed to sync profile picture from OIDC claim")
}
}
Comment on lines +174 to +175
Claim string `yaml:"claim" env:"PROXY_OIDC_PROFILE_PICTURE_CLAIM" desc:"The name of the OIDC claim that holds a URL to the user's profile picture. When set, the profile picture will be synced on login."`
DisableLocalChanges bool `yaml:"disable_local_changes" env:"PROXY_OIDC_PROFILE_PICTURE_DISABLE_LOCAL_CHANGES" desc:"When set, users authenticated via OIDC cannot change their profile picture locally (PUT/PATCH/DELETE on /graph/v1.0/me/photo/$value)."`
Comment on lines +296 to +301
backendHTTPClient := &http.Client{
Transport: &http.Transport{
TLSClientConfig: backendTLSConfig,
},
Timeout: time.Second * 10,
}
@dschmidt
Copy link
Copy Markdown
Contributor

dschmidt commented May 8, 2026

First of all: Thanks for the effort, @Guibi1!

I think this might entangle two separate concerns too tightly:

  1. Use profile pictures from oidc claims
  2. Disable profile picture updates for the user

I have a feeling the proxy is not the right place to handle the second concern. To me the route handling is crossing service domains in an unfortunate way.
IMHO it's the graph service which should be responsible for ultimately deciding this - and the frontend should be made aware of this, to hide the upload option etc.

When moving this responsibility to the graph service, this leaves us with the question of how to update the profile picture from the proxy (handling it at all there seems to be the right call as the proxy deals with all claims anyway), as the Graph HTTP Api now rejects updates.

Two possibilities from my view:

  1. Emit an event in the proxy and let the graph service consume it. This is rather easy and auditable, but it loses user auth. Again two possibilities:
    1.1 attach the image data to the event, limited by event bus config and feels a bit awkward
    1.2 have no user auth for fetching the image (is this really needed in reality? I haven't seen this in the wild anyway)
  2. Add a gRPC interface to the graph service: less auditable, but we can pass on user auth or bigger image data

It might be easier to discuss separate concerns in separate PRs - unfortunately they are heavily influencing each other...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FR] Tell Opencloud OIDC where to find the Profile Picture

3 participants