Skip to content

417 hwioauthbundle implementation - #65

Merged
tijsverkoyen merged 13 commits into
sumocoders:HWIOAuthBundle-implementationfrom
iliaskhat:417-hwioauthbundle-implementation
May 20, 2026
Merged

tijsverkoyen merged 13 commits into
sumocoders:HWIOAuthBundle-implementationfrom
iliaskhat:417-hwioauthbundle-implementation

Conversation

@iliaskhat

@iliaskhat iliaskhat commented May 12, 2026 •

Copy link
Copy Markdown

Added and documented use of hwi/oauthbundle

Summary by Sourcery

Integrate Azure-based SSO using HWIOAuthBundle and expose it as an optional authentication path alongside existing login.

New Features:

  • Add Azure Entra ID and SumoCoders SSO support via HWIOAuthBundle, including OAuth resource owners, routes, and security firewall configuration.
  • Expose Microsoft and SumoCoders sign-in options on the login page when corresponding environment variables are configured.

Enhancements:

  • Extend the User entity with an optional Azure object ID, helper methods for Azure-linked accounts, and role synchronization from Azure.
  • Dispatch an Azure login domain event on successful OAuth logins to allow downstream listeners to react.
  • Update documentation to describe Azure app registration, role configuration, environment variables, and migration steps for enabling Azure SSO.
  • Adjust migration path to reference the project-level migrations directory instead of the src migrations directory.

Build:

  • Add HWIOAuthBundle as a project dependency and register the bundle in the Symfony configuration.

Tests:

  • Add unit tests for Azure-backed users and for the Azure OAuth user provider, covering auto-provisioning, account linking, role syncing, and login events.

@iliaskhat iliaskhat self-assigned this May 12, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • The azure_object_id column is nullable but has a unique index in the migration; this can cause issues on some databases (e.g. PostgreSQL doesn’t allow multiple NULLs in a unique index), so consider using a partial/index-where non-null or enforcing uniqueness at the application level instead.
  • Injecting AZURE_CLIENT_ID and SUMOCODERS_CLIENT_ID as required constructor arguments in LoginController means the container will fail if these env vars are absent; if you want Azure/ Sumocoders login to be truly optional, consider using default env values (e.g. env(default::string: '')) or making the arguments nullable.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `azure_object_id` column is nullable but has a unique index in the migration; this can cause issues on some databases (e.g. PostgreSQL doesn’t allow multiple NULLs in a unique index), so consider using a partial/index-where non-null or enforcing uniqueness at the application level instead.
- Injecting `AZURE_CLIENT_ID` and `SUMOCODERS_CLIENT_ID` as required constructor arguments in `LoginController` means the container will fail if these env vars are absent; if you want Azure/ Sumocoders login to be truly optional, consider using default env values (e.g. `env(default::string: '')`) or making the arguments nullable.

## Individual Comments

### Comment 1
<location path="src/Security/OAuth/AzureUserProvider.php" line_range="116-117" />
<code_context>
+
+        $allowedDomain = ltrim($this->allowedEmailDomain, '@');
+        if (!str_ends_with(strtolower($email), '@' . strtolower($allowedDomain))) {
+            throw new AccessDeniedException(
+                sprintf('Email domain of "%s" is not allowed to log in via Azure.', $email)
+            );
+        }
</code_context>
<issue_to_address>
**issue (bug_risk):** The email-domain error message won't match the existing translation key, so it likely won't be localized.

The NL translation you added is tied to the key `"Email domain is not allowed to log in via Azure."`, but the thrown message is `"Email domain of "%s" is not allowed to log in via Azure."`. Because these differ, this message won’t be localized. To make it translatable, either reuse the existing key and log the email separately, or introduce a translation key with a parameter instead of hardcoding the `sprintf` string.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/Security/OAuth/AzureUserProvider.php Outdated
@jonasdekeukelaere
jonasdekeukelaere marked this pull request as draft May 13, 2026 05:39
Comment thread config/packages/hwi_oauth.yaml Outdated
Comment thread config/routes/hwi_oauth_routing.yaml Outdated
resource: "@HWIOAuthBundle/Resources/config/routing/redirect.php"
prefix: /connect

hwi_oauth_connect:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why?

@iliaskhat iliaskhat May 13, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

deze route is niet nodig, word gebruikt om meerdere OAuth providers (google, fb,..) aan 1 account te connecteren.

Comment thread migrations/Version20260512135528.php Outdated
Comment thread config/services.yaml Outdated
Comment thread src/Security/OAuth/AzureUserProvider.php Outdated
Comment thread src/Security/OAuth/AzureUserProvider.php Outdated
Comment thread README.md Outdated
Comment thread README.md Outdated
Comment thread README.md Outdated
Comment thread README.md Outdated
Comment thread src/Security/OAuth/AzureUserProvider.php Outdated
Comment thread src/Security/OAuth/AzureUserProvider.php Outdated
Comment thread migrations/Version20260512135528.php Outdated
Comment thread templates/user/login.html.twig Outdated
{{ form_end(form) }}

