diff --git a/lms/djangoapps/licenses/__init__.py b/lms/djangoapps/licenses/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/lms/djangoapps/licenses/management/__init__.py b/lms/djangoapps/licenses/management/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/lms/djangoapps/licenses/management/commands/__init__.py b/lms/djangoapps/licenses/management/commands/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/lms/djangoapps/licenses/management/commands/generate_serial_numbers.py b/lms/djangoapps/licenses/management/commands/generate_serial_numbers.py
new file mode 100644
index 0000000000000000000000000000000000000000..7c6b0d310e7e473ee92fd72cb58782418babf4cd
--- /dev/null
+++ b/lms/djangoapps/licenses/management/commands/generate_serial_numbers.py
@@ -0,0 +1,65 @@
+import os.path
+from uuid import uuid4
+from optparse import make_option
+
+from django.utils.html import escape
+from django.core.management.base import BaseCommand, CommandError
+
+from xmodule.modulestore.django import modulestore
+
+from licenses.models import CourseSoftware, UserLicense
+
+
+class Command(BaseCommand):
+    help = """Generate random serial numbers for software used in a course.
+
+    Usage: generate_serial_numbers <course_id> <software_name> <count>
+
+    <count> is the number of numbers to generate.
+
+    Example:
+
+       import_serial_numbers MITx/6.002x/2012_Fall matlab 100
+
+    """
+    args = "course_id software_id count"
+
+    def handle(self, *args, **options):
+        """
+        """
+        course_id, software_name, count = self._parse_arguments(args)
+
+        software, _ = CourseSoftware.objects.get_or_create(course_id=course_id,
+                                                           name=software_name)
+        self._generate_serials(software, count)
+
+    def _parse_arguments(self, args):
+        if len(args) != 3:
+            raise CommandError("Incorrect number of arguments")
+
+        course_id = args[0]
+        courses = modulestore().get_courses()
+        known_course_ids = set(c.id for c in courses)
+
+        if course_id not in known_course_ids:
+            raise CommandError("Unknown course_id")
+
+        software_name = escape(args[1].lower())
+
+        try:
+            count = int(args[2])
+        except ValueError:
+            raise CommandError("Invalid <count> argument.")
+
+        return course_id, software_name, count
+
+    def _generate_serials(self, software, count):
+        print "Generating {0} serials".format(count)
+
+        # add serial numbers them to the database
+        for _ in xrange(count):
+            serial = str(uuid4())
+            license = UserLicense(software=software, serial=serial)
+            license.save()
+
+        print "{0} new serial numbers generated.".format(count)
diff --git a/lms/djangoapps/licenses/management/commands/import_serial_numbers.py b/lms/djangoapps/licenses/management/commands/import_serial_numbers.py
new file mode 100644
index 0000000000000000000000000000000000000000..a3a8c0bad1017415f1e05c02391470153d8e3fec
--- /dev/null
+++ b/lms/djangoapps/licenses/management/commands/import_serial_numbers.py
@@ -0,0 +1,70 @@
+import os.path
+from optparse import make_option
+
+from django.utils.html import escape
+from django.core.management.base import BaseCommand, CommandError
+
+from xmodule.modulestore.django import modulestore
+
+from licenses.models import CourseSoftware, UserLicense
+
+
+class Command(BaseCommand):
+    help = """Imports serial numbers for software used in a course.
+
+    Usage: import_serial_numbers <course_id> <software_name> <file>
+
+    <file> is a text file that list one available serial number per line.
+
+    Example:
+
+       import_serial_numbers MITx/6.002x/2012_Fall matlab serials.txt
+
+    """
+    args = "course_id software_id serial_file"
+
+    def handle(self, *args, **options):
+        """
+        """
+        course_id, software_name, filename = self._parse_arguments(args)
+
+        software, _ = CourseSoftware.objects.get_or_create(course_id=course_id,
+                                                           name=software_name)
+        self._import_serials(software, filename)
+
+    def _parse_arguments(self, args):
+        if len(args) != 3:
+            raise CommandError("Incorrect number of arguments")
+
+        course_id = args[0]
+        courses = modulestore().get_courses()
+        known_course_ids = set(c.id for c in courses)
+
+        if course_id not in known_course_ids:
+            raise CommandError("Unknown course_id")
+
+        software_name = escape(args[1].lower())
+
+        filename = os.path.abspath(args[2])
+        if not os.path.exists(filename):
+            raise CommandError("Cannot find filename {0}".format(filename))
+
+        return course_id, software_name, filename
+
+    def _import_serials(self, software, filename):
+        print "Importing serial numbers for {0}.".format(software)
+
+        serials = set(unicode(l.strip()) for l in open(filename))
+
+        # remove serial numbers we already have
+        licenses = UserLicense.objects.filter(software=software)
+        known_serials = set(l.serial for l in licenses)
+        if known_serials:
+            serials = serials.difference(known_serials)
+
+        # add serial numbers them to the database
+        for serial in serials:
+            license = UserLicense(software=software, serial=serial)
+            license.save()
+
+        print "{0} new serial numbers imported.".format(len(serials))
diff --git a/lms/djangoapps/licenses/models.py b/lms/djangoapps/licenses/models.py
new file mode 100644
index 0000000000000000000000000000000000000000..d259892f5d61cfccccb51dbc6555d6ad2f5cd689
--- /dev/null
+++ b/lms/djangoapps/licenses/models.py
@@ -0,0 +1,78 @@
+import logging
+
+from django.db import models, transaction
+
+from student.models import User
+
+log = logging.getLogger("mitx.licenses")
+
+
+class CourseSoftware(models.Model):
+    name = models.CharField(max_length=255)
+    full_name = models.CharField(max_length=255)
+    url = models.CharField(max_length=255)
+    course_id = models.CharField(max_length=255)
+
+    def __unicode__(self):
+        return u'{0} for {1}'.format(self.name, self.course_id)
+
+
+class UserLicense(models.Model):
+    software = models.ForeignKey(CourseSoftware, db_index=True)
+    user = models.ForeignKey(User, null=True)
+    serial = models.CharField(max_length=255)
+
+
+def get_courses_licenses(user, courses):
+    course_ids = set(course.id for course in courses)
+    all_software = CourseSoftware.objects.filter(course_id__in=course_ids)
+
+    assigned_licenses = UserLicense.objects.filter(software__in=all_software,
+                                                   user=user)
+
+    licenses = dict.fromkeys(all_software, None)
+    for license in assigned_licenses:
+        licenses[license.software] = license
+
+    log.info(assigned_licenses)
+    log.info(licenses)
+
+    return licenses
+
+
+def get_license(user, software):
+    try:
+        license = UserLicense.objects.get(user=user, software=software)
+    except UserLicense.DoesNotExist:
+        license = None
+
+    return license
+
+
+def get_or_create_license(user, software):
+    license = get_license(user, software)
+    if license is None:
+        license = _create_license(user, software)
+
+    return license
+
+
+def _create_license(user, software):
+    license = None
+
+    try:
+        # find one license that has not been assigned, locking the
+        # table/rows with select_for_update to prevent race conditions
+        with transaction.commit_on_success():
+            selected = UserLicense.objects.select_for_update()
+            license = selected.filter(user__isnull=True, software=software)[0]
+            license.user = user
+            license.save()
+    except IndexError:
+        # there are no free licenses
+        log.error('No serial numbers available for {0}', software)
+        license = None
+        # TODO [rocha]look if someone has unenrolled from the class
+        # and already has a serial number
+
+    return license
diff --git a/lms/djangoapps/licenses/tests.py b/lms/djangoapps/licenses/tests.py
new file mode 100644
index 0000000000000000000000000000000000000000..f06899d2decfc99b7ef7331546bb27654195983a
--- /dev/null
+++ b/lms/djangoapps/licenses/tests.py
@@ -0,0 +1,85 @@
+import logging
+from uuid import uuid4
+from random import shuffle
+from tempfile import NamedTemporaryFile
+
+from django.test import TestCase
+from django.core.management import call_command
+
+from models import CourseSoftware, UserLicense
+
+COURSE_1 = 'MITx/6.002x/2012_Fall'
+
+SOFTWARE_1 = 'matlab'
+SOFTWARE_2 = 'stata'
+
+log = logging.getLogger(__name__)
+
+
+class CommandTest(TestCase):
+    def test_import_serial_numbers(self):
+        size = 20
+
+        log.debug('Adding one set of serials for {0}'.format(SOFTWARE_1))
+        with generate_serials_file(size) as temp_file:
+            args = [COURSE_1, SOFTWARE_1, temp_file.name]
+            call_command('import_serial_numbers', *args)
+
+        log.debug('Adding one set of serials for {0}'.format(SOFTWARE_2))
+        with generate_serials_file(size) as temp_file:
+            args = [COURSE_1, SOFTWARE_2, temp_file.name]
+            call_command('import_serial_numbers', *args)
+
+        log.debug('There should be only 2 course-software entries')
+        software_count = CourseSoftware.objects.all().count()
+        self.assertEqual(2, software_count)
+
+        log.debug('We added two sets of {0} serials'.format(size))
+        licenses_count = UserLicense.objects.all().count()
+        self.assertEqual(2 * size, licenses_count)
+
+        log.debug('Adding more serial numbers to {0}'.format(SOFTWARE_1))
+        with generate_serials_file(size) as temp_file:
+            args = [COURSE_1, SOFTWARE_1, temp_file.name]
+            call_command('import_serial_numbers', *args)
+
+        log.debug('There should be still only 2 course-software entries')
+        software_count = CourseSoftware.objects.all().count()
+        self.assertEqual(2, software_count)
+
+        log.debug('Now we should have 3 sets of 20 serials'.format(size))
+        licenses_count = UserLicense.objects.all().count()
+        self.assertEqual(3 * size, licenses_count)
+
+        cs = CourseSoftware.objects.get(pk=1)
+
+        lics = UserLicense.objects.filter(software=cs)[:size]
+        known_serials = list(l.serial for l in lics)
+        known_serials.extend(generate_serials(10))
+
+        shuffle(known_serials)
+
+        log.debug('Adding some new and old serials to {0}'.format(SOFTWARE_1))
+        with NamedTemporaryFile() as f:
+            f.write('\n'.join(known_serials))
+            f.flush()
+            args = [COURSE_1, SOFTWARE_1, f.name]
+            call_command('import_serial_numbers', *args)
+
+        log.debug('Check if we added only the new ones')
+        licenses_count = UserLicense.objects.filter(software=cs).count()
+        self.assertEqual((2 * size) + 10, licenses_count)
+
+
+def generate_serials(size=20):
+    return [str(uuid4()) for _ in range(size)]
+
+
+def generate_serials_file(size=20):
+    serials = generate_serials(size)
+
+    temp_file = NamedTemporaryFile()
+    temp_file.write('\n'.join(serials))
+    temp_file.flush()
+
+    return temp_file
diff --git a/lms/djangoapps/licenses/views.py b/lms/djangoapps/licenses/views.py
new file mode 100644
index 0000000000000000000000000000000000000000..7d804fbd3d5c32b6bb6376047648a62402e09b12
--- /dev/null
+++ b/lms/djangoapps/licenses/views.py
@@ -0,0 +1,84 @@
+import logging
+import json
+import re
+from urlparse import urlparse
+from collections import namedtuple, defaultdict
+
+
+from mitxmako.shortcuts import render_to_string
+
+from django.contrib.auth.models import User
+from django.http import HttpResponse, Http404
+from django.views.decorators.csrf import requires_csrf_token, csrf_protect
+
+from models import CourseSoftware
+from models import get_courses_licenses, get_or_create_license, get_license
+
+
+log = logging.getLogger("mitx.licenses")
+
+
+License = namedtuple('License', 'software serial')
+
+
+def get_licenses_by_course(user, courses):
+    licenses = get_courses_licenses(user, courses)
+    licenses_by_course = defaultdict(list)
+
+    # create missing licenses and group by course_id
+    for software, license in licenses.iteritems():
+        if license is None:
+            licenses[software] = get_or_create_license(user, software)
+
+        course_id = software.course_id
+        serial = license.serial if license else None
+        licenses_by_course[course_id].append(License(software, serial))
+
+    # render elements
+    data_by_course = {}
+    for course_id, licenses in licenses_by_course.iteritems():
+        context = {'licenses': licenses}
+        template = 'licenses/serial_numbers.html'
+        data_by_course[course_id] = render_to_string(template, context)
+
+    return data_by_course
+
+
+@requires_csrf_token
+def user_software_license(request):
+    if request.method != 'POST' or not request.is_ajax():
+        raise Http404
+
+    # get the course id from the referer
+    url_path = urlparse(request.META.get('HTTP_REFERER', '')).path
+    pattern = re.compile('^/courses/(?P<id>[^/]+/[^/]+/[^/]+)/.*/?$')
+    match = re.match(pattern, url_path)
+
+    if not match:
+        raise Http404
+    course_id = match.groupdict().get('id', '')
+
+    user_id = request.session.get('_auth_user_id')
+    software_name = request.POST.get('software')
+    generate = request.POST.get('generate', False) == 'true'
+
+    try:
+        software = CourseSoftware.objects.get(name=software_name,
+                                              course_id=course_id)
+        print software
+    except CourseSoftware.DoesNotExist:
+        raise Http404
+
+    user = User.objects.get(id=user_id)
+
+    if generate:
+        license = get_or_create_license(user, software)
+    else:
+        license = get_license(user, software)
+
+    if license:
+        response = {'serial': license.serial}
+    else:
+        response = {'error': 'No serial number found'}
+
+    return HttpResponse(json.dumps(response), mimetype='application/json')
diff --git a/lms/envs/common.py b/lms/envs/common.py
index a927da8e98251c17e8d7005710dbc82cd8618ab2..9b98e4ecfda3f50dae69609a6e182fea79668dbd 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -626,6 +626,7 @@ INSTALLED_APPS = (
     'certificates',
     'instructor',
     'psychometrics',
+    'licenses',
 
     #For the wiki
     'wiki', # The new django-wiki from benjaoming
diff --git a/lms/templates/licenses/serial_numbers.html b/lms/templates/licenses/serial_numbers.html
new file mode 100644
index 0000000000000000000000000000000000000000..18f0ff8a9bfd425ca83eace673495057976c66aa
--- /dev/null
+++ b/lms/templates/licenses/serial_numbers.html
@@ -0,0 +1,10 @@
+<dl>
+% for license in licenses:
+  <dt> ${license.software.name}: </dt>
+  % if license.serial:
+    <dd> ${license.serial} </dd>
+  % else:
+    <dd> None Available </dd>
+  % endif
+% endfor
+</dl>
diff --git a/lms/urls.py b/lms/urls.py
index 89a541ab0696dbafc69c17e69ae08c84fe308d2f..e02547838777a418099cdfc62043d3a683cd5a06 100644
--- a/lms/urls.py
+++ b/lms/urls.py
@@ -154,6 +154,14 @@ if settings.COURSEWARE_ENABLED:
         url(r'^preview/chemcalc', 'courseware.module_render.preview_chemcalc',
             name='preview_chemcalc'),
 
+        # Software Licenses
+
+        # TODO: for now, this is the endpoint of an ajax replay
+        # service that retrieve and assigns license numbers for
+        # software assigned to a course. The numbers have to be loaded
+        # into the database.
+        url(r'^software-licenses$', 'licenses.views.user_software_license', name="user_software_license"),
+
         url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/xqueue/(?P<userid>[^/]*)/(?P<id>.*?)/(?P<dispatch>[^/]*)$',
             'courseware.module_render.xqueue_callback',
             name='xqueue_callback'),