diff --git a/lms/envs/aws.py b/lms/envs/aws.py index 39e0f9a88f..9ba561965e 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -769,6 +769,8 @@ CREDIT_HELP_LINK_URL = ENV_TOKENS.get('CREDIT_HELP_LINK_URL', CREDIT_HELP_LINK_U JWT_ISSUER = ENV_TOKENS.get('JWT_ISSUER', JWT_ISSUER) JWT_EXPIRATION = ENV_TOKENS.get('JWT_EXPIRATION', JWT_EXPIRATION) JWT_AUTH.update(ENV_TOKENS.get('JWT_AUTH', {})) +PUBLIC_RSA_KEY = ENV_TOKENS.get('PUBLIC_RSA_KEY', PUBLIC_RSA_KEY) +PRIVATE_RSA_KEY = ENV_TOKENS.get('PRIVATE_RSA_KEY', PRIVATE_RSA_KEY) ################# PROCTORING CONFIGURATION ################## diff --git a/lms/envs/common.py b/lms/envs/common.py index 3daec93028..d2a64e9532 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -2784,6 +2784,10 @@ LTI_AGGREGATE_SCORE_PASSBACK_DELAY = 15 * 60 JWT_EXPIRATION = 30 JWT_ISSUER = None +# For help generating a key pair import and run `openedx.core.lib.rsa_key_utils.generate_rsa_key_pair()` +PUBLIC_RSA_KEY = None +PRIVATE_RSA_KEY = None + # Credit notifications settings NOTIFICATION_EMAIL_CSS = "templates/credit_notifications/credit_notification.css" NOTIFICATION_EMAIL_EDX_LOGO = "templates/credit_notifications/edx-logo-header.png" diff --git a/lms/envs/devstack.py b/lms/envs/devstack.py index 1657155966..fbc7dd29f4 100644 --- a/lms/envs/devstack.py +++ b/lms/envs/devstack.py @@ -225,6 +225,47 @@ CORS_ORIGIN_WHITELIST = () CORS_ORIGIN_ALLOW_ALL = True # JWT settings for devstack +PUBLIC_RSA_KEY = """\ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApCujf5oZBGK4MafMRGY9 ++zdRRI9YDm1r+81coDCysSrwkhTkFIwP2dmS6lYvJuQ5wifuQa3WFv1Kh9Nr2XRJ +1m9OL3/JpmMyTi/YuwD7tIf65tab1SOSRYkoxOKRuuvZuXQG9nWbXrGDncnwuWxf +eymwWaIrAhALUS5+nDa7dauj8VngsWauMrEA/MWShEzsR53wGKlciEZA1r/AfQ55 +XS42GvBobhhy9SeZ3B6LHiaAEywpwFmKPssuoHSNhbPa49LW3gXJ6CsFGRDcBFKd +xJ/l8O847Q7kg1lvckpLsKyu5167NK9Qj1X/O3SwVBL3cxx1HpQ6+q3SGLZ4ngow +hwIDAQAB +-----END PUBLIC KEY-----""" + +PRIVATE_RSA_KEY = """\ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCkK6N/mhkEYrgx +p8xEZj37N1FEj1gObWv7zVygMLKxKvCSFOQUjA/Z2ZLqVi8m5DnCJ+5BrdYW/UqH +02vZdEnWb04vf8mmYzJOL9i7APu0h/rm1pvVI5JFiSjE4pG669m5dAb2dZtesYOd +yfC5bF97KbBZoisCEAtRLn6cNrt1q6PxWeCxZq4ysQD8xZKETOxHnfAYqVyIRkDW +v8B9DnldLjYa8GhuGHL1J5ncHoseJoATLCnAWYo+yy6gdI2Fs9rj0tbeBcnoKwUZ +ENwEUp3En+Xw7zjtDuSDWW9ySkuwrK7nXrs0r1CPVf87dLBUEvdzHHUelDr6rdIY +tnieCjCHAgMBAAECggEBAJvTiAdQPzq4cVlAilTKLz7KTOsknFJlbj+9t5OdZZ9g +wKQIDE2sfEcti5O+Zlcl/eTaff39gN6lYR73gMEQ7h0J3U6cnsy+DzvDkpY94qyC +/ZYqUhPHBcnW3Mm0vNqNj0XGae15yBXjrKgSy9lUknSXJ3qMwQHeNL/DwA2KrfiL +g0iVjk32dvSSHWcBh0M+Qy1WyZU0cf9VWzx+Q1YLj9eUCHteStVubB610XV3JUZt +UTWiUCffpo2okHsTBuKPVXK/5BL+BpGplcxRSlnSbMaI611kN3iKlO8KGISXHBz7 +nOPdkfZC9poEXt5SshtINuGGCCc8hDxpg1otYqCLaYECgYEA1MSCPs3pBkEagchV +g0rxYmDUC8QkeIOBuZFjhkdoUgZ6rFntyRZd1NbCUi3YBbV1YC12ZGohqWUWom1S +AtNbQ2ZTbqEnDKWbNvLBRwkdp/9cKBce85lCCD6+U2o2Ha8C0+hKeLBn8un1y0zY +1AQTqLAz9ItNr0aDPb89cs5voWcCgYEAxYdC8vR3t8iYMUnK6LWYDrKSt7YiorvF +qXIMANcXQrnO0ptC0B56qrUCgKHNrtPi5bGpNBJ0oKMfbmGfwX+ca8sCUlLvq/O8 +S2WZwSJuaHH4lEBi8ErtY++8F4B4l3ENCT84Hyy5jiMpbpkHEnh/1GNcvvmyI8ud +3jzovCNZ4+ECgYEA0r+Oz0zAOzyzV8gqw7Cw5iRJBRqUkXaZQUj8jt4eO9lFG4C8 +IolwCclrk2Drb8Qsbka51X62twZ1ZA/qwve9l0Y88ADaIBHNa6EKxyUFZglvrBoy +w1GT8XzMou06iy52G5YkZeU+IYOSvnvw7hjXrChUXi65lRrAFqJd6GEIe5MCgYA/ +0LxDa9HFsWvh+JoyZoCytuSJr7Eu7AUnAi54kwTzzL3R8tE6Fa7BuesODbg6tD/I +v4YPyaqePzUnXyjSxdyOQq8EU8EUx5Dctv1elTYgTjnmA4szYLGjKM+WtC3Bl4eD +pkYGZFeqYRfAoHXVdNKvlk5fcKIpyF2/b+Qs7CrdYQKBgQCc/t+JxC9OpI+LhQtB +tEtwvklxuaBtoEEKJ76P9vrK1semHQ34M1XyNmvPCXUyKEI38MWtgCCXcdmg5syO +PBXdDINx+wKlW7LPgaiRL0Mi9G2aBpdFNI99CWVgCr88xqgSE24KsOxViMwmi0XB +Ld/IRK0DgpGP5EJRwpKsDYe/UQ== +-----END PRIVATE KEY-----""" + JWT_AUTH.update({ 'JWT_ALGORITHM': 'HS256', 'JWT_SECRET_KEY': 'lms-secret', diff --git a/openedx/core/lib/rsa_key_utils.py b/openedx/core/lib/rsa_key_utils.py new file mode 100644 index 0000000000..fd4853a5ec --- /dev/null +++ b/openedx/core/lib/rsa_key_utils.py @@ -0,0 +1,21 @@ +""" Utils for RSA keys""" +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives.serialization import( + Encoding, PublicFormat, PrivateFormat, NoEncryption +) + + +def generate_rsa_key_pair(key_size=2048): + """ Generates a public and private RSA PEM encoded key pair""" + private_key = rsa.generate_private_key( + public_exponent=65537, + key_size=key_size, + backend=default_backend() + ) + private_key_str = private_key.private_bytes(Encoding.PEM, PrivateFormat.PKCS8, NoEncryption()) + public_key_str = private_key.public_key().public_bytes(Encoding.PEM, PublicFormat.SubjectPublicKeyInfo) + + # Not intented for programmatic use, so we print the keys out + print public_key_str + print private_key_str diff --git a/openedx/core/lib/token_utils.py b/openedx/core/lib/token_utils.py index 4f9f00f406..9b5401d240 100644 --- a/openedx/core/lib/token_utils.py +++ b/openedx/core/lib/token_utils.py @@ -1,6 +1,8 @@ """Utilities for working with ID tokens.""" import datetime +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.serialization import load_pem_private_key from django.conf import settings from django.core.exceptions import ImproperlyConfigured import jwt @@ -63,3 +65,50 @@ def get_id_token(user, client_name): } return jwt.encode(payload, client.client_secret) + + +def get_asymmetric_token(user): + """Construct a JWT signed with this app's private key. + + The JWT includes the following claims: + + preferred_username (str): The user's username. The claim name is borrowed from edx-oauth2-provider. + name (str): The user's full name. + email (str): The user's email address. + administrator (Boolean): Whether the user has staff permissions. + iss (str): Registered claim. Identifies the principal that issued the JWT. + exp (int): Registered claim. Identifies the expiration time on or after which + the JWT must NOT be accepted for processing. + iat (int): Registered claim. Identifies the time at which the JWT was issued. + sub (int): Registered claim. Identifies the user. This implementation uses the raw user id. + + Arguments: + user (User): User for which to generate the JWT. + + Returns: + str: the JWT + + """ + private_key = load_pem_private_key(settings.PRIVATE_RSA_KEY, None, default_backend()) + + try: + # Service users may not have user profiles. + full_name = UserProfile.objects.get(user=user).name + except UserProfile.DoesNotExist: + full_name = None + + now = datetime.datetime.utcnow() + expires_in = getattr(settings, 'OAUTH_ID_TOKEN_EXPIRATION', 30) + + payload = { + 'preferred_username': user.username, + 'name': full_name, + 'email': user.email, + 'administrator': user.is_staff, + 'iss': settings.OAUTH_OIDC_ISSUER, + 'exp': now + datetime.timedelta(seconds=expires_in), + 'iat': now, + 'sub': anonymous_id_for_user(user, None), + } + + return jwt.encode(payload, private_key, algorithm='RS512') diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index a3ea098ea0..8c612a3f9c 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -10,6 +10,7 @@ bleach==1.4 html5lib==0.999 boto==2.39.0 celery==3.1.18 +cryptography==1.3.1 cssselect==0.9.1 dealer==2.0.4 defusedxml==0.4.1