Skip to content

Release 0.113.2 #2576

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,10 @@ POSTHOG_PROJECT_API_KEY=
POSTHOG_API_HOST=https://app.posthog.com/
HUBSPOT_HOME_PAGE_FORM_GUID=
HUBSPOT_PORTAL_ID=

KEYCLOAK_SVC_KEYSTORE_PASSWORD=supersecret123456789
Copy link
Contributor

Choose a reason for hiding this comment

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

critical

The KEYCLOAK_SVC_KEYSTORE_PASSWORD is set to supersecret123456789 by default. This is a major security risk, as anyone deploying the application without changing this password will be vulnerable. Consider removing the default value or generating a random one during deployment. It is recommended to not have a default value at all, forcing the user to set one.

KEYCLOAK_SVC_KEYSTORE_PASSWORD=

KEYCLOAK_SVC_HOSTNAME=kc.odl.local
KEYCLOAK_SVC_ADMIN=admin
KEYCLOAK_SVC_ADMIN_PASSWORD=admin
KEYCLOAK_PORT=7080
KEYCLOAK_SSL_PORT=7443
2 changes: 2 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ repos:
- compliance/test_data/cybersource/
- --exclude-files
- ".*_test.js"
- --exclude-files
- "config/keycloak/*"
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Excluding config/keycloak/* from pre-commit hooks and secrets scanning is a good idea to prevent secrets from being accidentally committed. However, ensure that this exclusion is intentional and doesn't bypass necessary security checks for other important files in the config directory.

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.7.0"
hooks:
Expand Down
7 changes: 4 additions & 3 deletions .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@
"poetry.lock",
"yarn.lock",
"compliance/test_data/cybersource/",
".*_test.js"
".*_test.js",
"config/keycloak/*"
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Excluding config/keycloak/* from secrets scanning is a good idea to prevent secrets from being accidentally committed. However, ensure that this exclusion is intentional and doesn't bypass necessary security checks for other important files in the config directory.

]
}
],
Expand Down Expand Up @@ -193,7 +194,7 @@
"filename": "main/settings.py",
"hashed_secret": "09edaaba587f94f60fbb5cee2234507bcb883cc2",
"is_verified": false,
"line_number": 959
"line_number": 1014
}
],
"pants": [
Expand Down Expand Up @@ -240,5 +241,5 @@
}
]
},
"generated_at": "2025-03-04T17:10:08Z"
"generated_at": "2025-03-18T15:57:12Z"
}
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ ENV PATH="$VIRTUAL_ENV/bin:$POETRY_HOME/bin:$PATH"
# Install project packages
COPY pyproject.toml /app
COPY poetry.lock /app
COPY mitol_*.gz /app

RUN chown -R mitodl:mitodl /app
RUN mkdir ${VIRTUAL_ENV} && chown -R mitodl:mitodl ${VIRTUAL_ENV}

Expand Down
111 changes: 111 additions & 0 deletions README-keycloak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# MITx Online Keycloak Integration

The Compose file includes a Keycloak instance that you can use for authentication instead of spinning up a separate one or using one of the deployed instances. It's not enabled by default, but you can run it if you prefer not to run your own Keycloak instance.

_If you're running the pack-in Keycloak with other apps (Unified Ecommerce, Learn), you might just want to use its instance instead._ There's instructions below on doing that. Start at "Configuring MITx Online" below.

## Default Settings

These are the defaults configured for the system.

### Ports and Hostname

By default, the Keycloak instance listens on ports `7080` and `7443` and the hostname it expects is `kc.odl.local`. If you want to change this, set these in your `.env` file:

- `KEYCLOAK_SVC_HOSTNAME`
- `KEYCLOAK_PORT`
- `KEYCLOAK_SSL_PORT`

### SSL Certificate

There's a self-signed cert that's in `config/keycloak/tls` - if you'd rather set up your own (or you have a real cert or something to use), you can drop the PEM files in there. See the README there for info.

### Realm and App Users

There's a `default-realm.json` in `config/keycloak` that will get loaded by Keycloak when it starts up, and will set up a realm for you with some users and a client so you don't have to set it up yourself. The realm it creates is called `ol-local`.

The _`ol_local`_ users it sets up are:

| User | Password |
|---|---|
| `[email protected]` | `student` |
| `[email protected]` | `prof` |
| `[email protected]` | `admin` |

> These will not get you into the Keycloak admin interface.

The default realm contains an OIDC client called `apisix`. You can get or change the secrets from within the Keycloak Admin, or you can create a new client if you wish.

These users are in groups, but the groups don't mean anything by default.

### Keycloak Admin

The Keycloak admin interface is at `https://kc.odl.local:7443` by default. As noted, the `ol-local` users above won't get you access to this interface. A separate admin account is configured on first run for this. By default, the credentials are `admin`/`admin` but you can change this by setting these in your `.env`:
- `KEYCLOAK_SVC_ADMIN`
- `KEYCLOAK_SVC_ADMIN_PASSWORD`

_You probably shouldn't change these, though._ If you want to use a different admin user/password, log into the Keycloak admin after first bringing the container up and create a new user in the Master realm. (There will also be a banner at the top instructing you to do so.)

## Making it Work

The Keycloak instance is hidden in the `keycloak` profile in the Composer file, so if you want to interact with it, you'll need to run `docker compose --profile keycloak`, or add `COMPOSE_PROFILES=keycloak` to your `.env` file. (If you start the app without the profile, you can still start Keycloak later by specifying the profile.)

### Database Config

If you're **starting from scratch** (no volumes, containers, etc.), the database container should pick up the init script in `config/db` and create a user and database for Keycloak. No further configuration is needed.

If you're starting with **an existing database**: you will need to create a Keycloak user and database. The easiest way to do it is to just run the `config/db/init-keycloak.sql` script against your running database.

### First Start

1. In `config/keycloak/tls`, copy `tls.crt.default` and `tls.key.default` to `tls.crt` and `tls.key`. (Or, you can regenerate them - see the README in that folder.)
2. Set Keycloak environment values in your `.env` file. Most of these are described above, and none are required.
- `KEYCLOAK_SVC_KEYSTORE_PASSWORD` - password for the keystore Keycloak will create
- `KEYCLOAK_SVC_ADMIN`
- `KEYCLOAK_SVC_ADMIN_PASSWORD`
- `KEYCLOAK_SVC_HOSTNAME`
- `KEYCLOAK_PORT`
- `KEYCLOAK_SSL_PORT`
3. Start the Keycloak service: `docker compose --profile keycloak up -d keycloak`

The Keycloak container should start and stay running. Once it does, you should be able to log in at `https://kc.odl.local:7443/` with username and password `admin` (or the values you supplied).

## Configuring MITx Online

To use the Keycloak instance with MITx Online, you need to set these in your `.env` file:

- `SOCIAL_AUTH_OL_OIDC_OIDC_ENDPOINT` - root endpoint for OIDC for the realm (see below)
- `SOCIAL_AUTH_OL_OIDC_KEY` - client ID - if you're using the defaults, this is `apisix`
- `SOCIAL_AUTH_OL_OIDC_SECRET` - client secret

These settings can be found in the Keycloak admin. It's easiest to bring Keycloak up on its own via `docker compose up keycloak` to get these values, then bring the rest of the system up later.

The endpoint URL is available in the Keycloak admin. Open the realm you wish to use - `ol-local` for the pack in one - then navigate to `Realm settings` under Configure. At the bottom of the `General` tab, the URL you want is the `OpenID Endpoint Configuration` link. This will be a URL like `http://kc.odl.local:7080/realms/ol-local/.well-known/openid-configuration` You will need to remove the `.well-known/openid-configuration` from this.

> The endpoint URL should be the **HTTP** version of this, unless you have a real certificate on your Keycloak instance. Otherwise, you'll get certificate errors.

> The endpoint URL isn't listed here because you have to go get the secret anyway. Additionally, the endpoint URL can change when new Keycloak versions are released, so it's better to get it out of the admin interface.

The key and secret are available under `Clients`. The key is the client name (so, by default, `apisix`) and the secret is available under `Credentials` once you open the client configuration.

### Other Keycloak Instances

You can use MITx Online with Keycloak instances that aren't the pack-in one - the provided one is just for convenience. Just set the same settings above, but get them from whatever Keycloak instance you already have running.

_If you're running Keycloak alongside MITx Online on the same machine,_ you will need to perform some additional configuration to make it visible to MITx Online. Docker containers and Compose projects can't generally see each other, and local-only hostnames (e.g. hosts file entries) don't apply inside containers. You can get around this by setting up a composer override file that adds an alias for the hostname you're using for the Keycloak instance to `host-gateway`. There's a sample override file called `docker-compose-keycloak-override.yml` you can use for this.

### SSO Admins

If you're using MITx Online with Keycloak, you can technically skip creating a superuser. Instead, you can log in with the account you want to use as a superuser account (e.g. `[email protected]`) and then use the `promote_user` command to add privileges:

`promote_user --promote --superuser [email protected]`

This can also be used to demote users or promote only to staff; see its help for more info. The account must exist first so the user must have logged in before you can run the command.

## Troubleshooting

A few things to check if you run into issues:

- _Redirect loop_: Your sessions have gotten into a weird state. Clear cookies for anything at `odl.local` (or whatever domain you're using).
- _Slowness when loading_: (macOS especially) Make sure your `/etc/hosts` contains equivalent entries for `::1` (IPv6 loopback) for your local hostnames. (macOS will generally prefer IPv6 over IPv4.)
- _Errors about discovery_: Make sure the discovery URL (`SOCIAL_AUTH_OL_OIDC_OIDC_ENDPOINT`) is correct. If Keycloak has updated, it may not be. Additionally, make sure you're connecting to the right ports - if you've changed them, the discovery URL may be somewhere else now.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ mitxonline follows the same [initial setup steps outlined in the common OL web a
Run through those steps **including the addition of `/etc/hosts` aliases and the optional step for running the
`createsuperuser` command**.

### Keycloak Integration

See `README-keycloak.md` for information on setting MITx Online to authenticate via Keycloak. A sample Keycloak instance is included (but not enabled by default), or you can use an external one.

### Configure mitxonline and Open edX

See MITx Online integration with edx:
Expand Down
7 changes: 7 additions & 0 deletions RELEASE.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
Release Notes
=============

Version 0.113.2
---------------

- Seed data for testing courses locally and RC (#2571)
- automatically create cache table (#2568)
- Add OIDC login via social-auth (#2550)

Version 0.113.1 (Released March 26, 2025)
---------------

Expand Down
24 changes: 24 additions & 0 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@
"description": "Timeout (in seconds) for requests made via the edX API client",
"required": false
},
"EXPOSE_OIDC_LOGIN": {
"description": "Expose the OIDC login functionality.",
"required": false
},
"FASTLY_AUTH_TOKEN": {
"description": "Optional token for the Fastly purge API.",
"required": false
Expand Down Expand Up @@ -170,6 +174,14 @@
"description": "Number of milliseconds to wait between consecutive Hubspot calls",
"required": false
},
"KEYCLOAK_BASE_URL": {
"description": "Base URL for the Keycloak instance.",
"required": false
},
"KEYCLOAK_REALM_NAME": {
"description": "Name of the realm the app uses in Keycloak.",
"required": false
},
"LOGOUT_REDIRECT_URL": {
"description": "Url to redirect to after logout, typically Open edX's own logout url",
"required": false
Expand Down Expand Up @@ -608,6 +620,18 @@
"description": "Name of the site. e.g MITx Online",
"required": false
},
"SOCIAL_AUTH_OL_OIDC_KEY": {
"description": "The client id for the OIDC provider",
"required": false
},
"SOCIAL_AUTH_OL_OIDC_OIDC_ENDPOINT": {
"description": "The configuration endpoint for the OIDC provider",
"required": false
},
"SOCIAL_AUTH_OL_OIDC_SECRET": {
"description": "The client secret for the OIDC provider",
"required": false
},
"STATUS_TOKEN": {
"description": "Token to access the status API.",
"required": false
Expand Down
37 changes: 37 additions & 0 deletions authentication/backends/ol_open_id_connect.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Keycloak Authentication Configuration"""

import logging

from social_core.backends.open_id_connect import OpenIdConnectAuth

log = logging.getLogger(__name__)


class OlOpenIdConnectAuth(OpenIdConnectAuth):
"""
Custom wrapper class for adding additional functionality to the
OpenIdConnectAuth child class.
"""

name = "ol-oidc"
REQUIRES_EMAIL_VALIDATION = False

def get_user_details(self, response):
"""Get the user details from the API response"""
details = super().get_user_details(response)

return {
**details,
"global_id": response.get("sub", None),
"name": response.get("name", None),
"is_active": True,
"profile": {
"name": response.get("name", ""),
"email_optin": bool(int(response["email_optin"]))
if "email_optin" in response
else None,
},
}

def __str__(self):
return "OL OpenID Connect (ol-oidc)"
72 changes: 57 additions & 15 deletions authentication/pipeline/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@
from social_core.backends.email import EmailAuth
from social_core.exceptions import AuthException
from social_core.pipeline.partial import partial
from social_core.pipeline.user import create_user

from authentication.backends.ol_open_id_connect import OlOpenIdConnectAuth
from authentication.exceptions import (
EmailBlockedException,
InvalidPasswordException,
RequireEmailException,
RequirePasswordAndPersonalInfoException,
RequirePasswordException,
RequireRegistrationException,
Expand All @@ -30,6 +33,55 @@
# pylint: disable=keyword-arg-before-vararg


def forbid_hijack(strategy, backend, **kwargs): # pylint: disable=unused-argument # noqa: ARG001
"""
Forbid an admin user from trying to login/register while hijacking another user

Args:
strategy (social_django.strategy.DjangoStrategy): the strategy used to authenticate
backend (social_core.backends.base.BaseAuth): the backend being used to authenticate
"""
# As first step in pipeline, stop a hijacking admin from going any further
if bool(strategy.session_get("hijack_history")):
raise AuthException("You are hijacking another user, don't try to login again") # noqa: EM101
return {}


# Pipeline steps for OIDC logins


def create_ol_oidc_user(strategy, details, backend, user=None, *args, **kwargs):
"""
Create the user if we're using the ol-oidc backend.

This also does a blocked user check and makes sure there's an email address.
If the created user is new, we make sure they're set active. (If the user is
inactive, they'll get knocked out of the pipeline elsewhere.)
"""

if backend.name != OlOpenIdConnectAuth.name:
return {}

if "email" not in details:
raise RequireEmailException(backend, None)

if "email" in details and is_user_email_blocked(details["email"]):
raise EmailBlockedException(backend, None)

retval = create_user(strategy, details, backend, user, *args, **kwargs)

# When we have deprecated direct login, remove this and default the is_active
# flag to True in the User model.
if retval.get("is_new"):
retval["user"].is_active = True
retval["user"].save()
Comment on lines +75 to +77
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

In create_ol_oidc_user, the is_active flag is set to True for new users. Ensure that this is the desired behavior and that there are no security implications. Consider adding a comment explaining why this is being done.


return retval


# Pipeline steps for email logins


def validate_email_auth_request(strategy, backend, user=None, *args, **kwargs): # pylint: disable=unused-argument # noqa: ARG001
"""
Validates an auth request for email
Expand All @@ -49,7 +101,7 @@ def validate_email_auth_request(strategy, backend, user=None, *args, **kwargs):
return {}


def get_username(strategy, backend, user=None, *args, **kwargs): # pylint: disable=unused-argument # noqa: ARG001
def get_username(strategy, backend, user=None, details=None, *args, **kwargs): # pylint: disable=unused-argument # noqa: ARG001
"""
Gets the username for a user

Expand All @@ -58,6 +110,10 @@ def get_username(strategy, backend, user=None, *args, **kwargs): # pylint: disa
backend (social_core.backends.base.BaseAuth): the backend being used to authenticate
user (User): the current user
"""

if backend and backend.name == OlOpenIdConnectAuth.name:
return {"username": details["username"] if not user else user.username}

return {"username": None if not user else strategy.storage.user.get_username(user)}


Expand Down Expand Up @@ -209,20 +265,6 @@ def validate_password(
return {}


def forbid_hijack(strategy, backend, **kwargs): # pylint: disable=unused-argument # noqa: ARG001
"""
Forbid an admin user from trying to login/register while hijacking another user

Args:
strategy (social_django.strategy.DjangoStrategy): the strategy used to authenticate
backend (social_core.backends.base.BaseAuth): the backend being used to authenticate
"""
# As first step in pipeline, stop a hijacking admin from going any further
if bool(strategy.session_get("hijack_history")):
raise AuthException("You are hijacking another user, don't try to login again") # noqa: EM101
return {}


def create_openedx_user(strategy, backend, user=None, is_new=False, **kwargs): # pylint: disable=unused-argument # noqa: FBT002, ARG001
"""
Create a user in the openedx, deferring a retry via celery if it fails
Expand Down
Loading