<p class="align-self-center">{{ 'New to x?'|trans }} <a href="{{ url('user_register') }}">{{ 'Sign up'|trans }}</a></p>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ik vraag me af of we niet beter de form vervangen.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

fix: d6e4293

Comment thread .env Outdated
@sumocoders sumocoders deleted a comment from sourcery-ai Bot May 13, 2026
Comment thread config/services.yaml Outdated
Comment thread src/Security/OAuth/AzureUserProvider.php Outdated
Comment thread src/Security/OAuth/AzureUserProvider.php
@tijsverkoyen
tijsverkoyen changed the base branch from main to HWIOAuthBundle-implementation May 20, 2026 06:55
@tijsverkoyen
tijsverkoyen marked this pull request as ready for review May 20, 2026 07:09
@tijsverkoyen
tijsverkoyen merged commit 44ea2ad into sumocoders:HWIOAuthBundle-implementation May 20, 2026
@sourcery-ai

sourcery-ai Bot commented May 20, 2026

Copy link
Copy Markdown

Reviewer's Guide

Integrates optional Azure Entra ID SSO via HWIOAuthBundle, wiring it into the existing Symfony security firewall, extending the User model for Azure identities, exposing Microsoft/SumoCoders login options in the UI, and covering the behavior with tests and documentation.

Sequence diagram for Azure SSO login via HWIOAuthBundle

sequenceDiagram
    actor EndUser
    participant Browser
    participant HWIOAuth as HWIOAuthBundle
    participant Azure
    participant AzureUserProvider
    participant UserRepository
    participant EventDispatcher

    EndUser->>Browser: Click Azure login button
    Browser->>HWIOAuth: GET /login/check-azure
    HWIOAuth->>Azure: Redirect to Azure authorize endpoint
    Azure-->>HWIOAuth: Authorization code
    HWIOAuth->>Azure: Exchange code for tokens
    Azure-->>HWIOAuth: ID token and profile data
    HWIOAuth->>AzureUserProvider: loadUserByOAuthUserResponse(response)

    alt Existing Azure user (matched by azureObjectId)
        AzureUserProvider->>UserRepository: findOneBy(azureObjectId)
        UserRepository-->>AzureUserProvider: User
        AzureUserProvider->>User: syncAzureRoles(roles)
        AzureUserProvider->>User: update(email, roles)
        AzureUserProvider->>UserRepository: save()
    else Existing local user (matched by email)
        AzureUserProvider->>UserRepository: findOneBy(email)
        UserRepository-->>AzureUserProvider: User
        AzureUserProvider->>User: linkAzureAccount(oid)
        AzureUserProvider->>User: syncAzureRoles(roles)
        AzureUserProvider->>UserRepository: save()
    else No existing user (auto provision)
        AzureUserProvider->>AzureUserProvider: User::createFromAzureProfile(email, oid, roles)
        AzureUserProvider->>UserRepository: add(User)
    end

    AzureUserProvider->>EventDispatcher: dispatch(AzureLoginEvent)
    AzureUserProvider-->>HWIOAuth: User
    HWIOAuth-->>Browser: Authenticated session cookie
Loading

Entity relationship diagram for User azure_object_id field

erDiagram
    USER {
        int id PK
        string email
        string azure_object_id "nullable, unique"
    }
Loading

File-Level Changes

Change Details Files
Extend the User entity and database schema to support Azure-linked identities and role syncing.
  • Add nullable unique azureObjectId field to User and map it via Doctrine ORM
  • Introduce createFromAzureProfile factory to auto-provision enabled/confirmed Azure users with roles
  • Add helpers to link/unlink Azure accounts, detect Azure users, expose Azure object ID, and sync roles from Azure
  • Create a Doctrine migration to add the azure_object_id column and unique index
src/Entity/User/User.php
migrations/Version20260512135528.php
tests/Entity/User/UserTest.php
Integrate HWIOAuthBundle for Azure and SumoCoders SSO and plug it into the security firewall and routing.
  • Add hwi/oauth-bundle dependency and register HWIOAuthBundle
  • Configure Azure and SumoCoders OAuth resource owners using env-based credentials
  • Wire HWIOAuth oauth block into main firewall with AzureUserProvider and mark callback/connect routes as PUBLIC_ACCESS
  • Register HWIOAuth routing for /connect and /login prefixes
composer.json
composer.lock
symfony.lock
config/bundles.php
config/packages/hwi_oauth.yaml
config/packages/security.yaml
config/routes/hwi_oauth_routing.yaml
Implement AzureUserProvider to resolve/link/provision users from Azure responses, sync roles, and dispatch login events.
  • Implement OAuthAwareUserProviderInterface and UserProviderInterface using UserRepository and EventDispatcher
  • Resolve users by azureObjectId first, then email, linking accounts and updating email when needed
  • Auto-provision new users from Azure profile (enabled, confirmed, roles synced) and persist via repository
  • Sync roles from Azure on each login and dispatch AzureLoginEvent on successful authentication
  • Implement refreshUser/loadUserByIdentifier with proper error handling
