|
1 | 1 | # Partner Integration Module |
2 | 2 |
|
3 | | -The `partner_integration` module provides secure, scalable REST APIs for partner clients to interact with LMS data. It leverages the **Facade pattern** to encapsulate data extraction logic while enforcing strict security and validation rules. |
| 3 | +The `partner_integration` module provides secure, scalable REST APIs for partner clients to interact with LMS data. It leverages the **Facade pattern** to encapsulate data extraction logic while enforcing strict security and validation rules. It also includes the SSO implementation, where it has the possibility of linking partner's users's account. |
4 | 4 |
|
5 | 5 | ## Features |
6 | 6 |
|
@@ -308,3 +308,152 @@ The `partner_integration` module provides: |
308 | 308 | - **Test coverage** for all critical use cases. |
309 | 309 |
|
310 | 310 | This module lays the foundation for future partner integrations and ensures compliance with organizational security policies. |
| 311 | + |
| 312 | +## SSO integration |
| 313 | + |
| 314 | +It classifies mainly in three states, in quick words: |
| 315 | + |
| 316 | +- A user that comes from the partner platform and has no SSO register. It redirects to login page. |
| 317 | +- A user that comes from the partner platform and has SSO register. It redicts to the `redirect_uri` in the url. |
| 318 | +- A user that comes from the partner platform and has SSO register, but with a different `external_user_id`. It redirects to login page and after authenticating it updates the `external_user_id` on the SSO register. |
| 319 | + |
| 320 | +### Important |
| 321 | +- All the flow respects our instance as the SSO authentication server. |
| 322 | +- All the features applied were created respecting the current available resources from Open Edx upstream, that is, it does not install new packages, nor uses new resources, it only implements the platform resources in a way that meets the SSO requirements. |
| 323 | + |
| 324 | +## The key concepts |
| 325 | + |
| 326 | +#### Partner JWT |
| 327 | +- Issued by the partner |
| 328 | +- Validated by ClientJWTAuthentication |
| 329 | +- Identifies which partner client is calling us |
| 330 | + |
| 331 | +#### external_user_id |
| 332 | +- The user identifier on the partner system |
| 333 | +- Stored locally in SSOPartnerIntegration |
| 334 | +- Used as the primary lookup key during SSO |
| 335 | + |
| 336 | +#### SSOPartnerIntegration |
| 337 | +- This model represents “this local user is linked to this partner user”. In other words "User X in our system corresponds to external user Y from partner Z" |
| 338 | + |
| 339 | +## How it works |
| 340 | + |
| 341 | +### `Applications` |
| 342 | + |
| 343 | +The Open Edx Applications model, the one that manages the SSO applications. In this model we creates an application for each partner who wants to use our SSO integration. |
| 344 | + |
| 345 | +### `PartnerAPIClient` |
| 346 | + |
| 347 | +The model that represents each partner client that consumes our partner integration solutions (SSO and webservice). |
| 348 | + |
| 349 | +### URL format |
| 350 | +```bash |
| 351 | +http://lms.local.nau.fccn.pt/nau-openedx-extensions/partner-integration/sso/authorize/ |
| 352 | +?client_id=wFkgI0PDXXSYkrLwC4I3pe4t7lhwmWbjScJUULW3 |
| 353 | +&redirect_uri=https://example.com |
| 354 | +&jwt_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMiIsInVzZXJuYW1lIjoiZ3RyYWluaW5nIiwiZXhwIjoxNzY3MDM3NzM5LCJpYXQiOjE3NjcwMzA1MzksImlzcyI6Imh0dHA6Ly9sbXMubG9jYWwubmF1LmZjY24ucHQvb2F1dGgyIiwiYXVkIjoib3BlbmVkeCJ9.5uf1q78l9jdKJ9n-Wu94IYPgtCRQknTCacHtjeGI0m0 |
| 355 | +&external_user_id=123456789 |
| 356 | +``` |
| 357 | +#### client_id |
| 358 | +Setup via `Applications` model, the client id of an application register. |
| 359 | + |
| 360 | +#### redirect_uri |
| 361 | +Dynamically setup, the partner changes the `redirect_uri` in order to redirect the user to the corresponding course he must to go. |
| 362 | + |
| 363 | +#### jwt_token |
| 364 | +Obtained via webservice authentication, the `PartnerAPIClient` uses the authentication endpoint to obtain a valid jwt token. |
| 365 | +`https://lms.nau.edu.pt/nau-openedx-extensions/partner-integration/auth-token/` |
| 366 | + |
| 367 | +#### external_user_id |
| 368 | +Dynamically setup, this is the information that identifies the user in the partner's platform, e.g. email, NIF, user_id or username. The user on our platform has no possibility to edit his register. |
| 369 | + |
| 370 | +### `SSOPartnerIntegrations` |
| 371 | + |
| 372 | +The model responsible for managing the SSO registers. Visible via Django admin. |
| 373 | + |
| 374 | +## The entry point: `CustomAuthorizationView` |
| 375 | + |
| 376 | +This view overrides the OAuth2 authorization process. |
| 377 | + |
| 378 | +### Two important overridden methods |
| 379 | +`dispatch()` → decision & authentication |
| 380 | +`get()` → final redirect handling |
| 381 | + |
| 382 | +### Methods descriptions: |
| 383 | +`dispatch()`: decides who the user is and what to do |
| 384 | +`get()`: decides where to send the user |
| 385 | +`handle_sso_registration()`: creates or updates the user SSO register. |
| 386 | + |
| 387 | + |
| 388 | +## Flow description |
| 389 | +1. Partner calls the endpoint |
| 390 | + |
| 391 | +The partner redirects the user’s browser to this endpoint with: |
| 392 | +- client_id |
| 393 | +- jwt_token |
| 394 | +- external_user_id |
| 395 | + |
| 396 | +2. Partner and application validation |
| 397 | + |
| 398 | +Inside `dispatch()`: |
| 399 | +- The JWT is validated |
| 400 | +- If invalid → redirect to default partner URL: |
| 401 | + - The OAuth Application is fetched using `client_id` |
| 402 | + - If it doesn’t exist → redirect to default partner URL settings (e.g. NAU home page) |
| 403 | + |
| 404 | +This ensures only known and active partners can use the flow. |
| 405 | + |
| 406 | +## The three scenarios |
| 407 | + |
| 408 | +### Scenario 1: User has no SSO registration |
| 409 | +No `SSOPartnerIntegration` exists for the given `external_user_id`: |
| 410 | + |
| 411 | +#### What we do: |
| 412 | +- Redirect the user to the login page |
| 413 | +- After login, `handle_sso_registration()` is called |
| 414 | +- A new `SSOPartnerIntegration` is created |
| 415 | +- links the local user |
| 416 | +- stores the `external_user_id` |
| 417 | +- User is redirected back to the partner callback |
| 418 | + |
| 419 | +#### Outcome: |
| 420 | +First-time SSO users get properly linked. From now on, future logins are seamless. This is first-time partner access. |
| 421 | + |
| 422 | +### Scenario 2: User already has an SSO registration |
| 423 | + |
| 424 | +#### A `SSOPartnerIntegration` exists for: |
| 425 | +- this `partner_client` |
| 426 | +- this `external_user_id` |
| 427 | + |
| 428 | +#### What we do: |
| 429 | +- Authenticate the request |
| 430 | +- If the user is not logged in, login programmatically |
| 431 | +- Continue the OAuth flow and redirect |
| 432 | + |
| 433 | +#### Outcome: |
| 434 | +- User is transparently logged in |
| 435 | +- No screens, no interaction |
| 436 | +- Clean SSO experience |
| 437 | + |
| 438 | +This is the success path. |
| 439 | + |
| 440 | +### Scenario 3: User exists, but `external_user_id` has changed |
| 441 | + |
| 442 | +The user already has an SSO record, but the incoming `external_user_id` is different from the stored one. |
| 443 | + |
| 444 | +#### This usually happens when: |
| 445 | +- Partner migrates users |
| 446 | +- Partner reissues accounts |
| 447 | +- External IDs change over time |
| 448 | + |
| 449 | +#### What we do: |
| 450 | +- Force the user to authenticate again |
| 451 | +- Update the existing SSO record with the new `external_user_id` |
| 452 | +- Redirect back to the partner callback |
| 453 | + |
| 454 | +#### Outcome: |
| 455 | +- The account link is repaired |
| 456 | +- No duplicate users are created |
| 457 | +- The system remains consistent |
| 458 | + |
| 459 | +It corrects automatically a register that has changed its main information (`external_user_id`). |
0 commit comments