diff --git a/api/config/settings/common.py b/api/config/settings/common.py
index 9804bb9c08d133b74bfd08440f5d207314ded2cf..7dffebe94ba9e528006778a1ca924626449e8f15 100644
--- a/api/config/settings/common.py
+++ b/api/config/settings/common.py
@@ -280,8 +280,9 @@ JWT_AUTH = {
     'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7),
     'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=30),
     'JWT_AUTH_HEADER_PREFIX': 'JWT',
+    'JWT_GET_USER_SECRET_KEY': lambda user: user.secret_key
 }
-
+OLD_PASSWORD_FIELD_ENABLED = True
 ACCOUNT_ADAPTER = 'funkwhale_api.users.adapters.FunkwhaleAccountAdapter'
 CORS_ORIGIN_ALLOW_ALL = True
 # CORS_ORIGIN_WHITELIST = (
diff --git a/api/funkwhale_api/users/migrations/0003_auto_20171226_1357.py b/api/funkwhale_api/users/migrations/0003_auto_20171226_1357.py
new file mode 100644
index 0000000000000000000000000000000000000000..fd75795d3fae3b63cb1e5f8830d1aaec7acf2118
--- /dev/null
+++ b/api/funkwhale_api/users/migrations/0003_auto_20171226_1357.py
@@ -0,0 +1,24 @@
+# Generated by Django 2.0 on 2017-12-26 13:57
+
+from django.db import migrations, models
+import uuid
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('users', '0002_auto_20171214_2205'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='user',
+            name='secret_key',
+            field=models.UUIDField(default=uuid.uuid4, null=True),
+        ),
+        migrations.AlterField(
+            model_name='user',
+            name='last_name',
+            field=models.CharField(blank=True, max_length=150, verbose_name='last name'),
+        ),
+    ]
diff --git a/api/funkwhale_api/users/models.py b/api/funkwhale_api/users/models.py
index c8d0b534c8b84706fcd050eee819e889e186f525..3a0baf11a30c73397b2c31d14fe1ec29d9557a7c 100644
--- a/api/funkwhale_api/users/models.py
+++ b/api/funkwhale_api/users/models.py
@@ -1,6 +1,8 @@
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals, absolute_import
 
+import uuid
+
 from django.contrib.auth.models import AbstractUser
 from django.urls import reverse
 from django.db import models
@@ -15,6 +17,8 @@ class User(AbstractUser):
     # around the globe.
     name = models.CharField(_("Name of User"), blank=True, max_length=255)
 
