diff --git a/reviewboard/accounts/forms/pages.py b/reviewboard/accounts/forms/pages.py
index edc092223625cadf19a41a7246fa66356e7bf6b7..19e911d029cf8aa5f40d32dfcd9dc1c3f3c4bb68 100644
--- a/reviewboard/accounts/forms/pages.py
+++ b/reviewboard/accounts/forms/pages.py
@@ -358,3 +358,51 @@ class GroupsForm(AccountPageForm):
             }
             for group in groups.order_by('name')
         ]
+
+
+class OAuth2AppListForm(AccountPageForm):
+    """Form for the OAuth2 client application listing page.
+
+    This form does not deal with fields or saving to the database.
+    Instead, it sets up the JavaScript view and provides serialized data
+    representing the applications.
+    """
+
+    form_id = 'oauth2_apps'
+    form_title = _('OAuth 2 Applications')
+    save_label = None
+
+    js_view_class = 'RB.OAuth2ClientAppsView'
+
+    def get_js_view_data(self):
+        """Return a dictionary containing a list of a user's OAuth2 apps data.
+
+        Returns:
+            dict: The dict containing a list of a user's OAuth2 apps data.
+        """
+        # Fetch a list of OAuth2 client applications that the user has created.
+        oauth2_client_apps = self.user.oauth2_oauth2clientapplication.all()
+
+        # Fetch a list of OAuth2 client applications available to the user.
+        return {
+            'oauth2_client_apps': [
+                self._serialize_app(client_app)
+                for client_app in oauth2_client_apps
+            ],
+        }
+
+    def _serialize_app(self, client_app):
+        """Return serialized data of the given OAuth2 client app.
+
+        Args:
+            client_app (reviewboard.oauth2.OAuth2ClientApplication):
+                The OAuth2 client app that needs to be serialized.
+
+        Returns:
+            dict: The serialized data of the given OAuth2 client app.
+        """
+        return {
+            'id': client_app.pk,
+            'name': client_app.name,
+            'clientId': client_app.client_id,
+        }
diff --git a/reviewboard/accounts/pages.py b/reviewboard/accounts/pages.py
index af3e91f49daaeb3f2bece34c3a2a7081ceebc799..79b64190d0d27b959727a69cba1a844f40833cc7 100644
--- a/reviewboard/accounts/pages.py
+++ b/reviewboard/accounts/pages.py
@@ -13,8 +13,9 @@ from djblets.registries.mixins import ExceptionFreeGetterMixin
 from reviewboard.accounts.forms.pages import (AccountSettingsForm,
                                               APITokensForm,
                                               ChangePasswordForm,
-                                              ProfileForm,
-                                              GroupsForm)
+                                              GroupsForm,
+                                              OAuth2AppListForm,
+                                              ProfileForm)
 
 
 class AccountPageRegistry(ExceptionFreeGetterMixin, ConfigPageRegistry):
@@ -29,7 +30,7 @@ class AccountPageRegistry(ExceptionFreeGetterMixin, ConfigPageRegistry):
             type: The page classes, as subclasses of :py:class:`AccountPage`.
         """
         return (GroupsPage, AccountSettingsPage, AuthenticationPage,
-                ProfilePage, APITokensPage)
+                ProfilePage, APITokensPage, OAuth2AppListPage)
 
     def unregister(self, page_class):
         """Unregister the page class.
@@ -108,6 +109,14 @@ class GroupsPage(AccountPage):
     form_classes = [GroupsForm]
 
 
