Resolve "Use cookies instead of local storage for auth in Web UI"
This commit is contained in:
parent
c0055b3b20
commit
c395076fce
|
@ -75,6 +75,9 @@ v1_patterns += [
|
||||||
r"^users/",
|
r"^users/",
|
||||||
include(("funkwhale_api.users.api_urls", "users"), namespace="users"),
|
include(("funkwhale_api.users.api_urls", "users"), namespace="users"),
|
||||||
),
|
),
|
||||||
|
url(
|
||||||
|
r"^auth/", include(("funkwhale_api.users.auth_urls", "auth"), namespace="auth")
|
||||||
|
),
|
||||||
url(r"^token/$", jwt_views.obtain_jwt_token, name="token"),
|
url(r"^token/$", jwt_views.obtain_jwt_token, name="token"),
|
||||||
url(r"^token/refresh/$", jwt_views.refresh_jwt_token, name="token_refresh"),
|
url(r"^token/refresh/$", jwt_views.refresh_jwt_token, name="token_refresh"),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||||
|
from channels.sessions import SessionMiddlewareStack
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from funkwhale_api.common.auth import TokenAuthMiddleware
|
from funkwhale_api.common.auth import TokenAuthMiddleware
|
||||||
|
@ -7,8 +8,12 @@ from funkwhale_api.instance import consumers
|
||||||
application = ProtocolTypeRouter(
|
application = ProtocolTypeRouter(
|
||||||
{
|
{
|
||||||
# Empty for now (http->django views is added by default)
|
# Empty for now (http->django views is added by default)
|
||||||
"websocket": TokenAuthMiddleware(
|
"websocket": SessionMiddlewareStack(
|
||||||
URLRouter([url("^api/v1/activity$", consumers.InstanceActivityConsumer)])
|
TokenAuthMiddleware(
|
||||||
|
URLRouter(
|
||||||
|
[url("^api/v1/activity$", consumers.InstanceActivityConsumer)]
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -330,7 +330,7 @@ AUTHENTICATION_BACKENDS = (
|
||||||
"funkwhale_api.users.auth_backends.ModelBackend",
|
"funkwhale_api.users.auth_backends.ModelBackend",
|
||||||
"allauth.account.auth_backends.AuthenticationBackend",
|
"allauth.account.auth_backends.AuthenticationBackend",
|
||||||
)
|
)
|
||||||
SESSION_COOKIE_HTTPONLY = False
|
SESSION_COOKIE_HTTPONLY = True
|
||||||
# Some really nice defaults
|
# Some really nice defaults
|
||||||
ACCOUNT_AUTHENTICATION_METHOD = "username_email"
|
ACCOUNT_AUTHENTICATION_METHOD = "username_email"
|
||||||
ACCOUNT_EMAIL_REQUIRED = True
|
ACCOUNT_EMAIL_REQUIRED = True
|
||||||
|
@ -537,7 +537,7 @@ MUSICBRAINZ_HOSTNAME = env("MUSICBRAINZ_HOSTNAME", default="musicbrainz.org")
|
||||||
|
|
||||||
# Custom Admin URL, use {% url 'admin:index' %}
|
# Custom Admin URL, use {% url 'admin:index' %}
|
||||||
ADMIN_URL = env("DJANGO_ADMIN_URL", default="^api/admin/")
|
ADMIN_URL = env("DJANGO_ADMIN_URL", default="^api/admin/")
|
||||||
CSRF_USE_SESSIONS = True
|
CSRF_USE_SESSIONS = False
|
||||||
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
|
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
|
||||||
|
|
||||||
# Playlist settings
|
# Playlist settings
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
|
from asgiref.sync import async_to_sync
|
||||||
from channels.generic.websocket import JsonWebsocketConsumer
|
from channels.generic.websocket import JsonWebsocketConsumer
|
||||||
|
from channels import auth
|
||||||
from funkwhale_api.common import channels
|
from funkwhale_api.common import channels
|
||||||
|
|
||||||
|
|
||||||
class JsonAuthConsumer(JsonWebsocketConsumer):
|
class JsonAuthConsumer(JsonWebsocketConsumer):
|
||||||
def connect(self):
|
def connect(self):
|
||||||
try:
|
if "user" not in self.scope:
|
||||||
assert self.scope["user"].pk is not None
|
try:
|
||||||
except (AssertionError, AttributeError, KeyError):
|
self.scope["user"] = async_to_sync(auth.get_user)(self.scope)
|
||||||
return self.close()
|
except (ValueError, AssertionError, AttributeError, KeyError):
|
||||||
|
return self.close()
|
||||||
|
|
||||||
return self.accept()
|
if self.scope["user"] and self.scope["user"].is_authenticated:
|
||||||
|
return self.accept()
|
||||||
|
else:
|
||||||
|
return self.close()
|
||||||
|
|
||||||
def accept(self):
|
def accept(self):
|
||||||
super().accept()
|
super().accept()
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
from django.contrib import auth
|
||||||
|
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
|
class LoginSerializer(serializers.Serializer):
|
||||||
|
username = serializers.CharField()
|
||||||
|
password = serializers.CharField()
|
||||||
|
|
||||||
|
def validate(self, validated_data):
|
||||||
|
user = auth.authenticate(request=None, **validated_data)
|
||||||
|
if user is None:
|
||||||
|
raise serializers.ValidationError("Invalid username or password")
|
||||||
|
|
||||||
|
validated_data["user"] = user
|
||||||
|
return validated_data
|
|
@ -0,0 +1,8 @@
|
||||||
|
from django.conf.urls import url
|
||||||
|
|
||||||
|
from . import auth_views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r"^login/$", auth_views.LoginView.as_view(), name="login"),
|
||||||
|
url(r"^logout/$", auth_views.LogoutView.as_view(), name="logout"),
|
||||||
|
]
|
|
@ -0,0 +1,32 @@
|
||||||
|
from django.contrib import auth
|
||||||
|
|
||||||
|
from rest_framework import response
|
||||||
|
from rest_framework import views
|
||||||
|
|
||||||
|
from . import auth_serializers
|
||||||
|
|
||||||
|
|
||||||
|
class LoginView(views.APIView):
|
||||||
|
authentication_classes = []
|
||||||
|
permission_classes = []
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
|
||||||
|
serializer = auth_serializers.LoginSerializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
auth.login(request=request, user=serializer.validated_data["user"])
|
||||||
|
|
||||||
|
payload = {}
|
||||||
|
|
||||||
|
return response.Response(payload)
|
||||||
|
|
||||||
|
|
||||||
|
class LogoutView(views.APIView):
|
||||||
|
authentication_classes = []
|
||||||
|
permission_classes = []
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
auth.logout(request)
|
||||||
|
payload = {}
|
||||||
|
return response.Response(payload)
|
|
@ -0,0 +1,86 @@
|
||||||
|
from django.contrib import auth
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
|
||||||
|
def test_restricted_access(api_client, db):
|
||||||
|
url = reverse("api:v1:artists-list")
|
||||||
|
response = api_client.get(url)
|
||||||
|
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
def test_login_correct(api_client, factories, mocker):
|
||||||
|
login = mocker.spy(auth, "login")
|
||||||
|
password = "hellotest"
|
||||||
|
user = factories["users.User"]()
|
||||||
|
user.set_password(password)
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
url = reverse("api:v1:auth:login")
|
||||||
|
data = {"username": user.username, "password": password}
|
||||||
|
expected = {}
|
||||||
|
response = api_client.post(url, data)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data == expected
|
||||||
|
login.assert_called_once_with(request=mocker.ANY, user=user)
|
||||||
|
|
||||||
|
|
||||||
|
def test_login_incorrect(api_client, factories, mocker):
|
||||||
|
login = mocker.spy(auth, "login")
|
||||||
|
user = factories["users.User"]()
|
||||||
|
|
||||||
|
url = reverse("api:v1:auth:login")
|
||||||
|
data = {"username": user.username, "password": "invalid"}
|
||||||
|
response = api_client.post(url, data)
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
|
||||||
|
login.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_login_inactive(api_client, factories, mocker):
|
||||||
|
login = mocker.spy(auth, "login")
|
||||||
|
password = "hellotest"
|
||||||
|
user = factories["users.User"](is_active=False)
|
||||||
|
user.set_password(password)
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
url = reverse("api:v1:auth:login")
|
||||||
|
data = {"username": user.username, "password": password}
|
||||||
|
response = api_client.post(url, data)
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert "Invalid username or password" in response.data["non_field_errors"]
|
||||||
|
|
||||||
|
login.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_logout(logged_in_api_client, factories, mocker):
|
||||||
|
logout = mocker.spy(auth, "logout")
|
||||||
|
|
||||||
|
url = reverse("api:v1:auth:logout")
|
||||||
|
response = logged_in_api_client.post(url)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data == {}
|
||||||
|
logout.assert_called_once_with(request=mocker.ANY)
|
||||||
|
|
||||||
|
|
||||||
|
def test_logout_real(api_client, factories):
|
||||||
|
password = "hellotest"
|
||||||
|
user = factories["users.User"]()
|
||||||
|
user.set_password(password)
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
url = reverse("api:v1:auth:login")
|
||||||
|
data = {"username": user.username, "password": password}
|
||||||
|
response = api_client.post(url, data)
|
||||||
|
|
||||||
|
url = reverse("api:v1:auth:logout")
|
||||||
|
response = api_client.post(url)
|
||||||
|
|
||||||
|
url = reverse("api:v1:artists-list")
|
||||||
|
response = api_client.get(url)
|
||||||
|
|
||||||
|
assert response.status_code == 401
|
|
@ -0,0 +1 @@
|
||||||
|
Now use cookie-based auth in browser instead of JWT/LocalStorage in (#629)
|
|
@ -17,7 +17,6 @@
|
||||||
"django-channels": "^1.1.6",
|
"django-channels": "^1.1.6",
|
||||||
"howler": "^2.0.14",
|
"howler": "^2.0.14",
|
||||||
"js-logger": "^1.4.1",
|
"js-logger": "^1.4.1",
|
||||||
"jwt-decode": "^2.2.0",
|
|
||||||
"lodash": "^4.17.10",
|
"lodash": "^4.17.10",
|
||||||
"masonry-layout": "^4.2.2",
|
"masonry-layout": "^4.2.2",
|
||||||
"moment": "^2.22.2",
|
"moment": "^2.22.2",
|
||||||
|
|
|
@ -175,11 +175,9 @@ export default {
|
||||||
}
|
}
|
||||||
this.disconnect()
|
this.disconnect()
|
||||||
let self = this
|
let self = this
|
||||||
let token = this.$store.state.auth.token
|
|
||||||
// let token = 'test'
|
|
||||||
const bridge = new WebSocketBridge()
|
const bridge = new WebSocketBridge()
|
||||||
this.bridge = bridge
|
this.bridge = bridge
|
||||||
let url = this.$store.getters['instance/absoluteUrl'](`api/v1/activity?token=${token}`)
|
let url = this.$store.getters['instance/absoluteUrl'](`api/v1/activity`)
|
||||||
url = url.replace('http://', 'ws://')
|
url = url.replace('http://', 'ws://')
|
||||||
url = url.replace('https://', 'wss://')
|
url = url.replace('https://', 'wss://')
|
||||||
bridge.connect(
|
bridge.connect(
|
||||||
|
|
|
@ -71,15 +71,6 @@ export default {
|
||||||
'mp3'
|
'mp3'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
if (this.$store.state.auth.authenticated) {
|
|
||||||
// we need to send the token directly in url
|
|
||||||
// so authentication can be checked by the backend
|
|
||||||
// because for audio files we cannot use the regular Authentication
|
|
||||||
// header
|
|
||||||
sources.forEach(e => {
|
|
||||||
e.url = url.updateQueryString(e.url, 'jwt', this.$store.state.auth.token)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return sources
|
return sources
|
||||||
},
|
},
|
||||||
updateProgressThrottled () {
|
updateProgressThrottled () {
|
||||||
|
|
|
@ -154,13 +154,6 @@ export default {
|
||||||
let u = this.$store.getters["instance/absoluteUrl"](
|
let u = this.$store.getters["instance/absoluteUrl"](
|
||||||
this.upload.listen_url
|
this.upload.listen_url
|
||||||
)
|
)
|
||||||
if (this.$store.state.auth.authenticated) {
|
|
||||||
u = url.updateQueryString(
|
|
||||||
u,
|
|
||||||
"jwt",
|
|
||||||
encodeURI(this.$store.state.auth.token)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return u
|
return u
|
||||||
},
|
},
|
||||||
cover() {
|
cover() {
|
||||||
|
|
|
@ -16,6 +16,7 @@ import GetTextPlugin from 'vue-gettext'
|
||||||
import { sync } from 'vuex-router-sync'
|
import { sync } from 'vuex-router-sync'
|
||||||
import locales from '@/locales'
|
import locales from '@/locales'
|
||||||
|
|
||||||
|
import cookie from '@/utils/cookie'
|
||||||
import filters from '@/filters' // eslint-disable-line
|
import filters from '@/filters' // eslint-disable-line
|
||||||
import globals from '@/components/globals' // eslint-disable-line
|
import globals from '@/components/globals' // eslint-disable-line
|
||||||
|
|
||||||
|
@ -70,8 +71,9 @@ Vue.directive('title', function (el, binding) {
|
||||||
)
|
)
|
||||||
axios.interceptors.request.use(function (config) {
|
axios.interceptors.request.use(function (config) {
|
||||||
// Do something before request is sent
|
// Do something before request is sent
|
||||||
if (store.state.auth.token) {
|
let csrfToken = cookie.get('csrftoken')
|
||||||
config.headers['Authorization'] = store.getters['auth/header']
|
if (csrfToken) {
|
||||||
|
config.headers['X-CSRFToken'] = csrfToken
|
||||||
}
|
}
|
||||||
return config
|
return config
|
||||||
}, function (error) {
|
}, function (error) {
|
||||||
|
@ -85,9 +87,14 @@ axios.interceptors.response.use(function (response) {
|
||||||
}, function (error) {
|
}, function (error) {
|
||||||
error.backendErrors = []
|
error.backendErrors = []
|
||||||
if (error.response.status === 401) {
|
if (error.response.status === 401) {
|
||||||
|
if (error.response.config.skipLoginRedirect) {
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
store.commit('auth/authenticated', false)
|
store.commit('auth/authenticated', false)
|
||||||
logger.default.warn('Received 401 response from API, redirecting to login form', router.currentRoute.fullPath)
|
if (router.currentRoute.name !== 'login') {
|
||||||
router.push({name: 'login', query: {next: router.currentRoute.fullPath}})
|
logger.default.warn('Received 401 response from API, redirecting to login form', router.currentRoute.fullPath)
|
||||||
|
router.push({name: 'login', query: {next: router.currentRoute.fullPath}})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (error.response.status === 404) {
|
if (error.response.status === 404) {
|
||||||
error.backendErrors.push('Resource not found')
|
error.backendErrors.push('Resource not found')
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import jwtDecode from 'jwt-decode'
|
|
||||||
import logger from '@/logging'
|
import logger from '@/logging'
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
|
|
||||||
|
@ -15,13 +14,6 @@ export default {
|
||||||
moderation: false
|
moderation: false
|
||||||
},
|
},
|
||||||
profile: null,
|
profile: null,
|
||||||
token: '',
|
|
||||||
tokenData: {}
|
|
||||||
},
|
|
||||||
getters: {
|
|
||||||
header: state => {
|
|
||||||
return 'JWT ' + state.token
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
reset (state) {
|
reset (state) {
|
||||||
|
@ -29,8 +21,6 @@ export default {
|
||||||
state.profile = null
|
state.profile = null
|
||||||
state.username = ''
|
state.username = ''
|
||||||
state.fullUsername = ''
|
state.fullUsername = ''
|
||||||
state.token = ''
|
|
||||||
state.tokenData = {}
|
|
||||||
state.availablePermissions = {
|
state.availablePermissions = {
|
||||||
federation: false,
|
federation: false,
|
||||||
settings: false,
|
settings: false,
|
||||||
|
@ -46,8 +36,6 @@ export default {
|
||||||
if (value === false) {
|
if (value === false) {
|
||||||
state.username = null
|
state.username = null
|
||||||
state.fullUsername = null
|
state.fullUsername = null
|
||||||
state.token = null
|
|
||||||
state.tokenData = null
|
|
||||||
state.profile = null
|
state.profile = null
|
||||||
state.availablePermissions = {}
|
state.availablePermissions = {}
|
||||||
}
|
}
|
||||||
|
@ -63,24 +51,15 @@ export default {
|
||||||
state.profile.avatar = value
|
state.profile.avatar = value
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
token: (state, value) => {
|
|
||||||
state.token = value
|
|
||||||
if (value) {
|
|
||||||
state.tokenData = jwtDecode(value)
|
|
||||||
} else {
|
|
||||||
state.tokenData = {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permission: (state, {key, status}) => {
|
permission: (state, {key, status}) => {
|
||||||
state.availablePermissions[key] = status
|
state.availablePermissions[key] = status
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
// Send a request to the login URL and save the returned JWT
|
// Send a request to the login URL
|
||||||
login ({commit, dispatch}, {next, credentials, onError}) {
|
login ({commit, dispatch}, {next, credentials, onError}) {
|
||||||
return axios.post('token/', credentials).then(response => {
|
return axios.post('auth/login/', credentials).then(response => {
|
||||||
logger.default.info('Successfully logged in as', credentials.username)
|
logger.default.info('Successfully logged in as', credentials.username)
|
||||||
commit('token', response.data.token)
|
|
||||||
dispatch('fetchProfile').then(() => {
|
dispatch('fetchProfile').then(() => {
|
||||||
// Redirect to a specified route
|
// Redirect to a specified route
|
||||||
router.push(next)
|
router.push(next)
|
||||||
|
@ -102,29 +81,28 @@ export default {
|
||||||
modules.forEach(m => {
|
modules.forEach(m => {
|
||||||
commit(`${m}/reset`, null, {root: true})
|
commit(`${m}/reset`, null, {root: true})
|
||||||
})
|
})
|
||||||
logger.default.info('Log out, goodbye!')
|
return axios.post('auth/logout/').then(response => {
|
||||||
router.push({name: 'index'})
|
logger.default.info('Successfully logged out')
|
||||||
|
}, response => {
|
||||||
|
// we cannot contact the backend but we can at least clear our local cookies
|
||||||
|
logger.default.info('Backend unreachable, cleaning local cookies…')
|
||||||
|
}).finally(() => {
|
||||||
|
logger.default.info('Log out, goodbye!')
|
||||||
|
router.push({name: 'index'})
|
||||||
|
})
|
||||||
},
|
},
|
||||||
check ({commit, dispatch, state}) {
|
check ({commit, dispatch, state}) {
|
||||||
logger.default.info('Checking authentication...')
|
logger.default.info('Checking authentication...')
|
||||||
var jwt = state.token
|
dispatch('fetchProfile').then(() => {
|
||||||
if (jwt) {
|
logger.default.info('Welcome back!')
|
||||||
commit('token', jwt)
|
}).catch(() => {
|
||||||
dispatch('fetchProfile')
|
|
||||||
dispatch('refreshToken')
|
|
||||||
} else {
|
|
||||||
logger.default.info('Anonymous user')
|
logger.default.info('Anonymous user')
|
||||||
commit('authenticated', false)
|
commit('authenticated', false)
|
||||||
}
|
})
|
||||||
},
|
},
|
||||||
fetchProfile ({commit, dispatch, state}) {
|
fetchProfile ({commit, dispatch, state}) {
|
||||||
if (document) {
|
|
||||||
// this is to ensure we do not have any leaking cookie set by django
|
|
||||||
document.cookie = 'sessionid=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;'
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
axios.get('users/users/me/').then((response) => {
|
axios.get('users/users/me/', {skipLoginRedirect: true}).then((response) => {
|
||||||
logger.default.info('Successfully fetched user profile')
|
logger.default.info('Successfully fetched user profile')
|
||||||
dispatch('updateProfile', response.data).then(() => {
|
dispatch('updateProfile', response.data).then(() => {
|
||||||
resolve(response.data)
|
resolve(response.data)
|
||||||
|
@ -156,13 +134,5 @@ export default {
|
||||||
resolve()
|
resolve()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
refreshToken ({commit, dispatch, state}) {
|
|
||||||
return axios.post('token/refresh/', {token: state.token}).then(response => {
|
|
||||||
logger.default.info('Refreshed auth token')
|
|
||||||
commit('token', response.data.token)
|
|
||||||
}, response => {
|
|
||||||
logger.default.error('Error while refreshing token', response.data)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,7 +114,6 @@ export default {
|
||||||
commit(`${m}/reset`, null, {root: true})
|
commit(`${m}/reset`, null, {root: true})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
// Send a request to the login URL and save the returned JWT
|
|
||||||
fetchSettings ({commit}, payload) {
|
fetchSettings ({commit}, payload) {
|
||||||
return axios.get('instance/settings/').then(response => {
|
return axios.get('instance/settings/').then(response => {
|
||||||
logger.default.info('Successfully fetched instance settings')
|
logger.default.info('Successfully fetched instance settings')
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
export default {
|
||||||
|
get (name) {
|
||||||
|
var value = "; " + document.cookie;
|
||||||
|
var parts = value.split("; " + name + "=");
|
||||||
|
if (parts.length == 2) return parts.pop().split(";").shift();
|
||||||
|
}
|
||||||
|
}
|
|
@ -37,54 +37,21 @@ describe('store/auth', () => {
|
||||||
it('authenticated false', () => {
|
it('authenticated false', () => {
|
||||||
const state = {
|
const state = {
|
||||||
username: 'dummy',
|
username: 'dummy',
|
||||||
token: 'dummy',
|
|
||||||
tokenData: 'dummy',
|
|
||||||
profile: 'dummy',
|
profile: 'dummy',
|
||||||
availablePermissions: 'dummy'
|
availablePermissions: 'dummy'
|
||||||
}
|
}
|
||||||
store.mutations.authenticated(state, false)
|
store.mutations.authenticated(state, false)
|
||||||
expect(state.authenticated).to.equal(false)
|
expect(state.authenticated).to.equal(false)
|
||||||
expect(state.username).to.equal(null)
|
expect(state.username).to.equal(null)
|
||||||
expect(state.token).to.equal(null)
|
|
||||||
expect(state.tokenData).to.equal(null)
|
|
||||||
expect(state.profile).to.equal(null)
|
expect(state.profile).to.equal(null)
|
||||||
expect(state.availablePermissions).to.deep.equal({})
|
expect(state.availablePermissions).to.deep.equal({})
|
||||||
})
|
})
|
||||||
it('token null', () => {
|
|
||||||
const state = {}
|
|
||||||
store.mutations.token(state, null)
|
|
||||||
expect(state.token).to.equal(null)
|
|
||||||
expect(state.tokenData).to.deep.equal({})
|
|
||||||
})
|
|
||||||
it('token real', () => {
|
|
||||||
// generated on http://kjur.github.io/jsjws/tool_jwt.html
|
|
||||||
const state = {}
|
|
||||||
let token = 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpc3MiOiJodHRwczovL2p3dC1pZHAuZXhhbXBsZS5jb20iLCJzdWIiOiJtYWlsdG86bWlrZUBleGFtcGxlLmNvbSIsIm5iZiI6MTUxNTUzMzQyOSwiZXhwIjoxNTE1NTM3MDI5LCJpYXQiOjE1MTU1MzM0MjksImp0aSI6ImlkMTIzNDU2IiwidHlwIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9yZWdpc3RlciJ9.'
|
|
||||||
let tokenData = {
|
|
||||||
iss: 'https://jwt-idp.example.com',
|
|
||||||
sub: 'mailto:mike@example.com',
|
|
||||||
nbf: 1515533429,
|
|
||||||
exp: 1515537029,
|
|
||||||
iat: 1515533429,
|
|
||||||
jti: 'id123456',
|
|
||||||
typ: 'https://example.com/register'
|
|
||||||
}
|
|
||||||
store.mutations.token(state, token)
|
|
||||||
expect(state.token).to.equal(token)
|
|
||||||
expect(state.tokenData).to.deep.equal(tokenData)
|
|
||||||
})
|
|
||||||
it('permissions', () => {
|
it('permissions', () => {
|
||||||
const state = { availablePermissions: {} }
|
const state = { availablePermissions: {} }
|
||||||
store.mutations.permission(state, {key: 'admin', status: true})
|
store.mutations.permission(state, {key: 'admin', status: true})
|
||||||
expect(state.availablePermissions).to.deep.equal({admin: true})
|
expect(state.availablePermissions).to.deep.equal({admin: true})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
describe('getters', () => {
|
|
||||||
it('header', () => {
|
|
||||||
const state = { token: 'helloworld' }
|
|
||||||
expect(store.getters['header'](state)).to.equal('JWT helloworld')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
describe('actions', () => {
|
describe('actions', () => {
|
||||||
it('logout', () => {
|
it('logout', () => {
|
||||||
testAction({
|
testAction({
|
||||||
|
@ -100,30 +67,8 @@ describe('store/auth', () => {
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
it('check jwt null', () => {
|
|
||||||
testAction({
|
|
||||||
action: store.actions.check,
|
|
||||||
params: {state: {}},
|
|
||||||
expectedMutations: [
|
|
||||||
{ type: 'authenticated', payload: false }
|
|
||||||
]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
it('check jwt set', () => {
|
|
||||||
testAction({
|
|
||||||
action: store.actions.check,
|
|
||||||
params: {state: {token: 'test', username: 'user'}},
|
|
||||||
expectedMutations: [
|
|
||||||
{ type: 'token', payload: 'test' }
|
|
||||||
],
|
|
||||||
expectedActions: [
|
|
||||||
{ type: 'fetchProfile' },
|
|
||||||
{ type: 'refreshToken' }
|
|
||||||
]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
it('login success', () => {
|
it('login success', () => {
|
||||||
moxios.stubRequest('token/', {
|
moxios.stubRequest('auth/login/', {
|
||||||
status: 200,
|
status: 200,
|
||||||
response: {
|
response: {
|
||||||
token: 'test'
|
token: 'test'
|
||||||
|
@ -135,9 +80,7 @@ describe('store/auth', () => {
|
||||||
testAction({
|
testAction({
|
||||||
action: store.actions.login,
|
action: store.actions.login,
|
||||||
payload: {credentials: credentials},
|
payload: {credentials: credentials},
|
||||||
expectedMutations: [
|
expectedMutations: [],
|
||||||
{ type: 'token', payload: 'test' }
|
|
||||||
],
|
|
||||||
expectedActions: [
|
expectedActions: [
|
||||||
{ type: 'fetchProfile' }
|
{ type: 'fetchProfile' }
|
||||||
]
|
]
|
||||||
|
@ -187,18 +130,5 @@ describe('store/auth', () => {
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
it('refreshToken', () => {
|
|
||||||
moxios.stubRequest('token/refresh/', {
|
|
||||||
status: 200,
|
|
||||||
response: {token: 'newtoken'}
|
|
||||||
})
|
|
||||||
testAction({
|
|
||||||
action: store.actions.refreshToken,
|
|
||||||
params: {state: {token: 'oldtoken'}},
|
|
||||||
expectedMutations: [
|
|
||||||
{ type: 'token', payload: 'newtoken' }
|
|
||||||
]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue