Skip to content

userinfo endpoint does not return email claim despite email scope #5374

@ivantohelpyou

Description

@ivantohelpyou

Bug Report: MAS userinfo endpoint does not return email claim

Summary

Matrix Authentication Service (MAS) v1.8.0 userinfo endpoint does not return the email claim even when:

  • The email scope is requested and granted
  • The user has a verified primary email address

Environment

  • MAS Version: v1.8.0 (mas-cli v1.8.0)
  • Repository: element-hq/matrix-authentication-service (latest main branch as of 2025-12-23)
  • Deployment: Render.com (Docker container)
  • Client: Authentik 2025.10.3 as OAuth source

Steps to Reproduce

  1. Configure a third-party OIDC client (e.g., Authentik) with MAS
  2. Request scopes: openid email
  3. User authenticates and grants consent
  4. Client exchanges code for token
  5. Client fetches userinfo endpoint: GET /oauth2/userinfo

Expected Behavior

Per MAS documentation:

The email scope... adds the user's email address to the id_token and to the claims returned by the userinfo endpoint.

Per OpenID Connect Core 1.0 spec, Section 5.4:

The email scope requests access to the email and email_verified Claims.

Expected userinfo response:

{
  "sub": "01KD48QPHGZ4Z6A2S1HEP1DMX6",
  "username": "ivantohelpyou",
  "email": "[email protected]",
  "email_verified": true
}

Actual Behavior

Userinfo response only contains:

{
  "sub": "01KD48QPHGZ4Z6A2S1HEP1DMX6",
  "username": "ivantohelpyou"
}

No email field is present.

Verification

Confirmed via database queries that:

  1. Session has email scope:
SELECT scope_list FROM oauth2_sessions ORDER BY created_at DESC LIMIT 1;
-- Result: {email,openid}
  1. User has confirmed primary email:
SELECT u.username, e.email, e.confirmed_at
FROM users u
JOIN user_emails e ON u.primary_user_email_id = e.user_email_id
WHERE u.username = 'ivantohelpyou';
-- Result: ivantohelpyou | [email protected] | 2025-12-23 07:33:19.063857+00

Source Code Analysis

Reviewed crates/handlers/src/oauth2/userinfo.rs (lines 34-37 in latest main):

#[derive(Serialize)]
struct UserInfo {
    sub: String,
    username: String,
}

Root Cause: The UserInfo struct only contains sub and username fields. The email and email_verified fields are completely missing from the struct definition.

There is no code in the handler to:

  1. Check if the email scope was granted
  2. Retrieve the user's primary email from the database
  3. Include email claims in the response

This appears to be an incomplete implementation rather than a configuration issue.

Impact

Severity: High - Breaks OIDC compliance and third-party integrations

  • Third-party applications (Vikunja, Gitea, Authentik, etc.) that rely on the userinfo endpoint to get user email addresses cannot function properly with MAS as the identity provider
  • MAS claims to support the email scope in documentation and discovery endpoint, but does not actually implement it
  • Forces manual workarounds or prevents using MAS as an identity provider for many applications
  • Violates OpenID Connect Core 1.0 specification for the email scope

Workaround

Manually set the email on the downstream IdP (Authentik) user after first login, bypassing the MAS userinfo endpoint.

Additional Context

  • MAS discovery endpoint (/.well-known/openid-configuration) correctly lists email in scopes_supported
  • MAS consent screen correctly displays "email" permission being requested
  • The OAuth flow itself works correctly - the issue is purely in the userinfo handler implementation
  • The database contains all necessary data (confirmed primary email exists and is verified)
  • This bug exists in both the archived matrix-org repository and the current element-hq repository

Proposed Fix

The UserInfo struct in crates/handlers/src/oauth2/userinfo.rs should be updated to:

#[derive(Serialize)]
struct UserInfo {
    sub: String,
    username: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    email: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    email_verified: Option<bool>,
}

And the handler should:

  1. Check if the session has the email scope
  2. If yes, fetch the user's primary email from the database
  3. Include email and email_verified in the response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions