4.3 KiB
OAuth2 Dependency Injection Fix
Problem
When the OAuth2 authorization flow was initiated, the OAuth2AuthorizationMiddleware was receiving a ContainerInstance object instead of the actual OAuth2Service instance. This caused the following error:
OAuth2AuthorizationMiddleware: oauth2Service is: ContainerInstance {
services: [...],
id: 'default'
}
OAuth2AuthorizationMiddleware: oauth2Service type: object
OAuth2AuthorizationMiddleware: isInitialized is not a function
OAuth2AuthorizationMiddleware: Available methods: [ 'services', 'id' ]
Root Cause
The issue was caused by how routing-controllers handles dependency injection when using the @UseBefore() decorator with middleware classes.
When you use @UseBefore(OAuth2AuthorizationMiddleware) on a controller, routing-controllers attempts to instantiate the middleware, but the constructor parameter injection wasn't working correctly with TypeDI despite calling useContainer(Container).
Original Code (Broken)
@Service()
export default class OAuth2AuthorizationMiddleware implements ExpressMiddlewareInterface {
constructor(private oauth2Service: OAuth2Service) {}
async use(request: Request, response: Response): Promise<void> {
// oauth2Service was receiving ContainerInstance instead of OAuth2Service
if (!this.oauth2Service.isInitialized()) { // Error: isInitialized is not a function
// ...
}
}
}
Solution
Instead of relying on constructor parameter injection, we explicitly retrieve the OAuth2Service from the TypeDI container inside the constructor:
Fixed Code
import { Service, Container } from 'typedi'
import { OAuth2Service } from '../services/OAuth2Service'
@Service()
export default class OAuth2AuthorizationMiddleware implements ExpressMiddlewareInterface {
private oauth2Service: OAuth2Service
constructor() {
// Explicitly get OAuth2Service from the container to avoid injection issues
this.oauth2Service = Container.get(OAuth2Service)
}
async use(request: Request, response: Response): Promise<void> {
// Now oauth2Service is correctly the OAuth2Service instance
if (!this.oauth2Service.isInitialized()) {
// Works correctly
}
}
}
Files Modified
-
server/middlewares/OAuth2AuthorizationMiddleware.ts- Changed from constructor parameter injection to explicit container retrieval
- Removed debugging console.log statements
-
server/middlewares/OAuth2CallbackMiddleware.ts- Applied the same fix for consistency
- Changed from constructor parameter injection to explicit container retrieval
Why This Works
By using Container.get(OAuth2Service) explicitly:
- We bypass the problematic parameter injection mechanism
- TypeDI correctly resolves the service as a singleton
- The same instance that was initialized in
app.tsis retrieved - All methods (
isInitialized(),createAuthorizationURL(), etc.) are available
Testing
After this fix, the OAuth2 flow should work correctly:
- User navigates to
/api/oauth2/connect OAuth2AuthorizationMiddlewaresuccessfully retrievesOAuth2Service- PKCE parameters are generated
- User is redirected to the OIDC provider
- After authentication, callback to
/api/oauth2/callbackworks OAuth2CallbackMiddlewareexchanges the code for tokens- User information is retrieved and stored in session
- User is redirected back to the original page
Related Documentation
- OAUTH2-README.md - Main OAuth2/OIDC documentation
- OAUTH2-QUICK-START.md - Quick start guide
- OAUTH2-IMPLEMENTATION-STATUS.md - Implementation status
Technical Notes
Why Not Global Middleware Registration?
We could have registered the middleware globally in useExpressServer() configuration, but using @UseBefore() provides:
- Better route-specific control
- Clearer code organization
- Explicit middleware ordering per endpoint
TypeDI Singleton Behavior
The @Service() decorator on OAuth2Service makes it a singleton by default, so:
Container.get(OAuth2Service)always returns the same instance- The instance initialized in
app.tswithinitializeFromWellKnown()is the same one used in middleware - No duplicate initialization occurs
Date
December 2024