+    # updated on logout or password change, to invalidate JWT
+    secret_key = models.UUIDField(default=uuid.uuid4, null=True)
     # permissions that are used for API access and that worth serializing
     relevant_permissions = {
         # internal_codename : {external_codename}
@@ -31,3 +35,11 @@ class User(AbstractUser):
 
     def get_absolute_url(self):
         return reverse('users:detail', kwargs={'username': self.username})
+
+    def update_secret_key(self):
+        self.secret_key = uuid.uuid4()
+        return self.secret_key
+
+    def set_password(self, raw_password):
+        super().set_password(raw_password)
+        self.update_secret_key()
diff --git a/api/funkwhale_api/users/rest_auth_urls.py b/api/funkwhale_api/users/rest_auth_urls.py
index 9770e69e467cfb6ed377ff09ccbf6e81975be213..31f5384aa7f2a750bcaa4fc9063658876fbbd968 100644
--- a/api/funkwhale_api/users/rest_auth_urls.py
+++ b/api/funkwhale_api/users/rest_auth_urls.py
@@ -2,11 +2,15 @@ from django.views.generic import TemplateView
 from django.conf.urls import url
 
 from rest_auth.registration.views import VerifyEmailView
+from rest_auth.views import PasswordChangeView
+
 from .views import RegisterView
 
+
 urlpatterns = [
     url(r'^$', RegisterView.as_view(), name='rest_register'),
     url(r'^verify-email/$', VerifyEmailView.as_view(), name='rest_verify_email'),
+    url(r'^change-password/$', PasswordChangeView.as_view(), name='change_password'),
 
     # This url is used by django-allauth and empty TemplateView is
     # defined just to allow reverse() call inside app, for example when email
diff --git a/api/tests/users/test_jwt.py b/api/tests/users/test_jwt.py
new file mode 100644
index 0000000000000000000000000000000000000000..d264494e59bfb06f358e7dd83225cf4eef187c0f
--- /dev/null
+++ b/api/tests/users/test_jwt.py
@@ -0,0 +1,27 @@
+import pytest
+import uuid
+
+from jwt.exceptions import DecodeError
+from rest_framework_jwt.settings import api_settings
+
+from funkwhale_api.users.models import User
+
+def test_can_invalidate_token_when_changing_user_secret_key(factories):
+    user = factories['users.User']()
+    u1 = user.secret_key
+    jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
+    jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
+    payload = jwt_payload_handler(user)
+    payload = jwt_encode_handler(payload)
+
+    # this should work
+    api_settings.JWT_DECODE_HANDLER(payload)
+
+    # now we update the secret key
+    user.update_secret_key()
+    user.save()
+    assert user.secret_key != u1
+
+    # token should be invalid
+    with pytest.raises(DecodeError):
+        api_settings.JWT_DECODE_HANDLER(payload)
diff --git a/api/tests/users/test_views.py b/api/tests/users/test_views.py
index 5dbb24ac67eae64b0f484beb95a1523d1b7a0333..1eb8ef222a79d68f41d40c8555c0c7cb9d931680 100644
--- a/api/tests/users/test_views.py
+++ b/api/tests/users/test_views.py
@@ -97,3 +97,22 @@ def test_can_refresh_token_via_api(client, factories):
     assert '"token":' in response.content.decode('utf-8')
     # a different token should be returned
     assert token in response.content.decode('utf-8')
+
+
+def test_changing_password_updates_secret_key(logged_in_client):
+    user = logged_in_client.user
+    password = user.password
+    secret_key = user.secret_key
+    payload = {
+        'old_password': 'test',
+        'new_password1': 'new',
+        'new_password2': 'new',
+    }
+    url = reverse('change_password')
+
+    response = logged_in_client.post(url, payload)
+
+    user.refresh_from_db()
+
+    assert user.secret_key != secret_key
+    assert user.password != password
diff --git a/front/src/components/auth/Profile.vue b/front/src/components/auth/Profile.vue
index 607fa8ff2b84ffdde60ccbf3b514938dfeb5aba4..54af5a11c4c0027fc68f81e9c6eca283d4ed8aee 100644
--- a/front/src/components/auth/Profile.vue
+++ b/front/src/components/auth/Profile.vue
@@ -17,6 +17,10 @@
           <i class="star icon"></i>
           Staff member
         </div>
+        <router-link class="ui tiny basic button" :to="{path: '/settings'}">
+          <i class="setting icon"> </i>Settings...
+        </router-link>
+
       </div>
     </template>
   </div>
diff --git a/front/src/components/auth/Settings.vue b/front/src/components/auth/Settings.vue
new file mode 100644
index 0000000000000000000000000000000000000000..d93373a1496937380eab3531855696289617996a
--- /dev/null
+++ b/front/src/components/auth/Settings.vue
@@ -0,0 +1,84 @@
+<template>
+  <div class="main pusher">
+    <div class="ui vertical stripe segment">
+      <div class="ui small text container">
+        <h2>Change my password</h2>
+        <form class="ui form" @submit.prevent="submit()">
+          <div v-if="error" class="ui negative message">
+            <div class="header">Cannot change your password</div>
+            <ul class="list">
+              <li v-if="error == 'invalid_credentials'">Please double-check your password is correct</li>
+            </ul>
+          </div>
+          <div class="field">
+            <label>Old password</label>
+            <input
+            required
+            type="password"
+            autofocus
+            placeholder="Enter your old password"
+            v-model="old_password">
+          </div>
+          <div class="field">
+            <label>New password</label>
+            <input
+            required
+            type="password"
+            autofocus
+            placeholder="Enter your new password"
+            v-model="new_password">
+          </div>
+          <button :class="['ui', {'loading': isLoading}, 'button']" type="submit">Change password</button>
+        </form>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import Vue from 'vue'
+import config from '@/config'
+import logger from '@/logging'
+
+export default {
+  data () {
+    return {
+      // We need to initialize the component with any
+      // properties that will be used in it
+      old_password: '',
+      new_password: '',
+      error: '',
+      isLoading: false
+    }
+  },
+  methods: {
+    submit () {
+      var self = this
+      self.isLoading = true
+      this.error = ''
+      var credentials = {
+        old_password: this.old_password,
+        new_password1: this.new_password,
+        new_password2: this.new_password
+      }
+      let resource = Vue.resource(config.BACKEND_URL + 'api/auth/registration/change-password/')
+      return resource.save({}, credentials).then(response => {
+        logger.default.info('Password successfully changed')
+        self.$router.push('/profile/me')
+      }, response => {
+        if (response.status === 400) {
+          self.error = 'invalid_credentials'
+        } else {
+          self.error = 'unknown_error'
+        }
+        self.isLoading = false
+      })
+    }
+  }
+
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+</style>
diff --git a/front/src/main.js b/front/src/main.js
index 0c9230e8e8acc3059bf7bf030f53e74e5a69e204..f7a6b65f4df7cdd2ddfc4b37914999b12b3e9186 100644
--- a/front/src/main.js
+++ b/front/src/main.js
@@ -31,6 +31,7 @@ Vue.http.interceptors.push(function (request, next) {
   next(function (response) {
     // redirect to login form when we get unauthorized response from server
     if (response.status === 401) {
+      store.commit('auth/authenticated', false)
       logger.default.warn('Received 401 response from API, redirecting to login form')
       router.push({name: 'login', query: {next: router.currentRoute.fullPath}})
     }
diff --git a/front/src/router/index.js b/front/src/router/index.js
index c7c46a27543bcd7b586c1e2679a0a32af3ba4597..f4efc723f4abc2fb9dfdca2e062d433c09d4e91e 100644
--- a/front/src/router/index.js
+++ b/front/src/router/index.js
@@ -4,6 +4,7 @@ import PageNotFound from '@/components/PageNotFound'
 import Home from '@/components/Home'
 import Login from '@/components/auth/Login'
 import Profile from '@/components/auth/Profile'
+import Settings from '@/components/auth/Settings'
 import Logout from '@/components/auth/Logout'
 import Library from '@/components/library/Library'
 import LibraryHome from '@/components/library/Home'
@@ -39,6 +40,11 @@ export default new Router({
       name: 'logout',
       component: Logout
     },
+    {
+      path: '/settings',
+      name: 'settings',
+      component: Settings
+    },
     {
       path: '/@:username',
       name: 'profile',
diff --git a/front/src/store/auth.js b/front/src/store/auth.js
index 815b0f70893da6fb0543b72c420dfcd9a02effc6..d8bd197f33fabd1938fe65a5c71ca950314b663f 100644
--- a/front/src/store/auth.js
+++ b/front/src/store/auth.js
@@ -29,13 +29,24 @@ export default {
     },
     authenticated: (state, value) => {
       state.authenticated = value
+      if (value === false) {
+        state.username = null
+        state.token = null
+        state.tokenData = null
+        state.profile = null
+        state.availablePermissions = {}
+      }
     },
     username: (state, value) => {
       state.username = value
     },
     token: (state, value) => {
       state.token = value
-      state.tokenData = jwtDecode(value)
+      if (value) {
+        state.tokenData = jwtDecode(value)
+      } else {
+        state.tokenData = {}
+      }
     },
     permission: (state, {key, status}) => {
       state.availablePermissions[key] = status
@@ -60,7 +71,6 @@ export default {
     },
     logout ({commit}) {
       commit('authenticated', false)
-      commit('profile', null)
       logger.default.info('Log out, goodbye!')
       router.push({name: 'index'})
     },