Skip to content

Commit 737c22f

Browse files
authored
Merge pull request #234 from rolweber/websocket_auth
Websocket auth with token as query param
2 parents 317033f + 6598e17 commit 737c22f

File tree

5 files changed

+158
-16
lines changed

5 files changed

+158
-16
lines changed

docs/source/devinstall.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ make docs
6969
After modifying any of the APIs in `jupyter-websocket` mode, you must update the project's Swagger API specification.
7070

7171
1. Load the current
72-
[swagger.yaml](https://github.com/jupyter/kernel_gateway/blob/master/kernel_gateway/services/api/swagger.yaml) file into the [Swagger editor](http://editor.swagger.io/#/).
72+
[swagger.yaml](https://github.com/jupyter/kernel_gateway/blob/master/kernel_gateway/jupyter_websocket/swagger.yaml) file into the [Swagger editor](http://editor.swagger.io/#/).
7373
2. Make your changes.
7474
3. Export both the `swagger.json` and `swagger.yaml` files.
75-
4. Place the files in `kernel_gateway/services/api`.
75+
4. Place the files in `kernel_gateway/jupyter_websocket`.
7676
5. Add, commit, and PR the changes.

kernel_gateway/jupyter_websocket/swagger.json

+12-3
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,17 @@
3434
}
3535
},
3636
"securityDefinitions": {
37-
"token": {
37+
"tokenHeader": {
3838
"type": "apiKey",
3939
"name": "Authorization",
4040
"in": "header",
41-
"description": "The authorization token to verify authorization. This is only needed when `KernelGatewayApp.auth_token` is set. This should take the form of `token {value}` where `{value}` is the value of the token."
41+
"description": "The authorization token to verify authorization. This is only needed when `KernelGatewayApp.auth_token` is set. This should take the form of `token {value}` where `{value}` is the value of the token. Alternatively, the token can be passed as a query parameter."
42+
},
43+
"tokenParam": {
44+
"type": "apiKey",
45+
"name": "token",
46+
"in": "query",
47+
"description": "The authorization token to verify authorization. This is only needed when `KernelGatewayApp.auth_token` is set. This should take the form of `token={value}` where `{value}` is the value of the token. Alternatively, the token can be passed as a header."
4248
}
4349
},
4450
"paths": {
@@ -91,7 +97,10 @@
9197
"get": {
9298
"security": [
9399
{
94-
"token": []
100+
"tokenHeader": []
101+
},
102+
{
103+
"tokenParam": []
95104
}
96105
],
97106
"summary": "Get kernel specs",

kernel_gateway/jupyter_websocket/swagger.yaml

+11-3
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,20 @@ parameters:
2929
format: uuid
3030

3131
securityDefinitions:
32-
token:
32+
tokenHeader:
3333
type: apiKey
3434
name: Authorization
3535
in: header
3636
description: The authorization token to verify authorization. This is only
3737
needed when `KernelGatewayApp.auth_token` is set. This should take the
38-
form of `token {value}` where `{value}` is the value of the token.
38+
form of `token {value}` where `{value}` is the value of the token. Alternatively, the token can be passed as a query parameter.
39+
tokenParam:
40+
type: apiKey
41+
name: token
42+
in: query
43+
description: The authorization token to verify authorization. This is only
44+
needed when `KernelGatewayApp.auth_token` is set. This should take the
45+
form of `token={value}` where `{value}` is the value of the token. Alternatively, the token can be passed as a header.
3946

4047
paths:
4148
/api:
@@ -69,7 +76,8 @@ paths:
6976
/api/kernelspecs:
7077
get:
7178
security:
72-
- token: []
79+
- tokenHeader: []
80+
- tokenParam: []
7381
summary: Get kernel specs
7482
tags:
7583
- kernelspecs

kernel_gateway/mixins.py

+15-5
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,15 @@ class TokenAuthorizationMixin(object):
3939
"""Mixes token auth into tornado.web.RequestHandlers and
4040
tornado.websocket.WebsocketHandlers.
4141
"""
42+
header_prefix = "token "
43+
header_prefix_len = len(header_prefix)
44+
4245
def prepare(self):
43-
"""Ensures the correct `Authorization: token <value>` is present in
44-
the request's header if an auth token is configured.
46+
"""Ensures the correct auth token is present, either as a parameter
47+
`token=<value>` or as a header `Authorization: token <value>`.
48+
Does nothing unless an auth token is configured in kg_auth_token.
4549
46-
If kg_auth_token is set and the token is not in the header, responds
50+
If kg_auth_token is set and the token is not present, responds
4751
with 401 Unauthorized.
4852
4953
Notes
@@ -54,8 +58,14 @@ def prepare(self):
5458
"""
5559
server_token = self.settings.get('kg_auth_token')
5660
if server_token:
57-
client_token = self.request.headers.get('Authorization')
58-
if client_token != 'token %s' % server_token:
61+
client_token = self.get_argument('token', None)
62+
if client_token is None:
63+
client_token = self.request.headers.get('Authorization')
64+
if client_token and client_token.startswith(self.header_prefix):
65+
client_token = client_token[self.header_prefix_len:]
66+
else:
67+
client_token = None
68+
if client_token != server_token:
5969
return self.send_error(401)
6070
return super(TokenAuthorizationMixin, self).prepare()
6171

kernel_gateway/tests/test_mixins.py

+118-3
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,125 @@
44

55
import json
66
import unittest
7+
8+
try:
9+
from unittest.mock import Mock, MagicMock
10+
except ImportError:
11+
# Python 2.7: use backport
12+
from mock import Mock, MagicMock
13+
714
from tornado import web
8-
from kernel_gateway.mixins import JSONErrorsMixin
15+
from kernel_gateway.mixins import TokenAuthorizationMixin, JSONErrorsMixin
16+
17+
class SuperTokenAuthHandler(object):
18+
"""Super class for the handler using TokenAuthorizationMixin."""
19+
is_prepared = False
20+
21+
def prepare(self):
22+
# called by the mixin when authentication succeeds
23+
self.is_prepared = True
24+
25+
class TestableTokenAuthHandler(TokenAuthorizationMixin, SuperTokenAuthHandler):
26+
"""Implementation that uses the TokenAuthorizationMixin for testing."""
27+
def __init__(self, token=''):
28+
self.settings = { 'kg_auth_token': token }
29+
self.arguments = {}
30+
self.response = None
31+
self.status_code = None
32+
33+
def send_error(self, status_code):
34+
self.status_code = status_code
35+
36+
def get_argument(self, name, default=''):
37+
return self.arguments.get(name, default)
38+
39+
40+
class TestTokenAuthMixin(unittest.TestCase):
41+
"""Unit tests the Token authorization mixin."""
42+
def setUp(self):
43+
"""Creates a handler that uses the mixin."""
44+
self.mixin = TestableTokenAuthHandler('YouKnowMe')
45+
46+
def test_no_token_required(self):
47+
"""Status should be None."""
48+
self.mixin.settings['kg_auth_token'] = ''
49+
self.mixin.prepare()
50+
self.assertEqual(self.mixin.is_prepared, True)
51+
self.assertEqual(self.mixin.status_code, None)
52+
53+
def test_missing_token(self):
54+
"""Status should be 'unauthorized'."""
55+
attrs = { 'headers' : {
56+
} }
57+
self.mixin.request = Mock(**attrs)
58+
self.mixin.prepare()
59+
self.assertEqual(self.mixin.is_prepared, False)
60+
self.assertEqual(self.mixin.status_code, 401)
61+
62+
def test_valid_header_token(self):
63+
"""Status should be None."""
64+
attrs = { 'headers' : {
65+
'Authorization' : 'token YouKnowMe'
66+
} }
67+
self.mixin.request = Mock(**attrs)
68+
self.mixin.prepare()
69+
self.assertEqual(self.mixin.is_prepared, True)
70+
self.assertEqual(self.mixin.status_code, None)
71+
72+
def test_wrong_header_token(self):
73+
"""Status should be 'unauthorized'."""
74+
attrs = { 'headers' : {
75+
'Authorization' : 'token NeverHeardOf'
76+
} }
77+
self.mixin.request = Mock(**attrs)
78+
self.mixin.prepare()
79+
self.assertEqual(self.mixin.is_prepared, False)
80+
self.assertEqual(self.mixin.status_code, 401)
81+
82+
def test_valid_url_token(self):
83+
"""Status should be None."""
84+
self.mixin.arguments['token'] = 'YouKnowMe'
85+
attrs = { 'headers' : {
86+
} }
87+
self.mixin.request = Mock(**attrs)
88+
self.mixin.prepare()
89+
self.assertEqual(self.mixin.is_prepared, True)
90+
self.assertEqual(self.mixin.status_code, None)
91+
92+
def test_wrong_url_token(self):
93+
"""Status should be 'unauthorized'."""
94+
self.mixin.arguments['token'] = 'NeverHeardOf'
95+
attrs = { 'headers' : {
96+
} }
97+
self.mixin.request = Mock(**attrs)
98+
self.mixin.prepare()
99+
self.assertEqual(self.mixin.is_prepared, False)
100+
self.assertEqual(self.mixin.status_code, 401)
101+
102+
def test_differing_tokens_valid_url(self):
103+
"""Status should be None, URL token takes precedence"""
104+
self.mixin.arguments['token'] = 'YouKnowMe'
105+
attrs = { 'headers' : {
106+
'Authorization' : 'token NeverHeardOf'
107+
} }
108+
self.mixin.request = Mock(**attrs)
109+
self.mixin.prepare()
110+
self.assertEqual(self.mixin.is_prepared, True)
111+
self.assertEqual(self.mixin.status_code, None)
112+
113+
def test_differing_tokens_wrong_url(self):
114+
"""Status should be 'unauthorized', URL token takes precedence"""
115+
attrs = { 'headers' : {
116+
'Authorization' : 'token YouKnowMe'
117+
} }
118+
self.mixin.request = Mock(**attrs)
119+
self.mixin.arguments['token'] = 'NeverHeardOf'
120+
self.mixin.prepare()
121+
self.assertEqual(self.mixin.is_prepared, False)
122+
self.assertEqual(self.mixin.status_code, 401)
123+
9124

10-
class TestableHandler(JSONErrorsMixin):
125+
class TestableJSONErrorsHandler(JSONErrorsMixin):
11126
"""Implementation that uses the JSONErrorsMixin for testing."""
12127
def __init__(self):
13128
self.headers = {}
@@ -27,7 +142,7 @@ class TestJSONErrorsMixin(unittest.TestCase):
27142
"""Unit tests the JSON errors mixin."""
28143
def setUp(self):
29144
"""Creates a handler that uses the mixin."""
30-
self.mixin = TestableHandler()
145+
self.mixin = TestableJSONErrorsHandler()
31146

32147
def test_status(self):
33148
"""Status should be set on the response."""

0 commit comments

Comments
 (0)