diff --git a/front/package.json b/front/package.json
index 004370f654a71e3bace6673319c87aded82fc94b..4a85f6469ebb2d6ab2898f7586f6eb9b7d5b30b1 100644
--- a/front/package.json
+++ b/front/package.json
@@ -16,6 +16,7 @@
   },
   "dependencies": {
     "axios": "^0.18.0",
+    "axios-auth-refresh": "^2.2.6",
     "core-js": "^3.6.4",
     "diff": "^4.0.1",
     "django-channels": "^1.1.6",
diff --git a/front/src/components/auth/Authorize.vue b/front/src/components/auth/Authorize.vue
index 91cd0e71fd9a571cd194cfc9c8093d1cec2a6f07..1ef8c97bc913735cf5eeb6153454569429bbd667 100644
--- a/front/src/components/auth/Authorize.vue
+++ b/front/src/components/auth/Authorize.vue
@@ -93,6 +93,8 @@ export default {
         {id: "filters", icon: 'eye slash'},
         {id: "notifications", icon: 'bell'},
         {id: "edits", icon: 'pencil alternate'},
+        {id: "security", icon: 'lock'},
+        {id: "reports", icon: 'warning sign'},
       ]
     }
   },
diff --git a/front/src/components/auth/LoginForm.vue b/front/src/components/auth/LoginForm.vue
index 5e9ec22a19f6a7a6f08e784fa114d2f3c74fb526..ee0b2cccc1817247b77a5d9e43bca58fd8988ead 100644
--- a/front/src/components/auth/LoginForm.vue
+++ b/front/src/components/auth/LoginForm.vue
@@ -1,5 +1,5 @@
 <template>
-  <form class="ui form" @submit.prevent="submit()">
+  <form class="ui form" @submit.prevent="submit">
     <div v-if="error" class="ui negative message">
       <div class="header"><translate translate-context="Content/Login/Error message.Title">We cannot log you in</translate></div>
       <ul class="list">
@@ -12,39 +12,46 @@
         <li v-else>{{ error }}</li>
       </ul>
     </div>
-    <div class="field">
-      <label>
-        <translate translate-context="Content/Login/Input.Label/Noun">Username or email</translate>
-        <template v-if="showSignup">
-          |
-          <router-link :to="{path: '/signup'}">
-            <translate translate-context="*/Signup/Link/Verb">Create an account</translate>
+    <template v-if="$store.getters['instance/appDomain'] === $store.getters['instance/domain']" >
+      <div class="field">
+        <label>
+          <translate translate-context="Content/Login/Input.Label/Noun">Username or email</translate>
+          <template v-if="showSignup">
+            |
+            <router-link :to="{path: '/signup'}">
+              <translate translate-context="*/Signup/Link/Verb">Create an account</translate>
+            </router-link>
+          </template>
+        </label>
+        <input
+        ref="username"
+        tabindex="1"
+        required
+        name="username"
+        type="text"
+        autofocus
+        :placeholder="labels.usernamePlaceholder"
+        v-model="credentials.username"
+        >
+      </div>
+      <div class="field">
+        <label>
+          <translate translate-context="*/*/*">Password</translate> |
+          <router-link :to="{name: 'auth.password-reset', query: {email: credentials.username}}">
+            <translate translate-context="*/Login/*/Verb">Reset your password</translate>
           </router-link>
-        </template>
-      </label>
-      <input
-      ref="username"
-      tabindex="1"
-      required
-      name="username"
-      type="text"
-      autofocus
-      :placeholder="labels.usernamePlaceholder"
-      v-model="credentials.username"
-      >
-    </div>
-    <div class="field">
-      <label>
-        <translate translate-context="*/*/*">Password</translate> |
-        <router-link :to="{name: 'auth.password-reset', query: {email: credentials.username}}">
-          <translate translate-context="*/Login/*/Verb">Reset your password</translate>
-        </router-link>
-      </label>
-      <password-input :index="2" required v-model="credentials.password" />
+        </label>
+        <password-input :index="2" required v-model="credentials.password" />
 