src/Security/OAuth/AzureUserProvider.php
src/Event/User/AzureLoginEvent.php
tests/Security/OAuth/AzureUserProviderTest.php
Expose conditional Azure and SumoCoders login options in the existing login flow using environment-driven toggles.
  • Inject AZURE_CLIENT_ID and SUMOCODERS_CLIENT_ID into LoginController via Autowire attributes
  • Pass boolean flags to the login template to enable/disable SSO buttons based on configured client IDs
  • Update login template to show Microsoft and SumoCoders sign-in buttons via HWIOAuth redirect routes when enabled and otherwise show the traditional form
src/Controller/User/LoginController.php
templates/user/login.html.twig
Document the Azure and SumoCoders SSO setup and add translation strings for the new login texts and error message.
  • Add detailed Azure SSO setup instructions, env var configuration, and SumoCoders tenant notes to README
  • Adjust migration path in README to point to migrations/ directory
  • Add Dutch translations for Microsoft/SumoCoders sign-in labels and an Azure domain restriction error message
README.md
translations/messages.nl.yaml

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • The production AzureUserProvider no longer matches the behaviour exercised in AzureUserProviderTest (e.g. constructor signature and allowed-domain logic), so it would be good to align the implementation with the tested contract or update the tests to reflect the intended design.
  • syncAzureRoles() currently overwrites the stored roles array, which can drop implicit roles like ROLE_USER; consider normalizing the input so base roles are preserved while still reflecting Azure role changes.
  • The login template hides the email/password form entirely when any SSO option is enabled, whereas the README suggests SSO can be used alongside local accounts; consider always rendering the traditional login form and showing the SSO buttons in addition to it.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The production AzureUserProvider no longer matches the behaviour exercised in AzureUserProviderTest (e.g. constructor signature and allowed-domain logic), so it would be good to align the implementation with the tested contract or update the tests to reflect the intended design.
- syncAzureRoles() currently overwrites the stored roles array, which can drop implicit roles like ROLE_USER; consider normalizing the input so base roles are preserved while still reflecting Azure role changes.
- The login template hides the email/password form entirely when any SSO option is enabled, whereas the README suggests SSO can be used alongside local accounts; consider always rendering the traditional login form and showing the SSO buttons in addition to it.

## Individual Comments

### Comment 1
<location path="templates/user/login.html.twig" line_range="10-8" />
<code_context>
         </div>
     {% endif %}

+    {% if azure_login_enabled or sumocoders_login_enabled %}
+        <div class="d-grid gap-3">
+        {% if azure_login_enabled %}
+            <a href="{{ path('hwi_oauth_service_redirect', { service: 'azure' }) }}" class="btn btn-primary btn-lg">
+                <i class="bi bi-microsoft me-2"></i>
+                {{ 'Sign in with Microsoft'|trans }}
+            </a>
+        {% endif %}
+        {% if sumocoders_login_enabled %}
+            <a href="{{ path('hwi_oauth_service_redirect', { service: 'sumocoders' }) }}" class="text-muted btn btn-link">
</code_context>
<issue_to_address>
**question:** Enabling Azure/SumoCoders disables the regular username/password login form entirely.

Given this `if/else` structure, any time `azure_login_enabled` or `sumocoders_login_enabled` is true, the classic form won’t render. If SSO is meant to be additive, this logic should render the SSO buttons alongside the form instead of replacing it. If SSO is meant to disable local login, that behavior is currently implicit, so please confirm it matches product requirements.
</issue_to_address>

### Comment 2
<location path="src/Security/OAuth/AzureUserProvider.php" line_range="89-92" />
<code_context>
+        return $refreshedUser;
+    }
+
+    public function supportsClass(string $class): bool
+    {
+        return $class === User::class;
+    }
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Strict `supportsClass` check can fail for Doctrine proxy classes.

Because Doctrine proxies use classes like `Proxies\__CG__\App\Entity\User\User`, this strict comparison means the provider won’t support proxied `User` instances, which can cause refresh/auth issues in some setups. Consider using `return is_a($class, User::class, true);` to keep the type narrow while correctly handling proxies.

```suggestion
    public function supportsClass(string $class): bool
    {
        return is_a($class, User::class, true);
    }
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@@ -7,6 +7,21 @@
</div>
{% endif %}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

question: Enabling Azure/SumoCoders disables the regular username/password login form entirely.

Given this if/else structure, any time azure_login_enabled or sumocoders_login_enabled is true, the classic form won’t render. If SSO is meant to be additive, this logic should render the SSO buttons alongside the form instead of replacing it. If SSO is meant to disable local login, that behavior is currently implicit, so please confirm it matches product requirements.

Comment on lines +89 to +92
public function supportsClass(string $class): bool
{
return $class === User::class;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): Strict supportsClass check can fail for Doctrine proxy classes.

Because Doctrine proxies use classes like Proxies\__CG__\App\Entity\User\User, this strict comparison means the provider won’t support proxied User instances, which can cause refresh/auth issues in some setups. Consider using return is_a($class, User::class, true); to keep the type narrow while correctly handling proxies.

Suggested change
public function supportsClass(string $class): bool
{
return $class === User::class;
}
public function supportsClass(string $class): bool
{
return is_a($class, User::class, true);
}

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.

3 participants