+class OAuth2AppListPage(AccountPage):
+    """A page containing a filterable list of OAuth2 client applications."""
+
+    page_id = 'oauth2_apps'
+    page_title = _('OAuth 2 Applications')
+    form_classes = [OAuth2AppListForm]
+
+
 def register_account_page_class(cls):
     """Register a custom account page class.
 
diff --git a/reviewboard/static/rb/css/pages/my-account.less b/reviewboard/static/rb/css/pages/my-account.less
index 1c9f55ce907165a8c5aa5352a05f3e3d442df4e8..bb6380ae0e6ac7e699e4451d5df2138fa1c0204c 100644
--- a/reviewboard/static/rb/css/pages/my-account.less
+++ b/reviewboard/static/rb/css/pages/my-account.less
@@ -55,3 +55,25 @@
     }
   }
 }
+
+.oauth2 {
+  .client-app-name {
+    display: inline-block;
+    min-width: 15em;
+    padding-right: 2em;
+  }
+
+  .client-app-form {
+    overflow-y: scroll;
+    width: 40em;
+    max-height: 30em;
+
+    input {
+      width: 90%;
+    }
+
+    select {
+      width: 90%;
+    }
+  }
+}
diff --git a/reviewboard/static/rb/js/accountPrefsPage/views/oauth2ClientAppsView.js b/reviewboard/static/rb/js/accountPrefsPage/views/oauth2ClientAppsView.js
new file mode 100644
index 0000000000000000000000000000000000000000..590c6614ba33088145a731104e1c77f2ac73187d
--- /dev/null
+++ b/reviewboard/static/rb/js/accountPrefsPage/views/oauth2ClientAppsView.js
@@ -0,0 +1,369 @@
+(function() {
+
+
+var OAuth2ClientAppFormView,
+    OAuth2ClientAppItem,
+    OAuth2ClientAppItemView,
+    OAuth2RedirectedURI;
+
+
+/*
+ * An item representing an OAuth2 client application created by the user.
+ *
+ * This allows user to remove the app or go to app editing page.
+ * This provides two actions: 'Edit', and 'Remove'.
+ *
+ * Model Attributes:
+ *      id (number):
+ *          The ID of the OAuth2 client application.
+ *
+ *      name (string):
+ *          The name of the OAuth2 client application.
+ *
+ *      clientId (string):
+ *          The OAuth2 id of client application.
+ */
+OAuth2ClientAppItem = RB.Config.ResourceListItem.extend({
+    defaults: _.defaults({
+        id: null,
+        name: null,
+        clientId: null
+    }, RB.Config.ResourceListItem.prototype.defaults),
+
+    /*
+     * Initialize an OAuth2ClientAppItem.
+     *
+     * The item's name will be taken from the serialized client app
+     * information.
+     *
+     * Args:
+     *      options (object):
+     *          An OAuth2 client app object.
+     */
+    initialize: function(options) {
+        _super(this).initialize.call(this, options);
+
+        this.actions = [
+            {
+                id: 'remove',
+                label: gettext('Remove')
+            },
+            {
+                id: 'edit',
+                label: gettext('Edit')
+            }
+        ];
+    },
+
+    /*
+     * Create an OAuth2ClientApp resource for the given attributes.
+     *
+     * Args:
+     *      attrs (object):
+     *          TODO understand the constructor
+     */
+    createResource: function(attrs) {
+        // TODO: implement the webapi
+        return new RB.OAuth2ClientApp();
+    }
+});
+
+
+/*
+ * Displays a list of client apps.
+ *
+ * Each client app in the list will be shown as an item with Edit/Remove buttons.
+ *
+ * The list of apps are filterable.
+ */
+OAuth2ClientAppItemView = Djblets.Config.ListItemView.extend({
+    actionHandlers: {
+        'edit': '_onEditClicked',
+        'remove': '_onRemoveClicked'
+    },
+
+    template: _.template([
+        '<div class="oauth2">',
+        ' <span class="client-app-name"><%- name %></span>',
+        ' <b>ID:</b><span><%- clientId %></span>',
+        '</div>'
+    ].join('')),
+
+    /*
+     * Handler for when Edit button is clicked.
+     *
+     * Bring up the client app form modal for user to edit the client app.
+     */
+    _onEditClicked: function() {
+        var view = new OAuth2ClientAppFormView({
+            title: gettext('Edit client application'),
+            saveButtonText: gettext('Save'),
+            clientAppName: this.model.get('clientAppName')
+        });
+        view.render();
+    },
+
+    /*
+     * Handler for when Remove button is clicked.
+     *
+     * Confirm with use before removing the client app through webAPI.
+     */
+    _onRemoveClicked: function() {
+        $('<p/>')
+            .html(gettext('This will prevent all users from using this client application'))
+            .modalBox({
+                title: gettext('Are you sure you want to remove this client application ?'),
+                buttons: [
+                    $('<input type="button"/>')
+                        .val(gettext('Cancel')),
+                    $('<input type="button" class="danger" />')
+                        .val(gettext('Remove'))
+                        .click(_.bind(function() {
+                            this.model.resource.destroy();
+                        }, this))
+                ]
+            });
+    }
+});
+
+
+/*
+ * A form to edit or add a OAuth2 client app.
+ */
+OAuth2ClientAppFormView = Backbone.View.extend({
+    id: 'oauth2_client_app_form',
+
+    template: _.template([
+        '<div class="oauth2">',
+        ' <% if (typeof(clientAppName) !== "undefined") { %>',
+        '  <h3><%- clientAppName %></h3>',
+        ' <% } %>',
+        ' <div class="client-app-form">',
+        '  <div>',
+        '   <label><%- textDisplay.clientName %></label>',
+        '   <input type="text"/>',
+        '  </div>',
+
+        '  <div>',
+        '   <label><%- textDisplay.clientId %></label>',
+        '   <input type="text"/>',
+        '  </div>',
+
+        '  <div>',
+        '   <label><%- textDisplay.clientSecret %></label>',
+        '   <input type="text"/>',
+        '  </div>',
+
+        '  <div>',
+        '   <label><%- textDisplay.clientType %></label>',
+        '   <div>',
+        '    <select>',
+        '     <option value="confidential" selected="selected">Confidential</option>',
+        '     <option value="public">Public</option>',
+        '    </select>',
+        '   </div>',
+        '  </div>',
+
+        '  <div>',
+        '   <label><%- textDisplay.authorizationGrantType %></label>',
+        '   <div>',
+        '    <select>',
+        '     <option value="authorization-code" selected="selected">Authorization code</option>',
+        '     <option value="implicit">Implicit</option>',
+        '     <option value="password">Resource owner password-based</option>',
+        '     <option value="client-credentials">Client credentials</option>',
+        '    </select>',
+        '   </div>',
+        '  </div>',
+
+        '  <div>',
+        '   <label><%- textDisplay.redirectUri %></label>',
+        '   <input type="text"/>',
+        '  </div>',
+        ' </div>',
+        '</div>',
+    ].join('')),
+
+    events: {
+        'click #btn-add-redirected-uri': '_onAddRedirectURIClicked'
+    },
+
+    /*
+     * Initialize the form.
+     */
+    initialize: function(options) {
+        this.title = options.title;
+        this.saveButtonText = options.saveButtonText;
+        this.clientAppName = options.clientAppName;
+        this.redirectURLs = [];
+    },
+
+    /*
+     * Render the form.
+     *
+     * Returns:
+     *      OAuth2ClientAppFormView: The current view.
+     */
+    render: function() {
+        this.$el.html(this.template({
+            clientAppName: this.clientAppName,
+            textDisplay: {
+                clientName: gettext('Client name'),
+                clientId: gettext('Client id'),
+                clientSecret: gettext('Client secret'),
+                clientType: gettext('Client type'),
+                authorizationGrantType: gettext('Authorization grant type'),
+                redirectUri: gettext('Redirect uri')
+            }
+        }));
+
+        this.$el.modalBox({
+            title: this.title,
+            buttons: [
+                $('<input type="button"/>')
+                    .val(gettext('Cancel')),
+                $('<input type="button" class="btn primary save-button"/>')
+                    .val(this.saveButtonText)
+                    .click(_.bind(function() {
+                        this.save();
+                    }, this)),
+            ]
+        });
+
+        this._$redirectURIsContainer = this.$('#oauth2-redirect-uris');
+        this._$saveButtons = this.$el.modalBox('buttons').find('.save-button');
+        return this;
+    },
+
+    /*
+     * Handler for when Add new redirect url button is clicked.
+     *
+     * Add new OAuth2RedirectedURI view to the form.
+     */
+    _onAddRedirectURIClicked: function() {
+        var newRedirectURI = new OAuth2RedirectedURI();
+        this.redirectURLs.push(newRedirectURI);
+
+        newRedirectURI.render();
+        newRedirectURI.$el.appendTo(this._$redirectURIsContainer);
+    }
+});
+
+
+/*
+ * Provides UI for managing a user's OAuth2 client apps.
+ *
+ * All apps will be shown to the user.
+ * This list is filterable through a search box at the top of the view.
+ *
+ * Each app entry provides a button for editing or removing the app,
+ * allowing users to manage their OAuth2 client apps.
+ */
+RB.OAuth2ClientAppsView = Backbone.View.extend({
+    template: _.template([
+        '<div class="oauth2">',
+        ' <div class="search">',
+        '  <span class="rb-icon rb-icon-search-dark"></span>',
+        '  <input id="client-app-search" type="text"/>',
+        ' </div>',
+        ' <div id="client-app-list"></div>',
+        ' <p>',
+        '  <a id="client-app-add-new" class="btn primary">',
+        '  <%- textDisplay.addClientApp %>',
+        '  </a>',
+        ' </p>',
+        '</div>'
+    ].join('')),
+
+    events: {
+        'keyup #client-app-search': '_onAppSearchChanged',
+        'change #client-app-search': '_onAppSearchChanged',
+        'click #client-app-add-new': '_onAddNewClientAppClicked'
+    },
+
+    /*
+     * Initialize the view.
+     */
+    initialize: function(options) {
+        this.oauth2_client_apps = options.oauth2_client_apps;
+        this._$listsContainer = null;
+        this._$search = null;
+        this._searchText = null;
+        this._appViews = [];
+
+        this.collection = new RB.FilteredCollection(null, {
+            collection: new Backbone.Collection(this.oauth2_client_apps, {
+                model: OAuth2ClientAppItem
+            })
+        });
+
+        this.appList = new Djblets.Config.List({}, {
+            collection: this.collection
+        });
+    },
+
+    /*
+     * Render the view.
+     *
+     * This will set up the elements and the list of OAuth2ClientAppItemView.
+     *
+     * Returns:
+     *       OAuth2ClientAppsView: The current view.
+     */
+    render: function() {
+        this.$el.html(this.template({
+            textDisplay: {
+                addClientApp: gettext('Add Client Application')
+            }
+        }));
+
+        this._$listsContainer = this.$('#client-app-list');
+        this._$search = this.$('#client-app-search');
+
+        this._listView = new Djblets.Config.ListView({
+            ItemView: OAuth2ClientAppItemView,
+            model: this.appList
+        });
+
+        this._listView.$el
+            .addClass('box-recessed')
+            .appendTo(this._$listsContainer);
+
+        this._listView.render();
+        return this;
+    },
+
+    /*
+     * Handler for when the search box changes.
+     *
+     * This will instruct the filter to filter their contents
+     * by the text entered into the search box.
+     */
+    _onAppSearchChanged: function() {
+        var text = this._$search.val();
+
+        if (text !== this._searchText) {
+            this._searchText = text;
+
+            this.collection.setFilters({
+                'name': text
+            });
+        }
+    },
+
+    /*
+     * Handler for when the add new client app button is clicked.
+     *
+     * Bring up the client app form modal for user to add a new client app.
+     */
+    _onAddNewClientAppClicked: function(){
+        var view = new OAuth2ClientAppFormView({
+            title: gettext('Add client application'),
+            saveButtonText: gettext('Add new client'),
+        });
+        view.render();
+    }
+});
+
+
+})();
diff --git a/reviewboard/static/rb/js/resources/models/oauth2ClientAppModel.js b/reviewboard/static/rb/js/resources/models/oauth2ClientAppModel.js
new file mode 100644
index 0000000000000000000000000000000000000000..b15149420136d46ced3332652c94a54fe147435d
--- /dev/null
+++ b/reviewboard/static/rb/js/resources/models/oauth2ClientAppModel.js
@@ -0,0 +1,30 @@
+/*
+ * An resource representing an OAuth2 client application created by the user.
+ */
+RB.OAuth2ClientApp = RB.BaseResource.extend({
+    defaults: function() {
+        return _.defaults({
+        }, RB.BaseResource.prototype.defaults());
+    },
+
+    rspNamespace: 'oauth2_client_app',
+
+    /*
+     * Return api url to access the resource.
+     * The url contains an ID in the end if the request is not to create
+     * a new OAuth2ClientApp. Otherwise there will be no ID.
+     *
+     * Returns:
+     *      The api url to access the resource.
+     */
+    url: function() {
+        var url = SITE_ROOT +
+                  'api/users/' + this.get('userName') + '/oauth2-client-apps/';
+
+        if (!this.isNew()) {
+            url += this.id + '/';
+        }
+
+        return url;
+    }
+});
diff --git a/reviewboard/staticbundles.py b/reviewboard/staticbundles.py
index e7d5f359aab79998fe498ea9905b99773e6139a5..0d614574e5dbaf9710ec286f27a33886d100556d 100644
--- a/reviewboard/staticbundles.py
+++ b/reviewboard/staticbundles.py
@@ -142,6 +142,7 @@ PIPELINE_JAVASCRIPT = dict({
             'rb/js/resources/models/generalCommentReplyModel.js',
             'rb/js/resources/models/fileDiffModel.js',
             'rb/js/resources/models/draftFileAttachmentModel.js',
+            'rb/js/resources/models/oauth2ClientAppModel.js',
             'rb/js/resources/models/repositoryModel.js',
             'rb/js/resources/models/reviewGroupModel.js',
             'rb/js/resources/models/reviewReplyModel.js',
@@ -167,6 +168,7 @@ PIPELINE_JAVASCRIPT = dict({
             'rb/js/accountPrefsPage/views/accountPrefsPageView.js',
             'rb/js/accountPrefsPage/views/apiTokensView.js',
             'rb/js/accountPrefsPage/views/joinedGroupsView.js',
+            'rb/js/accountPrefsPage/views/oauth2ClientAppsView.js',
         ),
         'output_filename': 'rb/js/account-page.min.js',
     },