-    </div>
+      </div>
+    </template>
+    <template v-else>
+      <p>
+        <translate translate-context="Contant/Auth/Paragraph" :translate-params="{domain: $store.getters['instance/domain']}">You will be redirected to %{ domain } to authenticate.</translate>
+      </p>
+    </template>
     <button tabindex="3" :class="['ui', {'loading': isLoading}, 'right', 'floated', buttonClasses, 'button']" type="submit">
-        <translate translate-context="*/Login/*/Verb">Login</translate>
+      <translate translate-context="*/Login/*/Verb">Login</translate>
     </button>
   </form>
 </template>
@@ -79,7 +86,9 @@ export default {
     }
   },
   mounted() {
-    this.$refs.username.focus()
+    if (this.$refs.username) {
+      this.$refs.username.focus()
+    }
   },
   computed: {
     labels() {
@@ -90,7 +99,15 @@ export default {
     }
   },
   methods: {
-    submit() {
+    async submit() {
+      if (this.$store.getters['instance/appDomain'] === this.$store.getters['instance/domain']) {
+        return await this.submitSession()
+      } else {
+        this.isLoading = true
+        await this.$store.dispatch('auth/oauthLogin', this.next)
+      }
+    },
+    async submitSession() {
       var self = this
       self.isLoading = true
       this.error = ""
diff --git a/front/src/main.js b/front/src/main.js
index 9047d67440be9cd6e1febe7bab59fb96e81cef34..514a5539a206698af9381f9f72321b4b4c081606 100644
--- a/front/src/main.js
+++ b/front/src/main.js
@@ -16,6 +16,7 @@ import store from './store'
 import GetTextPlugin from 'vue-gettext'
 import { sync } from 'vuex-router-sync'
 import locales from '@/locales'
+import createAuthRefreshInterceptor from 'axios-auth-refresh';
 
 import filters from '@/filters' // eslint-disable-line
 import {parseAPIErrors} from '@/utils'
@@ -73,7 +74,7 @@ axios.defaults.xsrfHeaderName = 'X-CSRFToken'
 axios.interceptors.request.use(function (config) {
 
   // Do something before request is sent
-  if (store.state.auth.token) {
+  if (store.state.auth.oauth.accessToken) {
     config.headers['Authorization'] = store.getters['auth/header']
   }
   return config
@@ -87,7 +88,7 @@ axios.interceptors.response.use(function (response) {
   return response
 }, function (error) {
   error.backendErrors = []
-  if (store.state.auth.authenticated && error.response.status === 401) {
+  if (store.state.auth.authenticated && !store.state.auth.oauth.accessToken && error.response.status === 401) {
     store.commit('auth/authenticated', false)
     logger.default.warn('Received 401 response from API, redirecting to login form', router.currentRoute.fullPath)
     router.push({name: 'login', query: {next: router.currentRoute.fullPath}})
@@ -140,6 +141,21 @@ axios.interceptors.response.use(function (response) {
   return Promise.reject(error)
 })
 
+const refreshAuth = (failedRequest) => {
+  if (store.state.auth.oauth.accessToken) {
+    console.log('Failed request, refreshing auth…')
+    // maybe the token was expired, let's try to refresh it
+    return store.dispatch('auth/refreshOauthToken').then(() => {
+      failedRequest.response.config.headers['Authorization'] = store.getters["auth/header"];
+      return Promise.resolve();
+    })
+  } else {
+    return Promise.resolve();
+  }
+}
+
+createAuthRefreshInterceptor(axios, refreshAuth);
+
 store.dispatch('instance/fetchFrontSettings').finally(() => {
   /* eslint-disable no-new */
   new Vue({
diff --git a/front/src/router/index.js b/front/src/router/index.js
index 5c6a42e241888b343dd39c4cf8c80e047ce21c3d..87d571cda946e3d99a66056d1dff2ae029bd9d55 100644
--- a/front/src/router/index.js
+++ b/front/src/router/index.js
@@ -65,6 +65,16 @@ export default new Router({
         defaultEmail: route.query.email
       })
     },
+    {
+      path: "/auth/callback",
+      name: "auth.callback",
+      component: () =>
+        import(/* webpackChunkName: "auth-callback" */ "@/views/auth/Callback"),
+      props: route => ({
+        code: route.query.code,
+        state: route.query.state,
+      })
+    },
     {
       path: "/auth/email/confirm",
       name: "auth.email-confirm",
diff --git a/front/src/store/auth.js b/front/src/store/auth.js
index 8919dc12265a84a329e167b51e853f7780599f4c..ed69bfc448a4910da68bb1f8f57a9be0d2edcf08 100644
--- a/front/src/store/auth.js
+++ b/front/src/store/auth.js
@@ -9,6 +9,41 @@ function getDefaultScopedTokens () {
     listen: null,
   }
 }
+
+function asForm (obj) {
+  let data = new FormData()
+  Object.entries(obj).forEach((e) => {
+    data.set(e[0], e[1])
+  })
+  return data
+}
+
+
+let baseUrl = `${window.location.protocol}//${window.location.hostname}`
+if (window.location.port) {
+  baseUrl = `${baseUrl}:${window.location.port}`
+}
+function getDefaultOauth () {
+  return {
+    clientId: null,
+    clientSecret: null,
+    accessToken: null,
+    refreshToken: null,
+  }
+}
+const NEEDED_SCOPES = [
+  "read",
+  "write",
+].join(' ')
+async function createOauthApp(domain) {
+  const payload = {
+    name: `Funkwhale web client at ${window.location.hostname}`,
+    website: baseUrl,
+    scopes: NEEDED_SCOPES,
+    redirect_uris: `${baseUrl}/auth/callback`
+  }
+  return (await axios.post('oauth/apps/', payload)).data
+}
 export default {
   namespaced: true,
   state: {
@@ -22,12 +57,13 @@ export default {
     },
     profile: null,
     token: '',
+    oauth: getDefaultOauth(),
     scopedTokens: getDefaultScopedTokens()
   },
   getters: {
     header: state => {
-      if (state.token) {
-        return 'JWT ' + state.token
+      if (state.oauth.accessToken) {
+        return 'Bearer ' + state.oauth.accessToken
       }
     }
   },
@@ -39,6 +75,7 @@ export default {
       state.fullUsername = ''
       state.token = ''
       state.scopedTokens = getDefaultScopedTokens()
+      state.oauth = getDefaultOauth()
       state.availablePermissions = {
         federation: false,
         settings: false,
@@ -84,6 +121,14 @@ export default {
       lodash.keys(payload).forEach((k) => {
         Vue.set(state.profile, k, payload[k])
       })
+    },
+    oauthApp: (state, payload) => {
+      state.oauth.clientId = payload.client_id
+      state.oauth.clientSecret = payload.client_secret
+    },
+    oauthToken: (state, payload) => {
+      state.oauth.accessToken = payload.access_token
+      state.oauth.refreshToken = payload.refresh_token
     }
   },
   actions: {
@@ -105,8 +150,12 @@ export default {
         onError(response)
       })
     },
-    async logout ({commit}) {
-      await axios.post('users/logout')
+    async logout ({state, commit}) {
+      try {
+        await axios.post('users/logout')
+      } catch {
+        console.log('Error while logging out, probably logged in via oauth')
+      }
       let modules = [
         'auth',
         'favorites',
@@ -177,5 +226,45 @@ export default {
         resolve()
       })
     },
+    async oauthLogin({ state, rootState, commit, getters }, next) {
+      let app = await createOauthApp(getters["appDomain"])
+      commit("oauthApp", app)
+      const redirectUri = encodeURIComponent(`${baseUrl}/auth/callback`)
+      let params = `response_type=code&scope=${encodeURIComponent(NEEDED_SCOPES)}&redirect_uri=${redirectUri}&state=${next}&client_id=${state.oauth.clientId}`
+      const authorizeUrl = `${rootState.instance.instanceUrl}authorize?${params}`
+      console.log('Redirecting user...', authorizeUrl)
+      window.location = authorizeUrl
+    },
+    async handleOauthCallback({ state, commit, dispatch }, authorizationCode) {
+      console.log('Fetching token...')
+      const payload = {
+        client_id: state.oauth.clientId,
+        client_secret: state.oauth.clientSecret,
+        grant_type: "authorization_code",
+        code: authorizationCode,
+        redirect_uri: `${baseUrl}/auth/callback`
+      }
+      const response = await axios.post(
+        'oauth/token/',
+        asForm(payload),
+        {headers: {'Content-Type': 'multipart/form-data'}}
+      )
+      commit("oauthToken", response.data)
+      await dispatch('fetchProfile')
+    },
+    async refreshOauthToken({ state, commit }, authorizationCode) {
+      const payload = {
+        client_id: state.oauth.clientId,
+        client_secret: state.oauth.clientSecret,
+        grant_type: "refresh_token",
+        refresh_token: state.oauth.refreshToken,
+      }
+      let response = await axios.post(
+        `oauth/token/`,
+        asForm(payload),
+        {headers: {'Content-Type': 'multipart/form-data'}}
+      )
+      commit('oauthToken', response.data)
+    },
   }
 }
diff --git a/front/src/store/instance.js b/front/src/store/instance.js
index 5d70688dcd1cc2e553d8b508b3d3575eafb1eb50..1f251bca9d00975654e8c5cfe887a7a7179cfa5e 100644
--- a/front/src/store/instance.js
+++ b/front/src/store/instance.js
@@ -124,6 +124,9 @@ export default {
       let parser = document.createElement("a")
       parser.href = url
       return parser.hostname
+    },
+    appDomain: (state) => {
+      return document.domain
     }
   },
   actions: {
diff --git a/front/src/views/auth/Callback.vue b/front/src/views/auth/Callback.vue
new file mode 100644
index 0000000000000000000000000000000000000000..47e970a0ac73f26d998ab13f28c2e7acabba54df
--- /dev/null
+++ b/front/src/views/auth/Callback.vue
@@ -0,0 +1,25 @@
+<template>
+  <main class="main pusher">
+    <section class="ui vertical stripe segment">
+      <div class="ui small text container">
+        <div class="ui hidden divider"></div>
+        <div class="ui active inverted dimmer">
+          <div class="ui text loader">
+            <h2><translate translate-context="*/Login/*">Logging in…</translate></h2> {{ next }} {{ code }} {{ state }}
+          </div>
+        </div>
+      </div>
+    </section>
+  </main>
+</template>
+
+<script>
+
+export default {
+  props: ["state", "code"],
+  async mounted () {
+    await this.$store.dispatch('auth/handleOauthCallback', this.code)
+    this.$router.push(this.state || '/library')
+  }
+}
+</script>
diff --git a/front/tests/unit/specs/store/auth.spec.js b/front/tests/unit/specs/store/auth.spec.js
index 63c6d2da0667316c45b983c0365e2bf418c6946a..3de563df3efe2e41067a2d68ba0d8acbfd60d75e 100644
--- a/front/tests/unit/specs/store/auth.spec.js
+++ b/front/tests/unit/specs/store/auth.spec.js
@@ -67,8 +67,8 @@ describe('store/auth', () => {
   })
   describe('getters', () => {
     it('header', () => {
-      const state = { token: 'helloworld' }
-      expect(store.getters['header'](state)).to.equal('JWT helloworld')
+      const state = { oauth: {accessToken: 'helloworld' }}
+      expect(store.getters['header'](state)).to.equal('Bearer helloworld')
     })
   })
   describe('actions', () => {
diff --git a/front/yarn.lock b/front/yarn.lock
index 8d39ab4d2c5c1f9ee887d4b956c9192711a62282..40aa5b9d00c10356d2c6ba8622a62a0fd9f5a1f8 100644
--- a/front/yarn.lock
+++ b/front/yarn.lock
@@ -1934,6 +1934,11 @@ aws4@^1.8.0:
   resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
   integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==
 
+axios-auth-refresh@^2.2.6:
+  version "2.2.6"
+  resolved "https://registry.yarnpkg.com/axios-auth-refresh/-/axios-auth-refresh-2.2.6.tgz#22cde7c961d4caa879da6337947301a63209cdd3"
+  integrity sha512-L3djMIUi/WTIzItr8VO9dFAraPRO9+T4sGkz5VEcxyDyX/gzPVhVvpWHwnqKxhojXZnMrTlZGIs98P12+ba/Ew==
+
 axios@^0.18.0:
   version "0.18.1"
   resolved "https://registry.yarnpkg.com/axios/-/axios-0.18.1.tgz#ff3f0de2e7b5d180e757ad98000f1081b87bcea3"